@vidtreo/recorder 0.8.2 → 0.8.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 +34 -0
- package/dist/index.js +381 -55
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -152,6 +152,30 @@ export type UploadResult = {
|
|
|
152
152
|
|
|
153
153
|
export declare function calculateBarColor(position: number): string;
|
|
154
154
|
|
|
155
|
+
declare const ANSI_COLORS: {
|
|
156
|
+
readonly reset: "\u001B[0m";
|
|
157
|
+
readonly bright: "\u001B[1m";
|
|
158
|
+
readonly dim: "\u001B[2m";
|
|
159
|
+
readonly red: "\u001B[31m";
|
|
160
|
+
readonly green: "\u001B[32m";
|
|
161
|
+
readonly yellow: "\u001B[33m";
|
|
162
|
+
readonly blue: "\u001B[34m";
|
|
163
|
+
readonly magenta: "\u001B[35m";
|
|
164
|
+
readonly cyan: "\u001B[36m";
|
|
165
|
+
readonly white: "\u001B[37m";
|
|
166
|
+
readonly gray: "\u001B[90m";
|
|
167
|
+
};
|
|
168
|
+
export declare const logger: {
|
|
169
|
+
readonly log: (message: string, ...args: unknown[]) => void;
|
|
170
|
+
readonly info: (message: string, ...args: unknown[]) => void;
|
|
171
|
+
readonly warn: (message: string, ...args: unknown[]) => void;
|
|
172
|
+
readonly error: (message: string, ...args: unknown[]) => void;
|
|
173
|
+
readonly debug: (message: string, ...args: unknown[]) => void;
|
|
174
|
+
readonly group: (label: string, color?: keyof typeof ANSI_COLORS) => void;
|
|
175
|
+
readonly groupEnd: () => void;
|
|
176
|
+
};
|
|
177
|
+
export {};
|
|
178
|
+
|
|
155
179
|
export declare function extractErrorMessage(error: unknown): string;
|
|
156
180
|
|
|
157
181
|
export declare const FILE_SIZE_UNITS: readonly ["Bytes", "KB", "MB", "GB"];
|
|
@@ -456,9 +480,19 @@ export declare class AudioTrackManager {
|
|
|
456
480
|
private gainNode;
|
|
457
481
|
private audioSource;
|
|
458
482
|
private lastAudioTimestamp;
|
|
483
|
+
private pendingAddOperation;
|
|
459
484
|
private isMuted;
|
|
460
485
|
private isPaused;
|
|
461
486
|
private onMuteStateChange?;
|
|
487
|
+
private validateStreamAndConfig;
|
|
488
|
+
private prepareAudioTrack;
|
|
489
|
+
private getAudioContextClass;
|
|
490
|
+
private setupAudioContext;
|
|
491
|
+
private createAudioSource;
|
|
492
|
+
private addWorkletModule;
|
|
493
|
+
private createWorkletNode;
|
|
494
|
+
private setupWorkletMessageHandler;
|
|
495
|
+
private setupAudioNodes;
|
|
462
496
|
setupAudioTrack(stream: MediaStream, config: TranscodeConfig): Promise<AudioSampleSource | null>;
|
|
463
497
|
toggleMute(): void;
|
|
464
498
|
isMutedState(): boolean;
|
package/dist/index.js
CHANGED
|
@@ -44,17 +44,22 @@ class AudioLevelAnalyzer {
|
|
|
44
44
|
if (!AudioContextClass) {
|
|
45
45
|
throw new Error("AudioContext is not supported in this browser");
|
|
46
46
|
}
|
|
47
|
+
let audioContext;
|
|
47
48
|
try {
|
|
48
|
-
|
|
49
|
+
audioContext = new AudioContextClass;
|
|
49
50
|
} catch (error) {
|
|
50
51
|
throw new Error(`Failed to create AudioContext: ${extractErrorMessage(error)}`);
|
|
51
52
|
}
|
|
53
|
+
this.audioContext = audioContext;
|
|
52
54
|
const source = this.audioContext.createMediaStreamSource(stream);
|
|
53
55
|
this.analyser = this.audioContext.createAnalyser();
|
|
54
56
|
this.analyser.fftSize = FFT_SIZE;
|
|
55
57
|
this.analyser.smoothingTimeConstant = SMOOTHING_TIME_CONSTANT;
|
|
56
58
|
source.connect(this.analyser);
|
|
57
59
|
const dataArray = new Uint8Array(this.analyser.fftSize);
|
|
60
|
+
this.audioLevel = 0;
|
|
61
|
+
const initialMutedState = this.checkMutedState();
|
|
62
|
+
callbacks.onLevelUpdate(0, initialMutedState);
|
|
58
63
|
this.audioLevelIntervalId = window.setInterval(() => {
|
|
59
64
|
if (!this.analyser) {
|
|
60
65
|
return;
|
|
@@ -426,6 +431,84 @@ async function transcodeVideo(input, config = {}, onProgress) {
|
|
|
426
431
|
// src/core/processor/stream-processor.ts
|
|
427
432
|
import { CanvasSource } from "mediabunny";
|
|
428
433
|
|
|
434
|
+
// src/core/utils/logger.ts
|
|
435
|
+
var isDevelopment = typeof process !== "undefined" && process.env && true || typeof window !== "undefined" && window.__VIDTREO_DEV__ === true;
|
|
436
|
+
var ANSI_COLORS = {
|
|
437
|
+
reset: "\x1B[0m",
|
|
438
|
+
bright: "\x1B[1m",
|
|
439
|
+
dim: "\x1B[2m",
|
|
440
|
+
red: "\x1B[31m",
|
|
441
|
+
green: "\x1B[32m",
|
|
442
|
+
yellow: "\x1B[33m",
|
|
443
|
+
blue: "\x1B[34m",
|
|
444
|
+
magenta: "\x1B[35m",
|
|
445
|
+
cyan: "\x1B[36m",
|
|
446
|
+
white: "\x1B[37m",
|
|
447
|
+
gray: "\x1B[90m"
|
|
448
|
+
};
|
|
449
|
+
function formatMessage(level, message, options) {
|
|
450
|
+
if (!isDevelopment) {
|
|
451
|
+
return "";
|
|
452
|
+
}
|
|
453
|
+
const prefix = options?.prefix || `[${level.toUpperCase()}]`;
|
|
454
|
+
const color = options?.color || getDefaultColor(level);
|
|
455
|
+
const colorCode = ANSI_COLORS[color];
|
|
456
|
+
const resetCode = ANSI_COLORS.reset;
|
|
457
|
+
return `${colorCode}${prefix}${resetCode} ${message}`;
|
|
458
|
+
}
|
|
459
|
+
function getDefaultColor(level) {
|
|
460
|
+
switch (level) {
|
|
461
|
+
case "error":
|
|
462
|
+
return "red";
|
|
463
|
+
case "warn":
|
|
464
|
+
return "yellow";
|
|
465
|
+
case "info":
|
|
466
|
+
return "cyan";
|
|
467
|
+
case "debug":
|
|
468
|
+
return "gray";
|
|
469
|
+
default:
|
|
470
|
+
return "white";
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
function log(level, message, ...args) {
|
|
474
|
+
if (!isDevelopment) {
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
const formatted = formatMessage(level, message);
|
|
478
|
+
console[level](formatted, ...args);
|
|
479
|
+
}
|
|
480
|
+
var logger = {
|
|
481
|
+
log: (message, ...args) => {
|
|
482
|
+
log("log", message, ...args);
|
|
483
|
+
},
|
|
484
|
+
info: (message, ...args) => {
|
|
485
|
+
log("info", message, ...args);
|
|
486
|
+
},
|
|
487
|
+
warn: (message, ...args) => {
|
|
488
|
+
log("warn", message, ...args);
|
|
489
|
+
},
|
|
490
|
+
error: (message, ...args) => {
|
|
491
|
+
log("error", message, ...args);
|
|
492
|
+
},
|
|
493
|
+
debug: (message, ...args) => {
|
|
494
|
+
log("debug", message, ...args);
|
|
495
|
+
},
|
|
496
|
+
group: (label, color = "cyan") => {
|
|
497
|
+
if (!isDevelopment) {
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
const colorCode = ANSI_COLORS[color];
|
|
501
|
+
const resetCode = ANSI_COLORS.reset;
|
|
502
|
+
console.group(`${colorCode}${label}${resetCode}`);
|
|
503
|
+
},
|
|
504
|
+
groupEnd: () => {
|
|
505
|
+
if (!isDevelopment) {
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
console.groupEnd();
|
|
509
|
+
}
|
|
510
|
+
};
|
|
511
|
+
|
|
429
512
|
// src/core/processor/audio-track-manager.ts
|
|
430
513
|
import { AudioSample, AudioSampleSource } from "mediabunny";
|
|
431
514
|
|
|
@@ -500,25 +583,29 @@ class AudioTrackManager {
|
|
|
500
583
|
gainNode = null;
|
|
501
584
|
audioSource = null;
|
|
502
585
|
lastAudioTimestamp = 0;
|
|
586
|
+
pendingAddOperation = null;
|
|
503
587
|
isMuted = false;
|
|
504
588
|
isPaused = false;
|
|
505
589
|
onMuteStateChange;
|
|
506
|
-
|
|
590
|
+
validateStreamAndConfig(stream, config) {
|
|
507
591
|
if (!stream) {
|
|
592
|
+
logger.debug("[AudioTrackManager] No stream provided");
|
|
508
593
|
return null;
|
|
509
594
|
}
|
|
510
595
|
const audioTracks = stream.getAudioTracks();
|
|
596
|
+
logger.debug("[AudioTrackManager] Audio tracks found:", audioTracks.length);
|
|
511
597
|
if (audioTracks.length === 0) {
|
|
598
|
+
logger.debug("[AudioTrackManager] No audio tracks in stream");
|
|
512
599
|
return null;
|
|
513
600
|
}
|
|
514
601
|
const audioTrack = audioTracks[0];
|
|
515
602
|
if (!audioTrack) {
|
|
603
|
+
logger.debug("[AudioTrackManager] First audio track is null");
|
|
516
604
|
return null;
|
|
517
605
|
}
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
if (!AudioContextClass) {
|
|
606
|
+
logger.debug("[AudioTrackManager] Audio track state:", audioTrack.readyState);
|
|
607
|
+
if (audioTrack.readyState !== "live") {
|
|
608
|
+
logger.debug("[AudioTrackManager] Audio track is not live");
|
|
522
609
|
return null;
|
|
523
610
|
}
|
|
524
611
|
if (config.audioBitrate === undefined || config.audioBitrate === null) {
|
|
@@ -527,32 +614,113 @@ class AudioTrackManager {
|
|
|
527
614
|
if (!config.audioCodec) {
|
|
528
615
|
return null;
|
|
529
616
|
}
|
|
530
|
-
|
|
617
|
+
return audioTrack;
|
|
618
|
+
}
|
|
619
|
+
prepareAudioTrack(audioTrack) {
|
|
620
|
+
logger.debug("[AudioTrackManager] Cloning audio track to avoid stopping original");
|
|
621
|
+
let clonedTrack;
|
|
622
|
+
if (typeof audioTrack.clone === "function") {
|
|
623
|
+
clonedTrack = audioTrack.clone();
|
|
624
|
+
logger.debug("[AudioTrackManager] Audio track cloned successfully", {
|
|
625
|
+
clonedState: clonedTrack.readyState,
|
|
626
|
+
originalState: audioTrack.readyState
|
|
627
|
+
});
|
|
628
|
+
} else {
|
|
629
|
+
logger.debug("[AudioTrackManager] clone() not available, using original track");
|
|
630
|
+
clonedTrack = audioTrack;
|
|
631
|
+
}
|
|
632
|
+
this.originalAudioTrack = clonedTrack;
|
|
633
|
+
this.lastAudioTimestamp = 0;
|
|
634
|
+
this.pendingAddOperation = null;
|
|
635
|
+
logger.debug("[AudioTrackManager] Reset state, originalAudioTrack set (cloned)");
|
|
636
|
+
return clonedTrack;
|
|
637
|
+
}
|
|
638
|
+
getAudioContextClass() {
|
|
639
|
+
const AudioContextClass = window.AudioContext || window.webkitAudioContext;
|
|
640
|
+
return AudioContextClass || null;
|
|
641
|
+
}
|
|
642
|
+
async setupAudioContext() {
|
|
643
|
+
const AudioContextClass = this.getAudioContextClass();
|
|
644
|
+
if (!AudioContextClass) {
|
|
645
|
+
return null;
|
|
646
|
+
}
|
|
647
|
+
if (!this.audioContext || this.audioContext.state === "closed") {
|
|
648
|
+
logger.debug("[AudioTrackManager] Creating new AudioContext");
|
|
649
|
+
this.audioContext = new AudioContextClass;
|
|
650
|
+
} else {
|
|
651
|
+
logger.debug("[AudioTrackManager] Reusing existing AudioContext", {
|
|
652
|
+
state: this.audioContext.state
|
|
653
|
+
});
|
|
654
|
+
}
|
|
531
655
|
if (this.audioContext.state === "suspended") {
|
|
656
|
+
logger.debug("[AudioTrackManager] Resuming suspended AudioContext");
|
|
532
657
|
await this.audioContext.resume();
|
|
533
658
|
}
|
|
534
659
|
if (!this.audioContext.audioWorklet) {
|
|
660
|
+
logger.debug("[AudioTrackManager] AudioContext has no audioWorklet support");
|
|
535
661
|
await this.audioContext.close();
|
|
536
662
|
this.audioContext = null;
|
|
537
663
|
return null;
|
|
538
664
|
}
|
|
665
|
+
return this.audioContext;
|
|
666
|
+
}
|
|
667
|
+
createAudioSource(config) {
|
|
539
668
|
const audioBitrate = config.audioBitrate;
|
|
540
669
|
const audioCodec = config.audioCodec;
|
|
541
|
-
|
|
670
|
+
logger.debug("[AudioTrackManager] Creating AudioSampleSource", {
|
|
542
671
|
codec: audioCodec,
|
|
543
672
|
bitrate: audioBitrate
|
|
544
673
|
});
|
|
545
|
-
const
|
|
546
|
-
|
|
674
|
+
const audioSource = new AudioSampleSource({
|
|
675
|
+
codec: audioCodec,
|
|
676
|
+
bitrate: audioBitrate
|
|
677
|
+
});
|
|
678
|
+
logger.debug("[AudioTrackManager] AudioSampleSource created:", !!audioSource);
|
|
679
|
+
return audioSource;
|
|
680
|
+
}
|
|
681
|
+
async addWorkletModule() {
|
|
682
|
+
if (!this.audioContext) {
|
|
683
|
+
throw new Error("AudioContext not initialized");
|
|
684
|
+
}
|
|
547
685
|
const processorUrl = getWorkletUrl();
|
|
548
|
-
|
|
686
|
+
logger.debug("[AudioTrackManager] Adding worklet module");
|
|
687
|
+
try {
|
|
688
|
+
await this.audioContext.audioWorklet.addModule(processorUrl);
|
|
689
|
+
logger.debug("[AudioTrackManager] Worklet module added successfully");
|
|
690
|
+
} catch (error) {
|
|
691
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
692
|
+
logger.warn("[AudioTrackManager] Error adding worklet module:", errorMessage);
|
|
693
|
+
if (!(errorMessage.includes("already") || errorMessage.includes("duplicate") || errorMessage.includes("InvalidStateError"))) {
|
|
694
|
+
logger.error("[AudioTrackManager] Fatal error adding module, cleaning up");
|
|
695
|
+
this.audioSource = null;
|
|
696
|
+
this.mediaStreamSource = null;
|
|
697
|
+
throw error;
|
|
698
|
+
}
|
|
699
|
+
logger.debug("[AudioTrackManager] Module already loaded, continuing");
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
createWorkletNode() {
|
|
703
|
+
if (!this.audioContext) {
|
|
704
|
+
throw new Error("AudioContext not initialized");
|
|
705
|
+
}
|
|
706
|
+
logger.debug("[AudioTrackManager] Creating AudioWorkletNode");
|
|
549
707
|
this.audioWorkletNode = new AudioWorkletNode(this.audioContext, "audio-capture-processor");
|
|
708
|
+
logger.debug("[AudioTrackManager] AudioWorkletNode created:", !!this.audioWorkletNode);
|
|
709
|
+
}
|
|
710
|
+
setupWorkletMessageHandler() {
|
|
711
|
+
if (!this.audioWorkletNode) {
|
|
712
|
+
throw new Error("AudioWorkletNode not initialized");
|
|
713
|
+
}
|
|
550
714
|
this.audioWorkletNode.port.onmessage = (event) => {
|
|
551
715
|
if (this.isPaused || this.isMuted || !this.audioSource) {
|
|
552
716
|
return;
|
|
553
717
|
}
|
|
554
718
|
const { data, sampleRate, numberOfChannels, duration, bufferLength } = event.data;
|
|
555
719
|
const float32Data = new Float32Array(data, 0, bufferLength);
|
|
720
|
+
if (!this.audioSource) {
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
const audioSource = this.audioSource;
|
|
556
724
|
const audioSample = new AudioSample({
|
|
557
725
|
data: float32Data,
|
|
558
726
|
format: "f32-planar",
|
|
@@ -560,18 +728,69 @@ class AudioTrackManager {
|
|
|
560
728
|
sampleRate,
|
|
561
729
|
timestamp: this.lastAudioTimestamp
|
|
562
730
|
});
|
|
563
|
-
this.
|
|
564
|
-
audioSample.
|
|
565
|
-
this.
|
|
731
|
+
const currentTimestamp = this.lastAudioTimestamp;
|
|
732
|
+
const addOperation = this.pendingAddOperation ? this.pendingAddOperation.then(() => audioSource.add(audioSample)).catch(() => audioSource.add(audioSample)) : audioSource.add(audioSample);
|
|
733
|
+
this.pendingAddOperation = addOperation.then(() => {
|
|
734
|
+
audioSample.close();
|
|
735
|
+
this.lastAudioTimestamp = currentTimestamp + duration;
|
|
736
|
+
}).catch(() => {
|
|
737
|
+
audioSample.close();
|
|
738
|
+
this.lastAudioTimestamp = currentTimestamp + duration;
|
|
739
|
+
});
|
|
566
740
|
};
|
|
741
|
+
logger.debug("[AudioTrackManager] onmessage handler set");
|
|
742
|
+
}
|
|
743
|
+
setupAudioNodes() {
|
|
744
|
+
if (!(this.audioContext && this.audioWorkletNode && this.mediaStreamSource)) {
|
|
745
|
+
throw new Error("Audio nodes not initialized");
|
|
746
|
+
}
|
|
567
747
|
this.gainNode = this.audioContext.createGain();
|
|
568
748
|
this.gainNode.gain.value = 0;
|
|
749
|
+
logger.debug("[AudioTrackManager] GainNode created");
|
|
569
750
|
this.mediaStreamSource.connect(this.audioWorkletNode);
|
|
570
751
|
this.audioWorkletNode.connect(this.gainNode);
|
|
571
752
|
this.gainNode.connect(this.audioContext.destination);
|
|
572
|
-
|
|
573
|
-
|
|
753
|
+
logger.debug("[AudioTrackManager] Audio nodes connected");
|
|
754
|
+
}
|
|
755
|
+
async setupAudioTrack(stream, config) {
|
|
756
|
+
logger.debug("[AudioTrackManager] setupAudioTrack called", {
|
|
757
|
+
hasStream: !!stream,
|
|
758
|
+
audioContextState: this.audioContext?.state,
|
|
759
|
+
hasAudioContext: !!this.audioContext,
|
|
760
|
+
hasAudioSource: !!this.audioSource
|
|
761
|
+
});
|
|
762
|
+
const audioTrack = this.validateStreamAndConfig(stream, config);
|
|
763
|
+
if (!audioTrack) {
|
|
764
|
+
return null;
|
|
765
|
+
}
|
|
766
|
+
this.prepareAudioTrack(audioTrack);
|
|
767
|
+
const audioContext = await this.setupAudioContext();
|
|
768
|
+
if (!audioContext) {
|
|
769
|
+
return null;
|
|
770
|
+
}
|
|
771
|
+
this.audioSource = this.createAudioSource(config);
|
|
772
|
+
if (!this.originalAudioTrack) {
|
|
773
|
+
throw new Error("Original audio track not set");
|
|
574
774
|
}
|
|
775
|
+
const audioOnlyStream = new MediaStream([this.originalAudioTrack]);
|
|
776
|
+
this.mediaStreamSource = audioContext.createMediaStreamSource(audioOnlyStream);
|
|
777
|
+
logger.debug("[AudioTrackManager] MediaStreamSource created:", !!this.mediaStreamSource);
|
|
778
|
+
try {
|
|
779
|
+
await this.addWorkletModule();
|
|
780
|
+
this.createWorkletNode();
|
|
781
|
+
} catch (error) {
|
|
782
|
+
logger.error("[AudioTrackManager] Error creating AudioWorkletNode:", error);
|
|
783
|
+
this.audioSource = null;
|
|
784
|
+
this.mediaStreamSource = null;
|
|
785
|
+
throw error;
|
|
786
|
+
}
|
|
787
|
+
this.setupWorkletMessageHandler();
|
|
788
|
+
this.setupAudioNodes();
|
|
789
|
+
if (audioContext.state === "suspended") {
|
|
790
|
+
logger.debug("[AudioTrackManager] Resuming AudioContext after setup");
|
|
791
|
+
await audioContext.resume();
|
|
792
|
+
}
|
|
793
|
+
logger.debug("[AudioTrackManager] setupAudioTrack completed, returning audioSource:", !!this.audioSource);
|
|
575
794
|
return this.audioSource;
|
|
576
795
|
}
|
|
577
796
|
toggleMute() {
|
|
@@ -624,32 +843,50 @@ class AudioTrackManager {
|
|
|
624
843
|
this.onMuteStateChange = callback;
|
|
625
844
|
}
|
|
626
845
|
cleanup() {
|
|
846
|
+
logger.debug("[AudioTrackManager] cleanup called", {
|
|
847
|
+
hasAudioSource: !!this.audioSource,
|
|
848
|
+
hasAudioContext: !!this.audioContext,
|
|
849
|
+
audioContextState: this.audioContext?.state,
|
|
850
|
+
hasWorkletNode: !!this.audioWorkletNode
|
|
851
|
+
});
|
|
627
852
|
this.audioSource = null;
|
|
853
|
+
this.pendingAddOperation = null;
|
|
854
|
+
this.isMuted = false;
|
|
855
|
+
this.isPaused = false;
|
|
628
856
|
if (this.audioWorkletNode) {
|
|
857
|
+
logger.debug("[AudioTrackManager] Disconnecting and closing worklet node");
|
|
629
858
|
this.audioWorkletNode.disconnect();
|
|
630
859
|
this.audioWorkletNode.port.close();
|
|
631
860
|
this.audioWorkletNode = null;
|
|
632
861
|
}
|
|
633
862
|
if (this.gainNode) {
|
|
863
|
+
logger.debug("[AudioTrackManager] Disconnecting gain node");
|
|
634
864
|
this.gainNode.disconnect();
|
|
635
865
|
this.gainNode = null;
|
|
636
866
|
}
|
|
637
867
|
if (this.mediaStreamSource) {
|
|
868
|
+
logger.debug("[AudioTrackManager] Disconnecting media stream source");
|
|
638
869
|
this.mediaStreamSource.disconnect();
|
|
639
870
|
this.mediaStreamSource = null;
|
|
640
871
|
}
|
|
641
|
-
if (this.audioContext) {
|
|
642
|
-
|
|
643
|
-
this.audioContext
|
|
872
|
+
if (this.audioContext && this.audioContext.state !== "closed") {
|
|
873
|
+
logger.debug("[AudioTrackManager] Suspending AudioContext (not closing)");
|
|
874
|
+
if (typeof this.audioContext.suspend === "function") {
|
|
875
|
+
this.audioContext.suspend().catch((err) => {
|
|
876
|
+
logger.warn("[AudioTrackManager] Error suspending AudioContext:", err);
|
|
877
|
+
});
|
|
878
|
+
}
|
|
644
879
|
}
|
|
645
880
|
if (this.originalAudioTrack) {
|
|
646
|
-
|
|
881
|
+
logger.debug("[AudioTrackManager] Stopping cloned audio track", {
|
|
882
|
+
trackState: this.originalAudioTrack.readyState
|
|
883
|
+
});
|
|
884
|
+
if (this.originalAudioTrack.readyState === "live" && typeof this.originalAudioTrack.stop === "function") {
|
|
885
|
+
this.originalAudioTrack.stop();
|
|
886
|
+
logger.debug("[AudioTrackManager] Cloned audio track stopped");
|
|
887
|
+
}
|
|
647
888
|
this.originalAudioTrack = null;
|
|
648
889
|
}
|
|
649
|
-
if (workletBlobUrl) {
|
|
650
|
-
URL.revokeObjectURL(workletBlobUrl);
|
|
651
|
-
workletBlobUrl = null;
|
|
652
|
-
}
|
|
653
890
|
this.lastAudioTimestamp = 0;
|
|
654
891
|
}
|
|
655
892
|
}
|
|
@@ -916,6 +1153,9 @@ class VideoElementManager {
|
|
|
916
1153
|
isActive = false;
|
|
917
1154
|
isIntentionallyPaused = false;
|
|
918
1155
|
create(stream) {
|
|
1156
|
+
if (this.videoElement) {
|
|
1157
|
+
this.cleanup();
|
|
1158
|
+
}
|
|
919
1159
|
const videoElement = document.createElement("video");
|
|
920
1160
|
videoElement.srcObject = stream;
|
|
921
1161
|
videoElement.autoplay = true;
|
|
@@ -965,9 +1205,17 @@ class VideoElementManager {
|
|
|
965
1205
|
}
|
|
966
1206
|
cleanup() {
|
|
967
1207
|
if (this.videoElement) {
|
|
1208
|
+
if (typeof this.videoElement.pause === "function") {
|
|
1209
|
+
this.videoElement.pause();
|
|
1210
|
+
}
|
|
968
1211
|
this.videoElement.srcObject = null;
|
|
1212
|
+
if (typeof this.videoElement.remove === "function") {
|
|
1213
|
+
this.videoElement.remove();
|
|
1214
|
+
}
|
|
969
1215
|
this.videoElement = null;
|
|
970
1216
|
}
|
|
1217
|
+
this.isActive = false;
|
|
1218
|
+
this.isIntentionallyPaused = false;
|
|
971
1219
|
}
|
|
972
1220
|
waitForVideoReady(shouldPlay) {
|
|
973
1221
|
if (!this.videoElement) {
|
|
@@ -975,21 +1223,30 @@ class VideoElementManager {
|
|
|
975
1223
|
}
|
|
976
1224
|
const videoElement = this.videoElement;
|
|
977
1225
|
return new Promise((resolve, reject) => {
|
|
1226
|
+
let timeoutId;
|
|
978
1227
|
const cleanup = () => {
|
|
979
1228
|
videoElement.removeEventListener("loadedmetadata", onLoadedMetadata);
|
|
980
1229
|
videoElement.removeEventListener("error", onError);
|
|
1230
|
+
if (timeoutId !== undefined) {
|
|
1231
|
+
clearTimeout(timeoutId);
|
|
1232
|
+
timeoutId = undefined;
|
|
1233
|
+
}
|
|
981
1234
|
};
|
|
982
1235
|
const onLoadedMetadata = () => {
|
|
983
1236
|
cleanup();
|
|
984
1237
|
if (shouldPlay) {
|
|
985
|
-
videoElement.play().then(
|
|
1238
|
+
videoElement.play().then(() => {
|
|
1239
|
+
resolve();
|
|
1240
|
+
}).catch((err) => {
|
|
1241
|
+
reject(err);
|
|
1242
|
+
});
|
|
986
1243
|
} else {
|
|
987
1244
|
resolve();
|
|
988
1245
|
}
|
|
989
1246
|
};
|
|
990
|
-
const onError = () => {
|
|
1247
|
+
const onError = (event) => {
|
|
991
1248
|
cleanup();
|
|
992
|
-
reject(new Error(
|
|
1249
|
+
reject(new Error(`Failed to load video metadata: ${event.type}`));
|
|
993
1250
|
};
|
|
994
1251
|
if (videoElement.readyState >= READY_STATE_HAVE_CURRENT_DATA2) {
|
|
995
1252
|
if (shouldPlay) {
|
|
@@ -999,14 +1256,16 @@ class VideoElementManager {
|
|
|
999
1256
|
}
|
|
1000
1257
|
return;
|
|
1001
1258
|
}
|
|
1002
|
-
videoElement.addEventListener("loadedmetadata", onLoadedMetadata
|
|
1003
|
-
|
|
1004
|
-
|
|
1259
|
+
videoElement.addEventListener("loadedmetadata", onLoadedMetadata, {
|
|
1260
|
+
once: true
|
|
1261
|
+
});
|
|
1262
|
+
videoElement.addEventListener("error", onError, { once: true });
|
|
1263
|
+
if (shouldPlay) {
|
|
1005
1264
|
videoElement.play().catch(reject);
|
|
1006
1265
|
}
|
|
1007
|
-
setTimeout(() => {
|
|
1266
|
+
timeoutId = window.setTimeout(() => {
|
|
1008
1267
|
cleanup();
|
|
1009
|
-
reject(new Error(
|
|
1268
|
+
reject(new Error(`Timeout waiting for video to load. ReadyState: ${videoElement.readyState}, SrcObject: ${!!videoElement.srcObject}`));
|
|
1010
1269
|
}, VIDEO_LOAD_TIMEOUT);
|
|
1011
1270
|
});
|
|
1012
1271
|
}
|
|
@@ -1066,9 +1325,20 @@ class StreamProcessor {
|
|
|
1066
1325
|
latencyMode: REALTIME_LATENCY
|
|
1067
1326
|
});
|
|
1068
1327
|
output.addVideoTrack(this.canvasSource);
|
|
1328
|
+
logger.debug("[StreamProcessor] Setting up audio track", {
|
|
1329
|
+
hasStream: !!stream,
|
|
1330
|
+
audioTracks: stream.getAudioTracks().length
|
|
1331
|
+
});
|
|
1069
1332
|
const audioSource = await this.audioTrackManager.setupAudioTrack(stream, config);
|
|
1333
|
+
logger.debug("[StreamProcessor] Audio track setup result:", {
|
|
1334
|
+
hasAudioSource: !!audioSource,
|
|
1335
|
+
audioSourceType: audioSource?.constructor?.name
|
|
1336
|
+
});
|
|
1070
1337
|
if (audioSource) {
|
|
1338
|
+
logger.debug("[StreamProcessor] Adding audio track to output");
|
|
1071
1339
|
output.addAudioTrack(audioSource);
|
|
1340
|
+
} else {
|
|
1341
|
+
logger.warn("[StreamProcessor] No audio source, skipping audio track");
|
|
1072
1342
|
}
|
|
1073
1343
|
await output.start();
|
|
1074
1344
|
if (!this.canvasRenderer) {
|
|
@@ -1100,9 +1370,12 @@ class StreamProcessor {
|
|
|
1100
1370
|
return this.frameCapturer.isPausedState();
|
|
1101
1371
|
}
|
|
1102
1372
|
async finalize() {
|
|
1373
|
+
logger.debug("[StreamProcessor] finalize called");
|
|
1103
1374
|
this.frameCapturer.stop();
|
|
1104
1375
|
await this.frameCapturer.waitForPendingFrames();
|
|
1376
|
+
logger.debug("[StreamProcessor] Cleaning up video element manager");
|
|
1105
1377
|
this.videoElementManager.cleanup();
|
|
1378
|
+
logger.debug("[StreamProcessor] Cleaning up audio track manager");
|
|
1106
1379
|
this.audioTrackManager.cleanup();
|
|
1107
1380
|
if (this.canvasSource) {
|
|
1108
1381
|
this.canvasSource.close();
|
|
@@ -1257,38 +1530,70 @@ class RecordingManager {
|
|
|
1257
1530
|
this.callbacks.onCountdownUpdate(this.recordingState, this.countdownRemaining);
|
|
1258
1531
|
}, COUNTDOWN_UPDATE_INTERVAL);
|
|
1259
1532
|
this.countdownTimeoutId = window.setTimeout(async () => {
|
|
1260
|
-
await this.doStartRecording();
|
|
1533
|
+
await this.doStartRecording().catch(() => {});
|
|
1261
1534
|
}, this.countdownDuration);
|
|
1262
1535
|
}
|
|
1263
1536
|
async doStartRecording() {
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1537
|
+
logger.debug("[RecordingManager] doStartRecording called");
|
|
1538
|
+
this.cancelCountdown();
|
|
1539
|
+
this.recordingState = RECORDING_STATE_RECORDING;
|
|
1540
|
+
this.callbacks.onStateChange(this.recordingState);
|
|
1541
|
+
this.resetRecordingState();
|
|
1542
|
+
const currentStream = this.streamManager.getStream();
|
|
1543
|
+
logger.debug("[RecordingManager] Current stream:", {
|
|
1544
|
+
hasStream: !!currentStream,
|
|
1545
|
+
audioTracks: currentStream?.getAudioTracks().length || 0,
|
|
1546
|
+
videoTracks: currentStream?.getVideoTracks().length || 0
|
|
1547
|
+
});
|
|
1548
|
+
if (!currentStream) {
|
|
1549
|
+
logger.warn("[RecordingManager] No stream available");
|
|
1550
|
+
this.handleError(new Error("No stream available for recording"));
|
|
1551
|
+
this.recordingState = RECORDING_STATE_IDLE;
|
|
1267
1552
|
this.callbacks.onStateChange(this.recordingState);
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
this.startRecordingTimer();
|
|
1278
|
-
if (this.maxRecordingTime && this.maxRecordingTime > 0) {
|
|
1279
|
-
this.maxTimeTimer = window.setTimeout(async () => {
|
|
1280
|
-
if (this.recordingState === RECORDING_STATE_RECORDING) {
|
|
1281
|
-
await this.stopRecording();
|
|
1282
|
-
}
|
|
1283
|
-
}, this.maxRecordingTime);
|
|
1284
|
-
}
|
|
1285
|
-
} catch (error) {
|
|
1286
|
-
this.handleError(error);
|
|
1553
|
+
return;
|
|
1554
|
+
}
|
|
1555
|
+
this.originalCameraStream = currentStream;
|
|
1556
|
+
logger.debug("[RecordingManager] Creating new StreamProcessor");
|
|
1557
|
+
this.streamProcessor = new StreamProcessor;
|
|
1558
|
+
logger.debug("[RecordingManager] StreamProcessor created:", !!this.streamProcessor);
|
|
1559
|
+
const configResult = await this.callbacks.onGetConfig().then((config) => ({ config, error: null })).catch((error) => ({ config: null, error }));
|
|
1560
|
+
if (configResult.error) {
|
|
1561
|
+
this.handleError(configResult.error);
|
|
1287
1562
|
this.recordingState = RECORDING_STATE_IDLE;
|
|
1288
1563
|
this.callbacks.onStateChange(this.recordingState);
|
|
1564
|
+
return;
|
|
1565
|
+
}
|
|
1566
|
+
if (!configResult.config) {
|
|
1567
|
+
this.handleError(new Error("Failed to get recording config"));
|
|
1568
|
+
this.recordingState = RECORDING_STATE_IDLE;
|
|
1569
|
+
this.callbacks.onStateChange(this.recordingState);
|
|
1570
|
+
return;
|
|
1571
|
+
}
|
|
1572
|
+
logger.debug("[RecordingManager] Starting recording with stream manager");
|
|
1573
|
+
const recordingError = await this.streamManager.startRecording(this.streamProcessor, configResult.config).then(() => {
|
|
1574
|
+
logger.info("[RecordingManager] Recording started successfully");
|
|
1575
|
+
return null;
|
|
1576
|
+
}).catch((error) => {
|
|
1577
|
+
logger.error("[RecordingManager] Error starting recording:", error);
|
|
1578
|
+
return error;
|
|
1579
|
+
});
|
|
1580
|
+
if (recordingError) {
|
|
1581
|
+
this.handleError(recordingError);
|
|
1582
|
+
this.recordingState = RECORDING_STATE_IDLE;
|
|
1583
|
+
this.callbacks.onStateChange(this.recordingState);
|
|
1584
|
+
return;
|
|
1585
|
+
}
|
|
1586
|
+
this.startRecordingTimer();
|
|
1587
|
+
if (this.maxRecordingTime && this.maxRecordingTime > 0) {
|
|
1588
|
+
this.maxTimeTimer = window.setTimeout(async () => {
|
|
1589
|
+
if (this.recordingState === RECORDING_STATE_RECORDING) {
|
|
1590
|
+
await this.stopRecording();
|
|
1591
|
+
}
|
|
1592
|
+
}, this.maxRecordingTime);
|
|
1289
1593
|
}
|
|
1290
1594
|
}
|
|
1291
1595
|
async stopRecording() {
|
|
1596
|
+
logger.debug("[RecordingManager] stopRecording called");
|
|
1292
1597
|
try {
|
|
1293
1598
|
this.cancelCountdown();
|
|
1294
1599
|
this.clearTimer(this.recordingIntervalId, clearInterval);
|
|
@@ -1297,7 +1602,9 @@ class RecordingManager {
|
|
|
1297
1602
|
this.maxTimeTimer = null;
|
|
1298
1603
|
this.resetPauseState();
|
|
1299
1604
|
this.callbacks.onStopAudioTracking();
|
|
1605
|
+
logger.debug("[RecordingManager] Stopping recording in stream manager");
|
|
1300
1606
|
const blob = await this.streamManager.stopRecording();
|
|
1607
|
+
logger.info("[RecordingManager] Recording stopped, blob size:", blob.size);
|
|
1301
1608
|
this.recordingState = RECORDING_STATE_IDLE;
|
|
1302
1609
|
this.callbacks.onStateChange(this.recordingState);
|
|
1303
1610
|
this.recordingSeconds = 0;
|
|
@@ -2630,14 +2937,23 @@ class CameraStreamManager {
|
|
|
2630
2937
|
this.mediaRecorder.stop();
|
|
2631
2938
|
}
|
|
2632
2939
|
async startRecording(processor, config) {
|
|
2940
|
+
logger.debug("[CameraStreamManager] startRecording called", {
|
|
2941
|
+
hasMediaStream: !!this.mediaStream,
|
|
2942
|
+
isRecording: this.isRecording(),
|
|
2943
|
+
hasProcessor: !!processor,
|
|
2944
|
+
audioTracks: this.mediaStream?.getAudioTracks().length || 0
|
|
2945
|
+
});
|
|
2633
2946
|
if (!this.mediaStream) {
|
|
2634
2947
|
throw new Error("Stream must be started before recording");
|
|
2635
2948
|
}
|
|
2636
2949
|
if (this.isRecording()) {
|
|
2950
|
+
logger.debug("[CameraStreamManager] Already recording, returning");
|
|
2637
2951
|
return;
|
|
2638
2952
|
}
|
|
2639
2953
|
this.streamProcessor = processor;
|
|
2954
|
+
logger.debug("[CameraStreamManager] StreamProcessor assigned, starting processing");
|
|
2640
2955
|
await processor.startProcessing(this.mediaStream, config);
|
|
2956
|
+
logger.info("[CameraStreamManager] Processing started");
|
|
2641
2957
|
this.bufferSizeUpdateInterval = window.setInterval(() => {
|
|
2642
2958
|
if (!this.streamProcessor) {
|
|
2643
2959
|
return;
|
|
@@ -2658,6 +2974,10 @@ class CameraStreamManager {
|
|
|
2658
2974
|
this.startRecordingTimer();
|
|
2659
2975
|
}
|
|
2660
2976
|
async stopRecording() {
|
|
2977
|
+
logger.debug("[CameraStreamManager] stopRecording called", {
|
|
2978
|
+
hasStreamProcessor: !!this.streamProcessor,
|
|
2979
|
+
isRecording: this.isRecording()
|
|
2980
|
+
});
|
|
2661
2981
|
if (!(this.streamProcessor && this.isRecording())) {
|
|
2662
2982
|
throw new Error("Not currently recording");
|
|
2663
2983
|
}
|
|
@@ -2665,13 +2985,19 @@ class CameraStreamManager {
|
|
|
2665
2985
|
this.clearRecordingTimer();
|
|
2666
2986
|
this.clearBufferSizeInterval();
|
|
2667
2987
|
this.resetPauseState();
|
|
2988
|
+
logger.debug("[CameraStreamManager] Finalizing stream processor");
|
|
2668
2989
|
const result = await this.streamProcessor.finalize();
|
|
2990
|
+
logger.info("[CameraStreamManager] Stream processor finalized", {
|
|
2991
|
+
blobSize: result.blob.size,
|
|
2992
|
+
hasBlob: !!result.blob
|
|
2993
|
+
});
|
|
2669
2994
|
this.setState("active");
|
|
2670
2995
|
this.emit("recordingstop", {
|
|
2671
2996
|
blob: result.blob,
|
|
2672
2997
|
mimeType: "video/mp4"
|
|
2673
2998
|
});
|
|
2674
2999
|
this.streamProcessor = null;
|
|
3000
|
+
logger.debug("[CameraStreamManager] StreamProcessor cleared");
|
|
2675
3001
|
return result.blob;
|
|
2676
3002
|
}
|
|
2677
3003
|
pauseRecording() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vidtreo/recorder",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.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",
|