@vidtreo/recorder 0.9.2 → 0.9.4

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.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;
@@ -1560,6 +1961,10 @@ class StreamManager {
1560
1961
  this.setState("starting");
1561
1962
  logger.debug("[StreamManager] State set to 'starting'");
1562
1963
  try {
1964
+ logger.debug("[StreamManager] Building constraints", {
1965
+ selectedVideoDeviceId: this.selectedVideoDeviceId,
1966
+ selectedAudioDeviceId: this.selectedAudioDeviceId
1967
+ });
1563
1968
  const constraints = {
1564
1969
  video: this.buildVideoConstraints(this.selectedVideoDeviceId),
1565
1970
  audio: this.buildAudioConstraints(this.selectedAudioDeviceId)
@@ -2611,55 +3016,6 @@ class VideoUploadService {
2611
3016
  }
2612
3017
  }
2613
3018
 
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
3019
  // src/core/utils/stream-utils.ts
2664
3020
  function isScreenCaptureStream(stream) {
2665
3021
  const videoTracks = stream.getVideoTracks();
@@ -2671,6 +3027,35 @@ function isScreenCaptureStream(stream) {
2671
3027
  return "displaySurface" in settings || videoTrack.label.toLowerCase().includes("screen") || videoTrack.label.toLowerCase().includes("display");
2672
3028
  }
2673
3029
 
3030
+ // src/core/processor/bitrate-utils.ts
3031
+ import {
3032
+ QUALITY_HIGH as QUALITY_HIGH3,
3033
+ QUALITY_LOW as QUALITY_LOW2,
3034
+ QUALITY_MEDIUM as QUALITY_MEDIUM2,
3035
+ QUALITY_VERY_HIGH as QUALITY_VERY_HIGH2
3036
+ } from "mediabunny";
3037
+ function serializeBitrate(bitrate) {
3038
+ if (bitrate === undefined) {
3039
+ return;
3040
+ }
3041
+ if (typeof bitrate === "number") {
3042
+ return bitrate;
3043
+ }
3044
+ if (bitrate === QUALITY_LOW2) {
3045
+ return "low";
3046
+ }
3047
+ if (bitrate === QUALITY_MEDIUM2) {
3048
+ return "medium";
3049
+ }
3050
+ if (bitrate === QUALITY_HIGH3) {
3051
+ return "high";
3052
+ }
3053
+ if (bitrate === QUALITY_VERY_HIGH2) {
3054
+ return "very-high";
3055
+ }
3056
+ return "high";
3057
+ }
3058
+
2674
3059
  // src/core/processor/codec-detector.ts
2675
3060
  async function detectBestCodec(width, height, bitrate) {
2676
3061
  try {
@@ -8414,7 +8799,10 @@ class Quality {
8414
8799
  return Math.round(finalBitrate / 1000) * 1000;
8415
8800
  }
8416
8801
  }
8802
+ var QUALITY_LOW = /* @__PURE__ */ new Quality(0.6);
8803
+ var QUALITY_MEDIUM = /* @__PURE__ */ new Quality(1);
8417
8804
  var QUALITY_HIGH = /* @__PURE__ */ new Quality(2);
8805
+ var QUALITY_VERY_HIGH = /* @__PURE__ */ new Quality(4);
8418
8806
 
8419
8807
  // ../../node_modules/mediabunny/dist/modules/src/media-source.js
8420
8808
  /*!
@@ -9949,7 +10337,7 @@ class RecorderWorker {
9949
10337
  contentHint: "detail",
9950
10338
  hardwareAcceleration: "prefer-hardware",
9951
10339
  keyFrameInterval: keyFrameIntervalSeconds,
9952
- bitrate: config.bitrate ?? QUALITY_HIGH
10340
+ bitrate: this.deserializeBitrate(config.bitrate)
9953
10341
  };
9954
10342
  this.videoSource = new VideoSampleSource(videoSourceOptions);
9955
10343
  const output = requireNonNull(this.output, "Output must be initialized before adding video track");
@@ -10662,6 +11050,24 @@ class RecorderWorker {
10662
11050
  };
10663
11051
  self.postMessage(response);
10664
11052
  }
11053
+ deserializeBitrate(bitrate) {
11054
+ if (typeof bitrate === "number") {
11055
+ return bitrate;
11056
+ }
11057
+ if (bitrate === "low") {
11058
+ return QUALITY_LOW;
11059
+ }
11060
+ if (bitrate === "medium") {
11061
+ return QUALITY_MEDIUM;
11062
+ }
11063
+ if (bitrate === "high") {
11064
+ return QUALITY_HIGH;
11065
+ }
11066
+ if (bitrate === "very-high") {
11067
+ return QUALITY_VERY_HIGH;
11068
+ }
11069
+ return QUALITY_HIGH;
11070
+ }
10665
11071
  }
10666
11072
  new RecorderWorker;
10667
11073
  `;
@@ -10799,7 +11205,7 @@ class WorkerProcessor {
10799
11205
  width: config.width,
10800
11206
  height: config.height,
10801
11207
  fps: config.fps,
10802
- bitrate: config.bitrate,
11208
+ bitrate: serializeBitrate(config.bitrate),
10803
11209
  audioCodec,
10804
11210
  audioBitrate: config.audioBitrate,
10805
11211
  codec,
@@ -11850,124 +12256,6 @@ class QuotaManager {
11850
12256
  return quota.percentage >= threshold;
11851
12257
  }
11852
12258
  }
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
12259
  // src/core/utils/audio-utils.ts
11972
12260
  function calculateBarColor(position) {
11973
12261
  if (position < 0.25) {
@@ -11985,6 +12273,23 @@ function calculateBarColor(position) {
11985
12273
  const t = (position - 0.75) / 0.25;
11986
12274
  return `rgb(0, ${Math.round(128 - (100 - 128) * t)}, ${Math.round(128 + (200 - 128) * t)})`;
11987
12275
  }
12276
+ // src/core/utils/device-detection.ts
12277
+ import { UAParser } from "ua-parser-js";
12278
+ function isMobileDevice() {
12279
+ const parser = new UAParser;
12280
+ const result = parser.getResult();
12281
+ const deviceType = result.device.type;
12282
+ const isMobile = deviceType === "mobile" || deviceType === "tablet";
12283
+ logger.debug("Mobile detection result", {
12284
+ userAgent: navigator.userAgent,
12285
+ deviceType,
12286
+ isMobile,
12287
+ device: result.device,
12288
+ os: result.os,
12289
+ browser: result.browser
12290
+ });
12291
+ return isMobile;
12292
+ }
11988
12293
  // src/vidtreo-recorder.ts
11989
12294
  class VidtreoRecorder {
11990
12295
  controller;
@@ -12188,6 +12493,8 @@ class VidtreoRecorder {
12188
12493
  }
12189
12494
  }
12190
12495
  export {
12496
+ validateFile,
12497
+ transcodeVideoForNativeCamera,
12191
12498
  transcodeVideo,
12192
12499
  requireStream,
12193
12500
  requireProcessor,
@@ -12197,12 +12504,14 @@ export {
12197
12504
  requireActive,
12198
12505
  mapPresetToConfig,
12199
12506
  logger,
12507
+ isMobileDevice,
12200
12508
  getDefaultConfigForFormat,
12201
12509
  getDefaultAudioCodecForFormat,
12202
12510
  getAudioCodecForFormat,
12203
12511
  formatTime,
12204
12512
  formatFileSize,
12205
12513
  extractVideoDuration,
12514
+ extractLastFrame,
12206
12515
  extractErrorMessage,
12207
12516
  calculateBarColor,
12208
12517
  VidtreoRecorder,
@@ -12214,6 +12523,7 @@ export {
12214
12523
  RecordingManager,
12215
12524
  RecorderController,
12216
12525
  QuotaManager,
12526
+ NativeCameraHandler,
12217
12527
  FORMAT_DEFAULT_CODECS,
12218
12528
  DeviceManager,
12219
12529
  DEFAULT_TRANSCODE_CONFIG,