@vidtreo/recorder 0.9.2 → 0.9.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -138,6 +138,52 @@ export declare class VidtreoRecorder {
138
138
  private ensureInitialized;
139
139
  }
140
140
 
141
+ export type NativeCameraFile = {
142
+ file: File;
143
+ previewUrl: string;
144
+ duration: number;
145
+ validated: boolean;
146
+ };
147
+ export type FileValidationResult = {
148
+ valid: boolean;
149
+ error?: string;
150
+ };
151
+ export type NativeCameraConfig = {
152
+ maxFileSize?: number;
153
+ maxDuration?: number;
154
+ allowedFormats?: string[];
155
+ };
156
+
157
+ export declare function validateFile(file: File, config?: {
158
+ maxFileSize?: number;
159
+ maxRecordingTime?: number | null;
160
+ allowedFormats?: string[];
161
+ }): Promise<FileValidationResult>;
162
+
163
+ import type { RecordingStopResult } from "../../vidtreo-recorder";
164
+ import type { ConfigService } from "../config/config-service";
165
+ import type { VideoUploadService } from "../upload/upload-service";
166
+ export type NativeCameraHandlerConfig = {
167
+ apiKey?: string | null;
168
+ backendUrl?: string | null;
169
+ maxRecordingTime?: number | null;
170
+ maxFileSize?: number;
171
+ userMetadata?: Record<string, unknown>;
172
+ };
173
+ export declare class NativeCameraHandler {
174
+ private pendingFile;
175
+ private readonly configService;
176
+ private readonly uploadService;
177
+ private readonly config;
178
+ constructor(config: NativeCameraHandlerConfig, configService: ConfigService | null, uploadService: VideoUploadService);
179
+ handleFileSelection(file: File): Promise<NativeCameraFile>;
180
+ processAndUpload(onTranscodeProgress: (progress: number) => void, onUploadProgress: (progress: number) => void): Promise<RecordingStopResult>;
181
+ cancel(): void;
182
+ preloadConfig(): Promise<void>;
183
+ }
184
+
185
+ export declare function extractLastFrame(file: File, timeoutMs?: number): Promise<Blob>;
186
+
141
187
  export type VideoUploadOptions = {
142
188
  apiKey: string;
143
189
  backendUrl: string;
@@ -278,6 +324,8 @@ export declare function requireInitialized<T>(value: T | null | undefined, compo
278
324
  export declare function requireStream(stream: MediaStream | null, message?: string): MediaStream;
279
325
  export declare function requireProcessor<T>(processor: T | null, componentName?: string): T;
280
326
 
327
+ export declare function isMobileDevice(): boolean;
328
+
281
329
  export declare function extractErrorMessage(error: unknown): string;
282
330
 
283
331
  export declare const FILE_SIZE_UNITS: readonly ["Bytes", "KB", "MB", "GB"];
@@ -285,7 +333,7 @@ export declare const FILE_SIZE_BASE = 1024;
285
333
  export declare function formatFileSize(bytes: number): string;
286
334
  export declare function formatTime(totalSeconds: number): string;
287
335
 
288
- export declare function extractVideoDuration(blob: Blob): Promise<number>;
336
+ export declare function extractVideoDuration(file: File | Blob): Promise<number>;
289
337
 
290
338
  export type StorageQuota = {
291
339
  usage: number;
@@ -888,8 +936,9 @@ export type BackendConfigResponse = {
888
936
  presetEncoding: BackendPreset;
889
937
  max_width: number;
890
938
  max_height: number;
939
+ outputFormat?: "mp4" | "webm" | "mkv" | "mov";
891
940
  };
892
- export declare function mapPresetToConfig(preset: BackendPreset, maxWidth: number, maxHeight: number, format?: TranscodeConfig["format"]): TranscodeConfig;
941
+ export declare function mapPresetToConfig(preset: BackendPreset, maxWidth: number, maxHeight: number, outputFormat?: TranscodeConfig["format"]): TranscodeConfig;
893
942
 
894
943
  import type { TranscodeConfig } from "../transcode/transcode-types";
895
944
  export declare const DEFAULT_BACKEND_URL = "https://api.vidtreo.com";
@@ -920,6 +969,7 @@ export type TranscodeResult = {
920
969
  };
921
970
 
922
971
  export declare function transcodeVideo(input: TranscodeInput, config?: Partial<TranscodeConfig>, onProgress?: (progress: number) => void): Promise<TranscodeResult>;
972
+ export declare function transcodeVideoForNativeCamera(file: File, config?: Partial<TranscodeConfig>, onProgress?: (progress: number) => void): Promise<TranscodeResult>;
923
973
 
924
974
  import type { AudioLevelCallbacks } from "../audio/types";
925
975
  import type { DeviceCallbacks } from "../device/types";
@@ -950,6 +1000,7 @@ export type RecorderConfig = {
950
1000
  enableDeviceChange?: boolean;
951
1001
  enableTabVisibilityOverlay?: boolean;
952
1002
  tabVisibilityOverlayText?: string;
1003
+ nativeCamera?: boolean;
953
1004
  };
954
1005
  export type RecorderCallbacks = {
955
1006
  recording?: Partial<RecordingCallbacks>;
package/dist/index.js CHANGED
@@ -203,7 +203,7 @@ var QUALITY_MAP = {
203
203
  "4k": QUALITY_VERY_HIGH
204
204
  };
205
205
  var AUDIO_BITRATE = 128000;
206
- function mapPresetToConfig(preset, maxWidth, maxHeight, format = "mp4") {
206
+ function mapPresetToConfig(preset, maxWidth, maxHeight, outputFormat) {
207
207
  if (!(preset in QUALITY_MAP)) {
208
208
  throw new Error(`Invalid preset: ${preset}`);
209
209
  }
@@ -213,6 +213,7 @@ function mapPresetToConfig(preset, maxWidth, maxHeight, format = "mp4") {
213
213
  if (typeof maxHeight !== "number" || maxHeight <= 0) {
214
214
  throw new Error("maxHeight must be a positive number");
215
215
  }
216
+ const format = outputFormat || "mp4";
216
217
  const audioCodec = getDefaultAudioCodecForFormat(format);
217
218
  return {
218
219
  format,
@@ -310,7 +311,7 @@ class ConfigService {
310
311
  if (!data.presetEncoding || typeof data.max_width !== "number" || typeof data.max_height !== "number") {
311
312
  throw new Error("Invalid config response from backend");
312
313
  }
313
- return mapPresetToConfig(data.presetEncoding, data.max_width, data.max_height);
314
+ return mapPresetToConfig(data.presetEncoding, data.max_width, data.max_height, data.outputFormat);
314
315
  }
315
316
  }
316
317
 
@@ -403,6 +404,406 @@ class DeviceManager {
403
404
  return this.availableDevices;
404
405
  }
405
406
  }
407
+ // src/core/utils/video-utils.ts
408
+ import { BlobSource, Input, MP4 } from "mediabunny";
409
+ async function extractVideoDuration(file) {
410
+ try {
411
+ const source = new BlobSource(file);
412
+ const input = new Input({
413
+ formats: [MP4],
414
+ source
415
+ });
416
+ if (typeof input.computeDuration !== "function") {
417
+ throw new Error("computeDuration method is not available");
418
+ }
419
+ const duration = await input.computeDuration();
420
+ if (!duration) {
421
+ throw new Error("Duration is missing from computeDuration");
422
+ }
423
+ if (duration <= 0) {
424
+ throw new Error("Invalid duration: must be greater than 0");
425
+ }
426
+ return duration;
427
+ } catch {
428
+ return extractDurationWithVideoElement(file);
429
+ }
430
+ }
431
+ function extractDurationWithVideoElement(blob) {
432
+ return new Promise((resolve, reject) => {
433
+ const video = document.createElement("video");
434
+ const url = URL.createObjectURL(blob);
435
+ const cleanup = () => {
436
+ URL.revokeObjectURL(url);
437
+ };
438
+ video.addEventListener("loadedmetadata", () => {
439
+ cleanup();
440
+ const duration = video.duration;
441
+ if (!Number.isFinite(duration) || duration <= 0) {
442
+ reject(new Error("Invalid video duration"));
443
+ return;
444
+ }
445
+ resolve(duration);
446
+ });
447
+ video.addEventListener("error", () => {
448
+ cleanup();
449
+ reject(new Error("Failed to load video metadata"));
450
+ });
451
+ video.src = url;
452
+ video.load();
453
+ });
454
+ }
455
+
456
+ // src/core/native-camera/file-validator.ts
457
+ var DEFAULT_MAX_FILE_SIZE = 500 * 1024 * 1024;
458
+ var ALLOWED_FORMATS = ["video/mp4", "video/quicktime", "video/webm"];
459
+ async function validateFile(file, config = {}) {
460
+ const maxSize = config.maxFileSize !== undefined ? config.maxFileSize : DEFAULT_MAX_FILE_SIZE;
461
+ if (file.size > maxSize) {
462
+ const error = `File too large. Maximum size is ${Math.round(maxSize / 1024 / 1024)}MB`;
463
+ return { valid: false, error };
464
+ }
465
+ const allowedFormats = config.allowedFormats !== undefined ? config.allowedFormats : ALLOWED_FORMATS;
466
+ if (!allowedFormats.includes(file.type)) {
467
+ const error = `Unsupported format. Please use: ${allowedFormats.join(", ")}`;
468
+ return { valid: false, error };
469
+ }
470
+ const durationResult = await extractVideoDuration(file).then((videoDuration) => ({
471
+ success: true,
472
+ duration: videoDuration
473
+ })).catch((error) => ({
474
+ success: false,
475
+ error: extractErrorMessage(error)
476
+ }));
477
+ if (durationResult.success === false) {
478
+ return {
479
+ valid: false,
480
+ error: `Failed to read video file: ${durationResult.error}`
481
+ };
482
+ }
483
+ const duration = durationResult.duration;
484
+ if (config.maxRecordingTime !== null && config.maxRecordingTime !== undefined && duration > config.maxRecordingTime) {
485
+ const error = `Video too long. Maximum duration is ${config.maxRecordingTime} seconds`;
486
+ return { valid: false, error };
487
+ }
488
+ return { valid: true };
489
+ }
490
+ // src/core/transcode/video-transcoder.ts
491
+ import {
492
+ ALL_FORMATS,
493
+ BlobSource as BlobSource2,
494
+ BufferTarget,
495
+ Conversion,
496
+ FilePathSource,
497
+ Input as Input2,
498
+ MkvOutputFormat,
499
+ MovOutputFormat,
500
+ Mp4OutputFormat,
501
+ Output,
502
+ WebMOutputFormat
503
+ } from "mediabunny";
504
+ function createSource(input) {
505
+ if (typeof input === "string") {
506
+ return new FilePathSource(input);
507
+ }
508
+ if (input instanceof Blob) {
509
+ return new BlobSource2(input);
510
+ }
511
+ throw new Error("Invalid input type. Expected Blob, File, or file path string.");
512
+ }
513
+ function createOutputFormat(format) {
514
+ switch (format) {
515
+ case "mp4":
516
+ return new Mp4OutputFormat;
517
+ case "webm":
518
+ return new WebMOutputFormat;
519
+ case "mkv":
520
+ return new MkvOutputFormat;
521
+ case "mov":
522
+ return new MovOutputFormat;
523
+ default:
524
+ throw new Error(`Unsupported output format: ${format}`);
525
+ }
526
+ }
527
+ function getMimeTypeForFormat(format) {
528
+ switch (format) {
529
+ case "mp4":
530
+ return "video/mp4";
531
+ case "webm":
532
+ return "video/webm";
533
+ case "mkv":
534
+ return "video/x-matroska";
535
+ case "mov":
536
+ return "video/quicktime";
537
+ default:
538
+ throw new Error(`Unsupported output format: ${format}`);
539
+ }
540
+ }
541
+ function createConversionOptions(config, optimizeForSpeed = false) {
542
+ const audioCodec = getAudioCodecForFormat(config.format, config.audioCodec);
543
+ const video = {
544
+ fit: "contain",
545
+ forceTranscode: true
546
+ };
547
+ if (config.width !== undefined) {
548
+ video.width = config.width;
549
+ }
550
+ if (config.height !== undefined) {
551
+ video.height = config.height;
552
+ }
553
+ if (config.fps !== undefined) {
554
+ video.frameRate = config.fps;
555
+ }
556
+ if (config.bitrate !== undefined) {
557
+ video.bitrate = config.bitrate;
558
+ }
559
+ if (config.codec !== undefined) {
560
+ video.codec = config.codec;
561
+ }
562
+ if (optimizeForSpeed) {
563
+ video.bitrateMode = "variable";
564
+ video.latencyMode = "realtime";
565
+ video.hardwareAcceleration = "prefer-hardware";
566
+ video.keyFrameInterval = 2;
567
+ }
568
+ const audio = {
569
+ codec: audioCodec,
570
+ forceTranscode: true,
571
+ ...optimizeForSpeed && { bitrateMode: "variable" }
572
+ };
573
+ return { video, audio };
574
+ }
575
+ function validateConversion(conversion) {
576
+ if (!conversion.isValid) {
577
+ const reasons = conversion.discardedTracks.map((track) => track.reason).join(", ");
578
+ throw new Error(`Conversion is invalid. Discarded tracks: ${reasons}`);
579
+ }
580
+ }
581
+ async function transcodeVideo(input, config = {}, onProgress) {
582
+ const finalConfig = {
583
+ ...DEFAULT_TRANSCODE_CONFIG,
584
+ ...config,
585
+ format: config.format || DEFAULT_TRANSCODE_CONFIG.format
586
+ };
587
+ if (!finalConfig.audioCodec) {
588
+ finalConfig.audioCodec = getDefaultAudioCodecForFormat(finalConfig.format);
589
+ }
590
+ const source = createSource(input);
591
+ const mediabunnyInput = new Input2({
592
+ formats: ALL_FORMATS,
593
+ source
594
+ });
595
+ const outputFormat = createOutputFormat(finalConfig.format);
596
+ const output = new Output({
597
+ format: outputFormat,
598
+ target: new BufferTarget
599
+ });
600
+ const conversion = await Conversion.init({
601
+ input: mediabunnyInput,
602
+ output,
603
+ ...createConversionOptions(finalConfig)
604
+ });
605
+ validateConversion(conversion);
606
+ if (onProgress) {
607
+ conversion.onProgress = onProgress;
608
+ }
609
+ await conversion.execute();
610
+ const buffer = output.target.buffer;
611
+ if (!buffer) {
612
+ throw new Error("Transcoding completed but no output buffer was generated");
613
+ }
614
+ const mimeType = getMimeTypeForFormat(finalConfig.format);
615
+ return {
616
+ buffer,
617
+ blob: new Blob([buffer], { type: mimeType })
618
+ };
619
+ }
620
+ async function transcodeVideoForNativeCamera(file, config = {}, onProgress) {
621
+ const finalConfig = {
622
+ ...DEFAULT_TRANSCODE_CONFIG,
623
+ ...config,
624
+ format: config.format || DEFAULT_TRANSCODE_CONFIG.format
625
+ };
626
+ if (!finalConfig.audioCodec) {
627
+ finalConfig.audioCodec = getDefaultAudioCodecForFormat(finalConfig.format);
628
+ }
629
+ const source = new BlobSource2(file);
630
+ const mediabunnyInput = new Input2({
631
+ formats: ALL_FORMATS,
632
+ source
633
+ });
634
+ const outputFormat = createOutputFormat(finalConfig.format);
635
+ const output = new Output({
636
+ format: outputFormat,
637
+ target: new BufferTarget
638
+ });
639
+ const conversion = await Conversion.init({
640
+ input: mediabunnyInput,
641
+ output,
642
+ video: {
643
+ codec: finalConfig.codec,
644
+ width: finalConfig.width,
645
+ height: finalConfig.height,
646
+ frameRate: finalConfig.fps,
647
+ bitrate: finalConfig.bitrate,
648
+ fit: "contain"
649
+ },
650
+ audio: {
651
+ codec: finalConfig.audioCodec
652
+ }
653
+ });
654
+ validateConversion(conversion);
655
+ if (onProgress) {
656
+ conversion.onProgress = onProgress;
657
+ }
658
+ await conversion.execute();
659
+ const buffer = output.target.buffer;
660
+ if (!buffer) {
661
+ throw new Error("Transcoding completed but no output buffer was generated");
662
+ }
663
+ const mimeType = getMimeTypeForFormat(finalConfig.format);
664
+ return {
665
+ buffer,
666
+ blob: new Blob([buffer], { type: mimeType })
667
+ };
668
+ }
669
+
670
+ // src/core/native-camera/preview-extractor.ts
671
+ function extractLastFrame(file, timeoutMs = 1e4) {
672
+ return new Promise((resolve, reject) => {
673
+ const video = document.createElement("video");
674
+ video.preload = "metadata";
675
+ video.muted = true;
676
+ const timeout = setTimeout(() => {
677
+ cleanup();
678
+ reject(new Error("Preview extraction timeout"));
679
+ }, timeoutMs);
680
+ const cleanup = () => {
681
+ clearTimeout(timeout);
682
+ URL.revokeObjectURL(video.src);
683
+ };
684
+ video.addEventListener("loadedmetadata", () => {
685
+ video.currentTime = Math.max(0, video.duration - 0.1);
686
+ });
687
+ video.addEventListener("seeked", () => {
688
+ const canvas = document.createElement("canvas");
689
+ canvas.width = video.videoWidth;
690
+ canvas.height = video.videoHeight;
691
+ const ctx = canvas.getContext("2d");
692
+ if (!ctx) {
693
+ cleanup();
694
+ reject(new Error("Canvas context unavailable"));
695
+ return;
696
+ }
697
+ if (video.videoWidth === 0 || video.videoHeight === 0) {
698
+ cleanup();
699
+ reject(new Error("Invalid video dimensions"));
700
+ return;
701
+ }
702
+ if (!(Number.isFinite(video.videoWidth) && Number.isFinite(video.videoHeight))) {
703
+ cleanup();
704
+ reject(new Error("Invalid video dimensions"));
705
+ return;
706
+ }
707
+ ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
708
+ canvas.toBlob((blob) => {
709
+ cleanup();
710
+ if (blob) {
711
+ resolve(blob);
712
+ } else {
713
+ reject(new Error("Failed to create preview blob"));
714
+ }
715
+ }, "image/jpeg", 0.8);
716
+ });
717
+ video.addEventListener("error", () => {
718
+ cleanup();
719
+ reject(new Error("Failed to load video for preview extraction"));
720
+ });
721
+ const videoUrl = URL.createObjectURL(file);
722
+ video.src = videoUrl;
723
+ });
724
+ }
725
+
726
+ // src/core/native-camera/native-camera-handler.ts
727
+ class NativeCameraHandler {
728
+ pendingFile = null;
729
+ configService = null;
730
+ uploadService;
731
+ config;
732
+ constructor(config, configService, uploadService) {
733
+ this.config = config;
734
+ this.configService = configService;
735
+ this.uploadService = uploadService;
736
+ }
737
+ async handleFileSelection(file) {
738
+ const maxFileSize = this.config.maxFileSize !== undefined ? this.config.maxFileSize : 2 * 1024 * 1024 * 1024;
739
+ const validation = await validateFile(file, {
740
+ maxFileSize,
741
+ maxRecordingTime: this.config.maxRecordingTime,
742
+ allowedFormats: ["video/mp4", "video/quicktime", "video/webm"]
743
+ });
744
+ if (!validation.valid) {
745
+ throw new Error(validation.error);
746
+ }
747
+ const previewBlob = await extractLastFrame(file);
748
+ const previewUrl = URL.createObjectURL(previewBlob);
749
+ const durationResult = await extractVideoDuration(file).then((videoDuration) => ({
750
+ success: true,
751
+ duration: videoDuration
752
+ })).catch((error) => ({
753
+ success: false,
754
+ error: extractErrorMessage(error)
755
+ }));
756
+ if (durationResult.success === false) {
757
+ throw new Error(`Failed to extract video duration: ${durationResult.error}`);
758
+ }
759
+ const duration = durationResult.duration;
760
+ this.pendingFile = file;
761
+ return { file, previewUrl, duration, validated: true };
762
+ }
763
+ async processAndUpload(onTranscodeProgress, onUploadProgress) {
764
+ if (!this.pendingFile) {
765
+ throw new Error("No file selected");
766
+ }
767
+ const transcodeConfig = this.configService ? await this.configService.fetchConfig() : DEFAULT_TRANSCODE_CONFIG;
768
+ const result = await transcodeVideoForNativeCamera(this.pendingFile, transcodeConfig, onTranscodeProgress);
769
+ if (!(this.config.apiKey && this.config.backendUrl)) {
770
+ throw new Error("Upload requires API key and backend URL");
771
+ }
772
+ const durationResult = await extractVideoDuration(result.blob).then((videoDuration) => ({
773
+ success: true,
774
+ duration: videoDuration
775
+ })).catch((error) => ({
776
+ success: false,
777
+ error: extractErrorMessage(error)
778
+ }));
779
+ if (durationResult.success === false) {
780
+ throw new Error(`Failed to extract video duration: ${durationResult.error}`);
781
+ }
782
+ const duration = durationResult.duration;
783
+ const uploadResult = await this.uploadService.uploadVideo(result.blob, {
784
+ apiKey: this.config.apiKey,
785
+ backendUrl: this.config.backendUrl,
786
+ filename: `${Date.now()}.mp4`,
787
+ duration,
788
+ userMetadata: this.config.userMetadata,
789
+ onProgress: onUploadProgress
790
+ });
791
+ this.pendingFile = null;
792
+ return {
793
+ blob: result.blob,
794
+ recordingId: uploadResult.videoId,
795
+ uploadUrl: uploadResult.uploadUrl
796
+ };
797
+ }
798
+ cancel() {
799
+ this.pendingFile = null;
800
+ }
801
+ async preloadConfig() {
802
+ if (this.configService) {
803
+ await this.configService.fetchConfig();
804
+ }
805
+ }
806
+ }
406
807
  // src/core/storage/video-storage.ts
407
808
  var DB_NAME = "vidtreo-recorder";
408
809
  var DB_VERSION = 1;
@@ -2611,55 +3012,6 @@ class VideoUploadService {
2611
3012
  }
2612
3013
  }
2613
3014
 
2614
- // src/core/utils/video-utils.ts
2615
- import { BlobSource, Input, MP4 } from "mediabunny";
2616
- async function extractVideoDuration(blob) {
2617
- try {
2618
- const source = new BlobSource(blob);
2619
- const input = new Input({
2620
- formats: [MP4],
2621
- source
2622
- });
2623
- if (typeof input.computeDuration !== "function") {
2624
- throw new Error("computeDuration method is not available");
2625
- }
2626
- const duration = await input.computeDuration();
2627
- if (!duration) {
2628
- throw new Error("Duration is missing from computeDuration");
2629
- }
2630
- if (duration <= 0) {
2631
- throw new Error("Invalid duration: must be greater than 0");
2632
- }
2633
- return duration;
2634
- } catch {
2635
- return extractDurationWithVideoElement(blob);
2636
- }
2637
- }
2638
- function extractDurationWithVideoElement(blob) {
2639
- return new Promise((resolve, reject) => {
2640
- const video = document.createElement("video");
2641
- const url = URL.createObjectURL(blob);
2642
- const cleanup = () => {
2643
- URL.revokeObjectURL(url);
2644
- };
2645
- video.addEventListener("loadedmetadata", () => {
2646
- cleanup();
2647
- const duration = video.duration;
2648
- if (!Number.isFinite(duration) || duration <= 0) {
2649
- reject(new Error("Invalid video duration"));
2650
- return;
2651
- }
2652
- resolve(duration);
2653
- });
2654
- video.addEventListener("error", () => {
2655
- cleanup();
2656
- reject(new Error("Failed to load video metadata"));
2657
- });
2658
- video.src = url;
2659
- video.load();
2660
- });
2661
- }
2662
-
2663
3015
  // src/core/utils/stream-utils.ts
2664
3016
  function isScreenCaptureStream(stream) {
2665
3017
  const videoTracks = stream.getVideoTracks();
@@ -11850,124 +12202,6 @@ class QuotaManager {
11850
12202
  return quota.percentage >= threshold;
11851
12203
  }
11852
12204
  }
11853
- // src/core/transcode/video-transcoder.ts
11854
- import {
11855
- BlobSource as BlobSource2,
11856
- BufferTarget,
11857
- Conversion,
11858
- FilePathSource,
11859
- Input as Input2,
11860
- MP4 as MP42,
11861
- Mp4OutputFormat,
11862
- Output
11863
- } from "mediabunny";
11864
- function createSource(input) {
11865
- if (typeof input === "string") {
11866
- return new FilePathSource(input);
11867
- }
11868
- if (input instanceof Blob) {
11869
- return new BlobSource2(input);
11870
- }
11871
- throw new Error("Invalid input type. Expected Blob, File, or file path string.");
11872
- }
11873
- function createOutputFormat(format) {
11874
- switch (format) {
11875
- case "mp4":
11876
- return new Mp4OutputFormat;
11877
- case "webm":
11878
- case "mkv":
11879
- case "mov":
11880
- throw new Error(`Format ${format} is not yet supported. Only MP4 is currently supported.`);
11881
- default:
11882
- throw new Error(`Unsupported output format: ${format}`);
11883
- }
11884
- }
11885
- function getMimeTypeForFormat(format) {
11886
- switch (format) {
11887
- case "mp4":
11888
- return "video/mp4";
11889
- case "webm":
11890
- return "video/webm";
11891
- case "mkv":
11892
- return "video/x-matroska";
11893
- case "mov":
11894
- return "video/quicktime";
11895
- default:
11896
- throw new Error(`Unsupported output format: ${format}`);
11897
- }
11898
- }
11899
- function createConversionOptions(config) {
11900
- const audioCodec = getAudioCodecForFormat(config.format, config.audioCodec);
11901
- const video = {
11902
- fit: "contain",
11903
- forceTranscode: true
11904
- };
11905
- if (config.width !== undefined) {
11906
- video.width = config.width;
11907
- }
11908
- if (config.height !== undefined) {
11909
- video.height = config.height;
11910
- }
11911
- if (config.fps !== undefined) {
11912
- video.frameRate = config.fps;
11913
- }
11914
- if (config.bitrate !== undefined) {
11915
- video.bitrate = config.bitrate;
11916
- }
11917
- if (config.codec !== undefined) {
11918
- video.codec = config.codec;
11919
- }
11920
- const audio = {
11921
- codec: audioCodec,
11922
- forceTranscode: true
11923
- };
11924
- return { video, audio };
11925
- }
11926
- function validateConversion(conversion) {
11927
- if (!conversion.isValid) {
11928
- const reasons = conversion.discardedTracks.map((track) => track.reason).join(", ");
11929
- throw new Error(`Conversion is invalid. Discarded tracks: ${reasons}`);
11930
- }
11931
- }
11932
- async function transcodeVideo(input, config = {}, onProgress) {
11933
- const finalConfig = {
11934
- ...DEFAULT_TRANSCODE_CONFIG,
11935
- ...config,
11936
- format: config.format || DEFAULT_TRANSCODE_CONFIG.format
11937
- };
11938
- if (!finalConfig.audioCodec) {
11939
- finalConfig.audioCodec = getDefaultAudioCodecForFormat(finalConfig.format);
11940
- }
11941
- const source = createSource(input);
11942
- const mediabunnyInput = new Input2({
11943
- formats: [MP42],
11944
- source
11945
- });
11946
- const outputFormat = createOutputFormat(finalConfig.format);
11947
- const output = new Output({
11948
- format: outputFormat,
11949
- target: new BufferTarget
11950
- });
11951
- const conversion = await Conversion.init({
11952
- input: mediabunnyInput,
11953
- output,
11954
- ...createConversionOptions(finalConfig)
11955
- });
11956
- validateConversion(conversion);
11957
- if (onProgress) {
11958
- conversion.onProgress = onProgress;
11959
- }
11960
- await conversion.execute();
11961
- const buffer = output.target.buffer;
11962
- if (!buffer) {
11963
- throw new Error("Transcoding completed but no output buffer was generated");
11964
- }
11965
- const mimeType = getMimeTypeForFormat(finalConfig.format);
11966
- return {
11967
- buffer,
11968
- blob: new Blob([buffer], { type: mimeType })
11969
- };
11970
- }
11971
12205
  // src/core/utils/audio-utils.ts
11972
12206
  function calculateBarColor(position) {
11973
12207
  if (position < 0.25) {
@@ -11985,6 +12219,23 @@ function calculateBarColor(position) {
11985
12219
  const t = (position - 0.75) / 0.25;
11986
12220
  return `rgb(0, ${Math.round(128 - (100 - 128) * t)}, ${Math.round(128 + (200 - 128) * t)})`;
11987
12221
  }
12222
+ // src/core/utils/device-detection.ts
12223
+ import { UAParser } from "ua-parser-js";
12224
+ function isMobileDevice() {
12225
+ const parser = new UAParser;
12226
+ const result = parser.getResult();
12227
+ const deviceType = result.device.type;
12228
+ const isMobile = deviceType === "mobile" || deviceType === "tablet";
12229
+ logger.debug("Mobile detection result", {
12230
+ userAgent: navigator.userAgent,
12231
+ deviceType,
12232
+ isMobile,
12233
+ device: result.device,
12234
+ os: result.os,
12235
+ browser: result.browser
12236
+ });
12237
+ return isMobile;
12238
+ }
11988
12239
  // src/vidtreo-recorder.ts
11989
12240
  class VidtreoRecorder {
11990
12241
  controller;
@@ -12188,6 +12439,8 @@ class VidtreoRecorder {
12188
12439
  }
12189
12440
  }
12190
12441
  export {
12442
+ validateFile,
12443
+ transcodeVideoForNativeCamera,
12191
12444
  transcodeVideo,
12192
12445
  requireStream,
12193
12446
  requireProcessor,
@@ -12197,12 +12450,14 @@ export {
12197
12450
  requireActive,
12198
12451
  mapPresetToConfig,
12199
12452
  logger,
12453
+ isMobileDevice,
12200
12454
  getDefaultConfigForFormat,
12201
12455
  getDefaultAudioCodecForFormat,
12202
12456
  getAudioCodecForFormat,
12203
12457
  formatTime,
12204
12458
  formatFileSize,
12205
12459
  extractVideoDuration,
12460
+ extractLastFrame,
12206
12461
  extractErrorMessage,
12207
12462
  calculateBarColor,
12208
12463
  VidtreoRecorder,
@@ -12214,6 +12469,7 @@ export {
12214
12469
  RecordingManager,
12215
12470
  RecorderController,
12216
12471
  QuotaManager,
12472
+ NativeCameraHandler,
12217
12473
  FORMAT_DEFAULT_CODECS,
12218
12474
  DeviceManager,
12219
12475
  DEFAULT_TRANSCODE_CONFIG,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vidtreo/recorder",
3
- "version": "0.9.2",
3
+ "version": "0.9.3",
4
4
  "type": "module",
5
5
  "description": "Vidtreo SDK for browser-based video recording and transcoding. Features include camera/screen recording, real-time MP4 transcoding, audio level analysis, mute/pause controls, source switching, device selection, and automatic backend uploads. Similar to Ziggeo and Addpipe, Vidtreo provides enterprise-grade video processing capabilities for web applications.",
6
6
  "main": "./dist/index.js",
@@ -44,7 +44,8 @@
44
44
  "author": "cfonseca@vidtreo.com",
45
45
  "license": "MIT",
46
46
  "dependencies": {
47
- "mediabunny": "^1.25.8"
47
+ "mediabunny": "^1.25.8",
48
+ "ua-parser-js": "^2.0.7"
48
49
  },
49
50
  "devDependencies": {
50
51
  "@happy-dom/global-registrator": "^20.0.11",