@vidtreo/recorder 0.8.1 → 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 -54
- 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,17 +728,69 @@ class AudioTrackManager {
|
|
|
560
728
|
sampleRate,
|
|
561
729
|
timestamp: this.lastAudioTimestamp
|
|
562
730
|
});
|
|
563
|
-
this.
|
|
564
|
-
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
|
+
});
|
|
565
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
|
+
}
|
|
566
747
|
this.gainNode = this.audioContext.createGain();
|
|
567
748
|
this.gainNode.gain.value = 0;
|
|
749
|
+
logger.debug("[AudioTrackManager] GainNode created");
|
|
568
750
|
this.mediaStreamSource.connect(this.audioWorkletNode);
|
|
569
751
|
this.audioWorkletNode.connect(this.gainNode);
|
|
570
752
|
this.gainNode.connect(this.audioContext.destination);
|
|
571
|
-
|
|
572
|
-
|
|
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");
|
|
573
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);
|
|
574
794
|
return this.audioSource;
|
|
575
795
|
}
|
|
576
796
|
toggleMute() {
|
|
@@ -623,32 +843,50 @@ class AudioTrackManager {
|
|
|
623
843
|
this.onMuteStateChange = callback;
|
|
624
844
|
}
|
|
625
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
|
+
});
|
|
626
852
|
this.audioSource = null;
|
|
853
|
+
this.pendingAddOperation = null;
|
|
854
|
+
this.isMuted = false;
|
|
855
|
+
this.isPaused = false;
|
|
627
856
|
if (this.audioWorkletNode) {
|
|
857
|
+
logger.debug("[AudioTrackManager] Disconnecting and closing worklet node");
|
|
628
858
|
this.audioWorkletNode.disconnect();
|
|
629
859
|
this.audioWorkletNode.port.close();
|
|
630
860
|
this.audioWorkletNode = null;
|
|
631
861
|
}
|
|
632
862
|
if (this.gainNode) {
|
|
863
|
+
logger.debug("[AudioTrackManager] Disconnecting gain node");
|
|
633
864
|
this.gainNode.disconnect();
|
|
634
865
|
this.gainNode = null;
|
|
635
866
|
}
|
|
636
867
|
if (this.mediaStreamSource) {
|
|
868
|
+
logger.debug("[AudioTrackManager] Disconnecting media stream source");
|
|
637
869
|
this.mediaStreamSource.disconnect();
|
|
638
870
|
this.mediaStreamSource = null;
|
|
639
871
|
}
|
|
640
|
-
if (this.audioContext) {
|
|
641
|
-
|
|
642
|
-
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
|
+
}
|
|
643
879
|
}
|
|
644
880
|
if (this.originalAudioTrack) {
|
|
645
|
-
|
|
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
|
+
}
|
|
646
888
|
this.originalAudioTrack = null;
|
|
647
889
|
}
|
|
648
|
-
if (workletBlobUrl) {
|
|
649
|
-
URL.revokeObjectURL(workletBlobUrl);
|
|
650
|
-
workletBlobUrl = null;
|
|
651
|
-
}
|
|
652
890
|
this.lastAudioTimestamp = 0;
|
|
653
891
|
}
|
|
654
892
|
}
|
|
@@ -915,6 +1153,9 @@ class VideoElementManager {
|
|
|
915
1153
|
isActive = false;
|
|
916
1154
|
isIntentionallyPaused = false;
|
|
917
1155
|
create(stream) {
|
|
1156
|
+
if (this.videoElement) {
|
|
1157
|
+
this.cleanup();
|
|
1158
|
+
}
|
|
918
1159
|
const videoElement = document.createElement("video");
|
|
919
1160
|
videoElement.srcObject = stream;
|
|
920
1161
|
videoElement.autoplay = true;
|
|
@@ -964,9 +1205,17 @@ class VideoElementManager {
|
|
|
964
1205
|
}
|
|
965
1206
|
cleanup() {
|
|
966
1207
|
if (this.videoElement) {
|
|
1208
|
+
if (typeof this.videoElement.pause === "function") {
|
|
1209
|
+
this.videoElement.pause();
|
|
1210
|
+
}
|
|
967
1211
|
this.videoElement.srcObject = null;
|
|
1212
|
+
if (typeof this.videoElement.remove === "function") {
|
|
1213
|
+
this.videoElement.remove();
|
|
1214
|
+
}
|
|
968
1215
|
this.videoElement = null;
|
|
969
1216
|
}
|
|
1217
|
+
this.isActive = false;
|
|
1218
|
+
this.isIntentionallyPaused = false;
|
|
970
1219
|
}
|
|
971
1220
|
waitForVideoReady(shouldPlay) {
|
|
972
1221
|
if (!this.videoElement) {
|
|
@@ -974,21 +1223,30 @@ class VideoElementManager {
|
|
|
974
1223
|
}
|
|
975
1224
|
const videoElement = this.videoElement;
|
|
976
1225
|
return new Promise((resolve, reject) => {
|
|
1226
|
+
let timeoutId;
|
|
977
1227
|
const cleanup = () => {
|
|
978
1228
|
videoElement.removeEventListener("loadedmetadata", onLoadedMetadata);
|
|
979
1229
|
videoElement.removeEventListener("error", onError);
|
|
1230
|
+
if (timeoutId !== undefined) {
|
|
1231
|
+
clearTimeout(timeoutId);
|
|
1232
|
+
timeoutId = undefined;
|
|
1233
|
+
}
|
|
980
1234
|
};
|
|
981
1235
|
const onLoadedMetadata = () => {
|
|
982
1236
|
cleanup();
|
|
983
1237
|
if (shouldPlay) {
|
|
984
|
-
videoElement.play().then(
|
|
1238
|
+
videoElement.play().then(() => {
|
|
1239
|
+
resolve();
|
|
1240
|
+
}).catch((err) => {
|
|
1241
|
+
reject(err);
|
|
1242
|
+
});
|
|
985
1243
|
} else {
|
|
986
1244
|
resolve();
|
|
987
1245
|
}
|
|
988
1246
|
};
|
|
989
|
-
const onError = () => {
|
|
1247
|
+
const onError = (event) => {
|
|
990
1248
|
cleanup();
|
|
991
|
-
reject(new Error(
|
|
1249
|
+
reject(new Error(`Failed to load video metadata: ${event.type}`));
|
|
992
1250
|
};
|
|
993
1251
|
if (videoElement.readyState >= READY_STATE_HAVE_CURRENT_DATA2) {
|
|
994
1252
|
if (shouldPlay) {
|
|
@@ -998,14 +1256,16 @@ class VideoElementManager {
|
|
|
998
1256
|
}
|
|
999
1257
|
return;
|
|
1000
1258
|
}
|
|
1001
|
-
videoElement.addEventListener("loadedmetadata", onLoadedMetadata
|
|
1002
|
-
|
|
1003
|
-
|
|
1259
|
+
videoElement.addEventListener("loadedmetadata", onLoadedMetadata, {
|
|
1260
|
+
once: true
|
|
1261
|
+
});
|
|
1262
|
+
videoElement.addEventListener("error", onError, { once: true });
|
|
1263
|
+
if (shouldPlay) {
|
|
1004
1264
|
videoElement.play().catch(reject);
|
|
1005
1265
|
}
|
|
1006
|
-
setTimeout(() => {
|
|
1266
|
+
timeoutId = window.setTimeout(() => {
|
|
1007
1267
|
cleanup();
|
|
1008
|
-
reject(new Error(
|
|
1268
|
+
reject(new Error(`Timeout waiting for video to load. ReadyState: ${videoElement.readyState}, SrcObject: ${!!videoElement.srcObject}`));
|
|
1009
1269
|
}, VIDEO_LOAD_TIMEOUT);
|
|
1010
1270
|
});
|
|
1011
1271
|
}
|
|
@@ -1065,9 +1325,20 @@ class StreamProcessor {
|
|
|
1065
1325
|
latencyMode: REALTIME_LATENCY
|
|
1066
1326
|
});
|
|
1067
1327
|
output.addVideoTrack(this.canvasSource);
|
|
1328
|
+
logger.debug("[StreamProcessor] Setting up audio track", {
|
|
1329
|
+
hasStream: !!stream,
|
|
1330
|
+
audioTracks: stream.getAudioTracks().length
|
|
1331
|
+
});
|
|
1068
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
|
+
});
|
|
1069
1337
|
if (audioSource) {
|
|
1338
|
+
logger.debug("[StreamProcessor] Adding audio track to output");
|
|
1070
1339
|
output.addAudioTrack(audioSource);
|
|
1340
|
+
} else {
|
|
1341
|
+
logger.warn("[StreamProcessor] No audio source, skipping audio track");
|
|
1071
1342
|
}
|
|
1072
1343
|
await output.start();
|
|
1073
1344
|
if (!this.canvasRenderer) {
|
|
@@ -1099,9 +1370,12 @@ class StreamProcessor {
|
|
|
1099
1370
|
return this.frameCapturer.isPausedState();
|
|
1100
1371
|
}
|
|
1101
1372
|
async finalize() {
|
|
1373
|
+
logger.debug("[StreamProcessor] finalize called");
|
|
1102
1374
|
this.frameCapturer.stop();
|
|
1103
1375
|
await this.frameCapturer.waitForPendingFrames();
|
|
1376
|
+
logger.debug("[StreamProcessor] Cleaning up video element manager");
|
|
1104
1377
|
this.videoElementManager.cleanup();
|
|
1378
|
+
logger.debug("[StreamProcessor] Cleaning up audio track manager");
|
|
1105
1379
|
this.audioTrackManager.cleanup();
|
|
1106
1380
|
if (this.canvasSource) {
|
|
1107
1381
|
this.canvasSource.close();
|
|
@@ -1256,38 +1530,70 @@ class RecordingManager {
|
|
|
1256
1530
|
this.callbacks.onCountdownUpdate(this.recordingState, this.countdownRemaining);
|
|
1257
1531
|
}, COUNTDOWN_UPDATE_INTERVAL);
|
|
1258
1532
|
this.countdownTimeoutId = window.setTimeout(async () => {
|
|
1259
|
-
await this.doStartRecording();
|
|
1533
|
+
await this.doStartRecording().catch(() => {});
|
|
1260
1534
|
}, this.countdownDuration);
|
|
1261
1535
|
}
|
|
1262
1536
|
async doStartRecording() {
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
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;
|
|
1266
1552
|
this.callbacks.onStateChange(this.recordingState);
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
this.startRecordingTimer();
|
|
1277
|
-
if (this.maxRecordingTime && this.maxRecordingTime > 0) {
|
|
1278
|
-
this.maxTimeTimer = window.setTimeout(async () => {
|
|
1279
|
-
if (this.recordingState === RECORDING_STATE_RECORDING) {
|
|
1280
|
-
await this.stopRecording();
|
|
1281
|
-
}
|
|
1282
|
-
}, this.maxRecordingTime);
|
|
1283
|
-
}
|
|
1284
|
-
} catch (error) {
|
|
1285
|
-
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);
|
|
1286
1562
|
this.recordingState = RECORDING_STATE_IDLE;
|
|
1287
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);
|
|
1288
1593
|
}
|
|
1289
1594
|
}
|
|
1290
1595
|
async stopRecording() {
|
|
1596
|
+
logger.debug("[RecordingManager] stopRecording called");
|
|
1291
1597
|
try {
|
|
1292
1598
|
this.cancelCountdown();
|
|
1293
1599
|
this.clearTimer(this.recordingIntervalId, clearInterval);
|
|
@@ -1296,7 +1602,9 @@ class RecordingManager {
|
|
|
1296
1602
|
this.maxTimeTimer = null;
|
|
1297
1603
|
this.resetPauseState();
|
|
1298
1604
|
this.callbacks.onStopAudioTracking();
|
|
1605
|
+
logger.debug("[RecordingManager] Stopping recording in stream manager");
|
|
1299
1606
|
const blob = await this.streamManager.stopRecording();
|
|
1607
|
+
logger.info("[RecordingManager] Recording stopped, blob size:", blob.size);
|
|
1300
1608
|
this.recordingState = RECORDING_STATE_IDLE;
|
|
1301
1609
|
this.callbacks.onStateChange(this.recordingState);
|
|
1302
1610
|
this.recordingSeconds = 0;
|
|
@@ -2629,14 +2937,23 @@ class CameraStreamManager {
|
|
|
2629
2937
|
this.mediaRecorder.stop();
|
|
2630
2938
|
}
|
|
2631
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
|
+
});
|
|
2632
2946
|
if (!this.mediaStream) {
|
|
2633
2947
|
throw new Error("Stream must be started before recording");
|
|
2634
2948
|
}
|
|
2635
2949
|
if (this.isRecording()) {
|
|
2950
|
+
logger.debug("[CameraStreamManager] Already recording, returning");
|
|
2636
2951
|
return;
|
|
2637
2952
|
}
|
|
2638
2953
|
this.streamProcessor = processor;
|
|
2954
|
+
logger.debug("[CameraStreamManager] StreamProcessor assigned, starting processing");
|
|
2639
2955
|
await processor.startProcessing(this.mediaStream, config);
|
|
2956
|
+
logger.info("[CameraStreamManager] Processing started");
|
|
2640
2957
|
this.bufferSizeUpdateInterval = window.setInterval(() => {
|
|
2641
2958
|
if (!this.streamProcessor) {
|
|
2642
2959
|
return;
|
|
@@ -2657,6 +2974,10 @@ class CameraStreamManager {
|
|
|
2657
2974
|
this.startRecordingTimer();
|
|
2658
2975
|
}
|
|
2659
2976
|
async stopRecording() {
|
|
2977
|
+
logger.debug("[CameraStreamManager] stopRecording called", {
|
|
2978
|
+
hasStreamProcessor: !!this.streamProcessor,
|
|
2979
|
+
isRecording: this.isRecording()
|
|
2980
|
+
});
|
|
2660
2981
|
if (!(this.streamProcessor && this.isRecording())) {
|
|
2661
2982
|
throw new Error("Not currently recording");
|
|
2662
2983
|
}
|
|
@@ -2664,13 +2985,19 @@ class CameraStreamManager {
|
|
|
2664
2985
|
this.clearRecordingTimer();
|
|
2665
2986
|
this.clearBufferSizeInterval();
|
|
2666
2987
|
this.resetPauseState();
|
|
2988
|
+
logger.debug("[CameraStreamManager] Finalizing stream processor");
|
|
2667
2989
|
const result = await this.streamProcessor.finalize();
|
|
2990
|
+
logger.info("[CameraStreamManager] Stream processor finalized", {
|
|
2991
|
+
blobSize: result.blob.size,
|
|
2992
|
+
hasBlob: !!result.blob
|
|
2993
|
+
});
|
|
2668
2994
|
this.setState("active");
|
|
2669
2995
|
this.emit("recordingstop", {
|
|
2670
2996
|
blob: result.blob,
|
|
2671
2997
|
mimeType: "video/mp4"
|
|
2672
2998
|
});
|
|
2673
2999
|
this.streamProcessor = null;
|
|
3000
|
+
logger.debug("[CameraStreamManager] StreamProcessor cleared");
|
|
2674
3001
|
return result.blob;
|
|
2675
3002
|
}
|
|
2676
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",
|