@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.d.ts +62 -5
- package/dist/index.js +481 -171
- package/package.json +3 -2
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,
|
|
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
|
|
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,
|