@vidtreo/recorder 0.8.2 → 0.8.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 +41 -0
- package/dist/index.js +456 -73
- 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,26 @@ 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 readonly pendingAudioSamples;
|
|
488
|
+
private isCleanedUp;
|
|
489
|
+
private validateStreamAndConfig;
|
|
490
|
+
private prepareAudioTrack;
|
|
491
|
+
private getAudioContextClass;
|
|
492
|
+
private setupAudioContext;
|
|
493
|
+
private createAudioSource;
|
|
494
|
+
private addWorkletModule;
|
|
495
|
+
private createWorkletNode;
|
|
496
|
+
private setupWorkletMessageHandler;
|
|
497
|
+
private closeSampleSafely;
|
|
498
|
+
private closePendingAudioSamples;
|
|
499
|
+
private cleanupAudioNodes;
|
|
500
|
+
private cleanupAudioContext;
|
|
501
|
+
private cleanupAudioTrack;
|
|
502
|
+
private setupAudioNodes;
|
|
462
503
|
setupAudioTrack(stream: MediaStream, config: TranscodeConfig): Promise<AudioSampleSource | null>;
|
|
463
504
|
toggleMute(): void;
|
|
464
505
|
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,31 @@ 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
|
+
pendingAudioSamples = new Set;
|
|
591
|
+
isCleanedUp = false;
|
|
592
|
+
validateStreamAndConfig(stream, config) {
|
|
507
593
|
if (!stream) {
|
|
594
|
+
logger.debug("[AudioTrackManager] No stream provided");
|
|
508
595
|
return null;
|
|
509
596
|
}
|
|
510
597
|
const audioTracks = stream.getAudioTracks();
|
|
598
|
+
logger.debug("[AudioTrackManager] Audio tracks found:", audioTracks.length);
|
|
511
599
|
if (audioTracks.length === 0) {
|
|
600
|
+
logger.debug("[AudioTrackManager] No audio tracks in stream");
|
|
512
601
|
return null;
|
|
513
602
|
}
|
|
514
603
|
const audioTrack = audioTracks[0];
|
|
515
604
|
if (!audioTrack) {
|
|
605
|
+
logger.debug("[AudioTrackManager] First audio track is null");
|
|
516
606
|
return null;
|
|
517
607
|
}
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
if (!AudioContextClass) {
|
|
608
|
+
logger.debug("[AudioTrackManager] Audio track state:", audioTrack.readyState);
|
|
609
|
+
if (audioTrack.readyState !== "live") {
|
|
610
|
+
logger.debug("[AudioTrackManager] Audio track is not live");
|
|
522
611
|
return null;
|
|
523
612
|
}
|
|
524
613
|
if (config.audioBitrate === undefined || config.audioBitrate === null) {
|
|
@@ -527,32 +616,114 @@ class AudioTrackManager {
|
|
|
527
616
|
if (!config.audioCodec) {
|
|
528
617
|
return null;
|
|
529
618
|
}
|
|
530
|
-
|
|
619
|
+
return audioTrack;
|
|
620
|
+
}
|
|
621
|
+
prepareAudioTrack(audioTrack) {
|
|
622
|
+
logger.debug("[AudioTrackManager] Cloning audio track to avoid stopping original");
|
|
623
|
+
let clonedTrack;
|
|
624
|
+
if (typeof audioTrack.clone === "function") {
|
|
625
|
+
clonedTrack = audioTrack.clone();
|
|
626
|
+
logger.debug("[AudioTrackManager] Audio track cloned successfully", {
|
|
627
|
+
clonedState: clonedTrack.readyState,
|
|
628
|
+
originalState: audioTrack.readyState
|
|
629
|
+
});
|
|
630
|
+
} else {
|
|
631
|
+
logger.debug("[AudioTrackManager] clone() not available, using original track");
|
|
632
|
+
clonedTrack = audioTrack;
|
|
633
|
+
}
|
|
634
|
+
this.originalAudioTrack = clonedTrack;
|
|
635
|
+
this.lastAudioTimestamp = 0;
|
|
636
|
+
this.pendingAddOperation = null;
|
|
637
|
+
this.isCleanedUp = false;
|
|
638
|
+
logger.debug("[AudioTrackManager] Reset state, originalAudioTrack set (cloned)");
|
|
639
|
+
return clonedTrack;
|
|
640
|
+
}
|
|
641
|
+
getAudioContextClass() {
|
|
642
|
+
const AudioContextClass = window.AudioContext || window.webkitAudioContext;
|
|
643
|
+
return AudioContextClass || null;
|
|
644
|
+
}
|
|
645
|
+
async setupAudioContext() {
|
|
646
|
+
const AudioContextClass = this.getAudioContextClass();
|
|
647
|
+
if (!AudioContextClass) {
|
|
648
|
+
return null;
|
|
649
|
+
}
|
|
650
|
+
if (!this.audioContext || this.audioContext.state === "closed") {
|
|
651
|
+
logger.debug("[AudioTrackManager] Creating new AudioContext");
|
|
652
|
+
this.audioContext = new AudioContextClass;
|
|
653
|
+
} else {
|
|
654
|
+
logger.debug("[AudioTrackManager] Reusing existing AudioContext", {
|
|
655
|
+
state: this.audioContext.state
|
|
656
|
+
});
|
|
657
|
+
}
|
|
531
658
|
if (this.audioContext.state === "suspended") {
|
|
659
|
+
logger.debug("[AudioTrackManager] Resuming suspended AudioContext");
|
|
532
660
|
await this.audioContext.resume();
|
|
533
661
|
}
|
|
534
662
|
if (!this.audioContext.audioWorklet) {
|
|
663
|
+
logger.debug("[AudioTrackManager] AudioContext has no audioWorklet support");
|
|
535
664
|
await this.audioContext.close();
|
|
536
665
|
this.audioContext = null;
|
|
537
666
|
return null;
|
|
538
667
|
}
|
|
668
|
+
return this.audioContext;
|
|
669
|
+
}
|
|
670
|
+
createAudioSource(config) {
|
|
539
671
|
const audioBitrate = config.audioBitrate;
|
|
540
672
|
const audioCodec = config.audioCodec;
|
|
541
|
-
|
|
673
|
+
logger.debug("[AudioTrackManager] Creating AudioSampleSource", {
|
|
542
674
|
codec: audioCodec,
|
|
543
675
|
bitrate: audioBitrate
|
|
544
676
|
});
|
|
545
|
-
const
|
|
546
|
-
|
|
677
|
+
const audioSource = new AudioSampleSource({
|
|
678
|
+
codec: audioCodec,
|
|
679
|
+
bitrate: audioBitrate
|
|
680
|
+
});
|
|
681
|
+
logger.debug("[AudioTrackManager] AudioSampleSource created:", !!audioSource);
|
|
682
|
+
return audioSource;
|
|
683
|
+
}
|
|
684
|
+
async addWorkletModule() {
|
|
685
|
+
if (!this.audioContext) {
|
|
686
|
+
throw new Error("AudioContext not initialized");
|
|
687
|
+
}
|
|
547
688
|
const processorUrl = getWorkletUrl();
|
|
548
|
-
|
|
689
|
+
logger.debug("[AudioTrackManager] Adding worklet module");
|
|
690
|
+
try {
|
|
691
|
+
await this.audioContext.audioWorklet.addModule(processorUrl);
|
|
692
|
+
logger.debug("[AudioTrackManager] Worklet module added successfully");
|
|
693
|
+
} catch (error) {
|
|
694
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
695
|
+
logger.warn("[AudioTrackManager] Error adding worklet module:", errorMessage);
|
|
696
|
+
if (!(errorMessage.includes("already") || errorMessage.includes("duplicate") || errorMessage.includes("InvalidStateError"))) {
|
|
697
|
+
logger.error("[AudioTrackManager] Fatal error adding module, cleaning up");
|
|
698
|
+
this.audioSource = null;
|
|
699
|
+
this.mediaStreamSource = null;
|
|
700
|
+
throw error;
|
|
701
|
+
}
|
|
702
|
+
logger.debug("[AudioTrackManager] Module already loaded, continuing");
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
createWorkletNode() {
|
|
706
|
+
if (!this.audioContext) {
|
|
707
|
+
throw new Error("AudioContext not initialized");
|
|
708
|
+
}
|
|
709
|
+
logger.debug("[AudioTrackManager] Creating AudioWorkletNode");
|
|
549
710
|
this.audioWorkletNode = new AudioWorkletNode(this.audioContext, "audio-capture-processor");
|
|
711
|
+
logger.debug("[AudioTrackManager] AudioWorkletNode created:", !!this.audioWorkletNode);
|
|
712
|
+
}
|
|
713
|
+
setupWorkletMessageHandler() {
|
|
714
|
+
if (!this.audioWorkletNode) {
|
|
715
|
+
throw new Error("AudioWorkletNode not initialized");
|
|
716
|
+
}
|
|
550
717
|
this.audioWorkletNode.port.onmessage = (event) => {
|
|
551
|
-
if (this.isPaused || this.isMuted || !this.audioSource) {
|
|
718
|
+
if (this.isCleanedUp || this.isPaused || this.isMuted || !this.audioSource) {
|
|
552
719
|
return;
|
|
553
720
|
}
|
|
554
721
|
const { data, sampleRate, numberOfChannels, duration, bufferLength } = event.data;
|
|
555
722
|
const float32Data = new Float32Array(data, 0, bufferLength);
|
|
723
|
+
if (this.isCleanedUp || !this.audioSource) {
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
const audioSource = this.audioSource;
|
|
556
727
|
const audioSample = new AudioSample({
|
|
557
728
|
data: float32Data,
|
|
558
729
|
format: "f32-planar",
|
|
@@ -560,18 +731,151 @@ class AudioTrackManager {
|
|
|
560
731
|
sampleRate,
|
|
561
732
|
timestamp: this.lastAudioTimestamp
|
|
562
733
|
});
|
|
563
|
-
this.
|
|
564
|
-
|
|
565
|
-
|
|
734
|
+
this.pendingAudioSamples.add(audioSample);
|
|
735
|
+
const currentTimestamp = this.lastAudioTimestamp;
|
|
736
|
+
let addOperation;
|
|
737
|
+
if (this.pendingAddOperation) {
|
|
738
|
+
addOperation = this.pendingAddOperation.then(() => {
|
|
739
|
+
if (this.isCleanedUp || !this.audioSource) {
|
|
740
|
+
this.closeSampleSafely(audioSample);
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
return audioSource.add(audioSample);
|
|
744
|
+
}).catch(() => {
|
|
745
|
+
if (this.isCleanedUp || !this.audioSource) {
|
|
746
|
+
this.closeSampleSafely(audioSample);
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
return audioSource.add(audioSample);
|
|
750
|
+
});
|
|
751
|
+
} else if (this.isCleanedUp) {
|
|
752
|
+
this.closeSampleSafely(audioSample);
|
|
753
|
+
addOperation = Promise.resolve();
|
|
754
|
+
} else {
|
|
755
|
+
addOperation = audioSource.add(audioSample);
|
|
756
|
+
}
|
|
757
|
+
this.pendingAddOperation = addOperation.then(() => {
|
|
758
|
+
this.closeSampleSafely(audioSample);
|
|
759
|
+
if (!this.isCleanedUp) {
|
|
760
|
+
this.lastAudioTimestamp = currentTimestamp + duration;
|
|
761
|
+
}
|
|
762
|
+
}).catch(() => {
|
|
763
|
+
this.closeSampleSafely(audioSample);
|
|
764
|
+
if (!this.isCleanedUp) {
|
|
765
|
+
this.lastAudioTimestamp = currentTimestamp + duration;
|
|
766
|
+
}
|
|
767
|
+
});
|
|
566
768
|
};
|
|
769
|
+
logger.debug("[AudioTrackManager] onmessage handler set");
|
|
770
|
+
}
|
|
771
|
+
closeSampleSafely(audioSample) {
|
|
772
|
+
if (this.pendingAudioSamples.has(audioSample)) {
|
|
773
|
+
audioSample.close();
|
|
774
|
+
this.pendingAudioSamples.delete(audioSample);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
closePendingAudioSamples() {
|
|
778
|
+
for (const audioSample of this.pendingAudioSamples) {
|
|
779
|
+
try {
|
|
780
|
+
audioSample.close();
|
|
781
|
+
} catch (error) {
|
|
782
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
783
|
+
logger.warn("[AudioTrackManager] Error closing AudioSample during cleanup:", errorMessage);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
this.pendingAudioSamples.clear();
|
|
787
|
+
}
|
|
788
|
+
cleanupAudioNodes() {
|
|
789
|
+
if (this.audioWorkletNode) {
|
|
790
|
+
logger.debug("[AudioTrackManager] Disconnecting and closing worklet node");
|
|
791
|
+
this.audioWorkletNode.disconnect();
|
|
792
|
+
this.audioWorkletNode.port.close();
|
|
793
|
+
this.audioWorkletNode = null;
|
|
794
|
+
}
|
|
795
|
+
if (this.gainNode) {
|
|
796
|
+
logger.debug("[AudioTrackManager] Disconnecting gain node");
|
|
797
|
+
this.gainNode.disconnect();
|
|
798
|
+
this.gainNode = null;
|
|
799
|
+
}
|
|
800
|
+
if (this.mediaStreamSource) {
|
|
801
|
+
logger.debug("[AudioTrackManager] Disconnecting media stream source");
|
|
802
|
+
this.mediaStreamSource.disconnect();
|
|
803
|
+
this.mediaStreamSource = null;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
cleanupAudioContext() {
|
|
807
|
+
if (this.audioContext && this.audioContext.state !== "closed") {
|
|
808
|
+
logger.debug("[AudioTrackManager] Suspending AudioContext (not closing)");
|
|
809
|
+
if (typeof this.audioContext.suspend === "function") {
|
|
810
|
+
this.audioContext.suspend().catch((err) => {
|
|
811
|
+
logger.warn("[AudioTrackManager] Error suspending AudioContext:", err);
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
cleanupAudioTrack() {
|
|
817
|
+
if (this.originalAudioTrack) {
|
|
818
|
+
logger.debug("[AudioTrackManager] Stopping cloned audio track", {
|
|
819
|
+
trackState: this.originalAudioTrack.readyState
|
|
820
|
+
});
|
|
821
|
+
if (this.originalAudioTrack.readyState === "live" && typeof this.originalAudioTrack.stop === "function") {
|
|
822
|
+
this.originalAudioTrack.stop();
|
|
823
|
+
logger.debug("[AudioTrackManager] Cloned audio track stopped");
|
|
824
|
+
}
|
|
825
|
+
this.originalAudioTrack = null;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
setupAudioNodes() {
|
|
829
|
+
if (!(this.audioContext && this.audioWorkletNode && this.mediaStreamSource)) {
|
|
830
|
+
throw new Error("Audio nodes not initialized");
|
|
831
|
+
}
|
|
567
832
|
this.gainNode = this.audioContext.createGain();
|
|
568
833
|
this.gainNode.gain.value = 0;
|
|
834
|
+
logger.debug("[AudioTrackManager] GainNode created");
|
|
569
835
|
this.mediaStreamSource.connect(this.audioWorkletNode);
|
|
570
836
|
this.audioWorkletNode.connect(this.gainNode);
|
|
571
837
|
this.gainNode.connect(this.audioContext.destination);
|
|
572
|
-
|
|
573
|
-
|
|
838
|
+
logger.debug("[AudioTrackManager] Audio nodes connected");
|
|
839
|
+
}
|
|
840
|
+
async setupAudioTrack(stream, config) {
|
|
841
|
+
logger.debug("[AudioTrackManager] setupAudioTrack called", {
|
|
842
|
+
hasStream: !!stream,
|
|
843
|
+
audioContextState: this.audioContext?.state,
|
|
844
|
+
hasAudioContext: !!this.audioContext,
|
|
845
|
+
hasAudioSource: !!this.audioSource
|
|
846
|
+
});
|
|
847
|
+
const audioTrack = this.validateStreamAndConfig(stream, config);
|
|
848
|
+
if (!audioTrack) {
|
|
849
|
+
return null;
|
|
850
|
+
}
|
|
851
|
+
this.prepareAudioTrack(audioTrack);
|
|
852
|
+
const audioContext = await this.setupAudioContext();
|
|
853
|
+
if (!audioContext) {
|
|
854
|
+
return null;
|
|
574
855
|
}
|
|
856
|
+
this.audioSource = this.createAudioSource(config);
|
|
857
|
+
if (!this.originalAudioTrack) {
|
|
858
|
+
throw new Error("Original audio track not set");
|
|
859
|
+
}
|
|
860
|
+
const audioOnlyStream = new MediaStream([this.originalAudioTrack]);
|
|
861
|
+
this.mediaStreamSource = audioContext.createMediaStreamSource(audioOnlyStream);
|
|
862
|
+
logger.debug("[AudioTrackManager] MediaStreamSource created:", !!this.mediaStreamSource);
|
|
863
|
+
try {
|
|
864
|
+
await this.addWorkletModule();
|
|
865
|
+
this.createWorkletNode();
|
|
866
|
+
} catch (error) {
|
|
867
|
+
logger.error("[AudioTrackManager] Error creating AudioWorkletNode:", error);
|
|
868
|
+
this.audioSource = null;
|
|
869
|
+
this.mediaStreamSource = null;
|
|
870
|
+
throw error;
|
|
871
|
+
}
|
|
872
|
+
this.setupWorkletMessageHandler();
|
|
873
|
+
this.setupAudioNodes();
|
|
874
|
+
if (audioContext.state === "suspended") {
|
|
875
|
+
logger.debug("[AudioTrackManager] Resuming AudioContext after setup");
|
|
876
|
+
await audioContext.resume();
|
|
877
|
+
}
|
|
878
|
+
logger.debug("[AudioTrackManager] setupAudioTrack completed, returning audioSource:", !!this.audioSource);
|
|
575
879
|
return this.audioSource;
|
|
576
880
|
}
|
|
577
881
|
toggleMute() {
|
|
@@ -624,32 +928,22 @@ class AudioTrackManager {
|
|
|
624
928
|
this.onMuteStateChange = callback;
|
|
625
929
|
}
|
|
626
930
|
cleanup() {
|
|
931
|
+
logger.debug("[AudioTrackManager] cleanup called", {
|
|
932
|
+
hasAudioSource: !!this.audioSource,
|
|
933
|
+
hasAudioContext: !!this.audioContext,
|
|
934
|
+
audioContextState: this.audioContext?.state,
|
|
935
|
+
hasWorkletNode: !!this.audioWorkletNode,
|
|
936
|
+
pendingSamples: this.pendingAudioSamples.size
|
|
937
|
+
});
|
|
938
|
+
this.isCleanedUp = true;
|
|
939
|
+
this.closePendingAudioSamples();
|
|
627
940
|
this.audioSource = null;
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
this.gainNode.disconnect();
|
|
635
|
-
this.gainNode = null;
|
|
636
|
-
}
|
|
637
|
-
if (this.mediaStreamSource) {
|
|
638
|
-
this.mediaStreamSource.disconnect();
|
|
639
|
-
this.mediaStreamSource = null;
|
|
640
|
-
}
|
|
641
|
-
if (this.audioContext) {
|
|
642
|
-
this.audioContext.close();
|
|
643
|
-
this.audioContext = null;
|
|
644
|
-
}
|
|
645
|
-
if (this.originalAudioTrack) {
|
|
646
|
-
this.originalAudioTrack.stop();
|
|
647
|
-
this.originalAudioTrack = null;
|
|
648
|
-
}
|
|
649
|
-
if (workletBlobUrl) {
|
|
650
|
-
URL.revokeObjectURL(workletBlobUrl);
|
|
651
|
-
workletBlobUrl = null;
|
|
652
|
-
}
|
|
941
|
+
this.pendingAddOperation = null;
|
|
942
|
+
this.isMuted = false;
|
|
943
|
+
this.isPaused = false;
|
|
944
|
+
this.cleanupAudioNodes();
|
|
945
|
+
this.cleanupAudioContext();
|
|
946
|
+
this.cleanupAudioTrack();
|
|
653
947
|
this.lastAudioTimestamp = 0;
|
|
654
948
|
}
|
|
655
949
|
}
|
|
@@ -916,6 +1210,9 @@ class VideoElementManager {
|
|
|
916
1210
|
isActive = false;
|
|
917
1211
|
isIntentionallyPaused = false;
|
|
918
1212
|
create(stream) {
|
|
1213
|
+
if (this.videoElement) {
|
|
1214
|
+
this.cleanup();
|
|
1215
|
+
}
|
|
919
1216
|
const videoElement = document.createElement("video");
|
|
920
1217
|
videoElement.srcObject = stream;
|
|
921
1218
|
videoElement.autoplay = true;
|
|
@@ -965,9 +1262,17 @@ class VideoElementManager {
|
|
|
965
1262
|
}
|
|
966
1263
|
cleanup() {
|
|
967
1264
|
if (this.videoElement) {
|
|
1265
|
+
if (typeof this.videoElement.pause === "function") {
|
|
1266
|
+
this.videoElement.pause();
|
|
1267
|
+
}
|
|
968
1268
|
this.videoElement.srcObject = null;
|
|
1269
|
+
if (typeof this.videoElement.remove === "function") {
|
|
1270
|
+
this.videoElement.remove();
|
|
1271
|
+
}
|
|
969
1272
|
this.videoElement = null;
|
|
970
1273
|
}
|
|
1274
|
+
this.isActive = false;
|
|
1275
|
+
this.isIntentionallyPaused = false;
|
|
971
1276
|
}
|
|
972
1277
|
waitForVideoReady(shouldPlay) {
|
|
973
1278
|
if (!this.videoElement) {
|
|
@@ -975,21 +1280,30 @@ class VideoElementManager {
|
|
|
975
1280
|
}
|
|
976
1281
|
const videoElement = this.videoElement;
|
|
977
1282
|
return new Promise((resolve, reject) => {
|
|
1283
|
+
let timeoutId;
|
|
978
1284
|
const cleanup = () => {
|
|
979
1285
|
videoElement.removeEventListener("loadedmetadata", onLoadedMetadata);
|
|
980
1286
|
videoElement.removeEventListener("error", onError);
|
|
1287
|
+
if (timeoutId !== undefined) {
|
|
1288
|
+
clearTimeout(timeoutId);
|
|
1289
|
+
timeoutId = undefined;
|
|
1290
|
+
}
|
|
981
1291
|
};
|
|
982
1292
|
const onLoadedMetadata = () => {
|
|
983
1293
|
cleanup();
|
|
984
1294
|
if (shouldPlay) {
|
|
985
|
-
videoElement.play().then(
|
|
1295
|
+
videoElement.play().then(() => {
|
|
1296
|
+
resolve();
|
|
1297
|
+
}).catch((err) => {
|
|
1298
|
+
reject(err);
|
|
1299
|
+
});
|
|
986
1300
|
} else {
|
|
987
1301
|
resolve();
|
|
988
1302
|
}
|
|
989
1303
|
};
|
|
990
|
-
const onError = () => {
|
|
1304
|
+
const onError = (event) => {
|
|
991
1305
|
cleanup();
|
|
992
|
-
reject(new Error(
|
|
1306
|
+
reject(new Error(`Failed to load video metadata: ${event.type}`));
|
|
993
1307
|
};
|
|
994
1308
|
if (videoElement.readyState >= READY_STATE_HAVE_CURRENT_DATA2) {
|
|
995
1309
|
if (shouldPlay) {
|
|
@@ -999,14 +1313,16 @@ class VideoElementManager {
|
|
|
999
1313
|
}
|
|
1000
1314
|
return;
|
|
1001
1315
|
}
|
|
1002
|
-
videoElement.addEventListener("loadedmetadata", onLoadedMetadata
|
|
1003
|
-
|
|
1004
|
-
|
|
1316
|
+
videoElement.addEventListener("loadedmetadata", onLoadedMetadata, {
|
|
1317
|
+
once: true
|
|
1318
|
+
});
|
|
1319
|
+
videoElement.addEventListener("error", onError, { once: true });
|
|
1320
|
+
if (shouldPlay) {
|
|
1005
1321
|
videoElement.play().catch(reject);
|
|
1006
1322
|
}
|
|
1007
|
-
setTimeout(() => {
|
|
1323
|
+
timeoutId = window.setTimeout(() => {
|
|
1008
1324
|
cleanup();
|
|
1009
|
-
reject(new Error(
|
|
1325
|
+
reject(new Error(`Timeout waiting for video to load. ReadyState: ${videoElement.readyState}, SrcObject: ${!!videoElement.srcObject}`));
|
|
1010
1326
|
}, VIDEO_LOAD_TIMEOUT);
|
|
1011
1327
|
});
|
|
1012
1328
|
}
|
|
@@ -1066,9 +1382,20 @@ class StreamProcessor {
|
|
|
1066
1382
|
latencyMode: REALTIME_LATENCY
|
|
1067
1383
|
});
|
|
1068
1384
|
output.addVideoTrack(this.canvasSource);
|
|
1385
|
+
logger.debug("[StreamProcessor] Setting up audio track", {
|
|
1386
|
+
hasStream: !!stream,
|
|
1387
|
+
audioTracks: stream.getAudioTracks().length
|
|
1388
|
+
});
|
|
1069
1389
|
const audioSource = await this.audioTrackManager.setupAudioTrack(stream, config);
|
|
1390
|
+
logger.debug("[StreamProcessor] Audio track setup result:", {
|
|
1391
|
+
hasAudioSource: !!audioSource,
|
|
1392
|
+
audioSourceType: audioSource?.constructor?.name
|
|
1393
|
+
});
|
|
1070
1394
|
if (audioSource) {
|
|
1395
|
+
logger.debug("[StreamProcessor] Adding audio track to output");
|
|
1071
1396
|
output.addAudioTrack(audioSource);
|
|
1397
|
+
} else {
|
|
1398
|
+
logger.warn("[StreamProcessor] No audio source, skipping audio track");
|
|
1072
1399
|
}
|
|
1073
1400
|
await output.start();
|
|
1074
1401
|
if (!this.canvasRenderer) {
|
|
@@ -1100,9 +1427,12 @@ class StreamProcessor {
|
|
|
1100
1427
|
return this.frameCapturer.isPausedState();
|
|
1101
1428
|
}
|
|
1102
1429
|
async finalize() {
|
|
1430
|
+
logger.debug("[StreamProcessor] finalize called");
|
|
1103
1431
|
this.frameCapturer.stop();
|
|
1104
1432
|
await this.frameCapturer.waitForPendingFrames();
|
|
1433
|
+
logger.debug("[StreamProcessor] Cleaning up video element manager");
|
|
1105
1434
|
this.videoElementManager.cleanup();
|
|
1435
|
+
logger.debug("[StreamProcessor] Cleaning up audio track manager");
|
|
1106
1436
|
this.audioTrackManager.cleanup();
|
|
1107
1437
|
if (this.canvasSource) {
|
|
1108
1438
|
this.canvasSource.close();
|
|
@@ -1257,38 +1587,70 @@ class RecordingManager {
|
|
|
1257
1587
|
this.callbacks.onCountdownUpdate(this.recordingState, this.countdownRemaining);
|
|
1258
1588
|
}, COUNTDOWN_UPDATE_INTERVAL);
|
|
1259
1589
|
this.countdownTimeoutId = window.setTimeout(async () => {
|
|
1260
|
-
await this.doStartRecording();
|
|
1590
|
+
await this.doStartRecording().catch(() => {});
|
|
1261
1591
|
}, this.countdownDuration);
|
|
1262
1592
|
}
|
|
1263
1593
|
async doStartRecording() {
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1594
|
+
logger.debug("[RecordingManager] doStartRecording called");
|
|
1595
|
+
this.cancelCountdown();
|
|
1596
|
+
this.recordingState = RECORDING_STATE_RECORDING;
|
|
1597
|
+
this.callbacks.onStateChange(this.recordingState);
|
|
1598
|
+
this.resetRecordingState();
|
|
1599
|
+
const currentStream = this.streamManager.getStream();
|
|
1600
|
+
logger.debug("[RecordingManager] Current stream:", {
|
|
1601
|
+
hasStream: !!currentStream,
|
|
1602
|
+
audioTracks: currentStream?.getAudioTracks().length || 0,
|
|
1603
|
+
videoTracks: currentStream?.getVideoTracks().length || 0
|
|
1604
|
+
});
|
|
1605
|
+
if (!currentStream) {
|
|
1606
|
+
logger.warn("[RecordingManager] No stream available");
|
|
1607
|
+
this.handleError(new Error("No stream available for recording"));
|
|
1608
|
+
this.recordingState = RECORDING_STATE_IDLE;
|
|
1267
1609
|
this.callbacks.onStateChange(this.recordingState);
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
this.
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
}, this.maxRecordingTime);
|
|
1284
|
-
}
|
|
1285
|
-
} catch (error) {
|
|
1286
|
-
this.handleError(error);
|
|
1610
|
+
return;
|
|
1611
|
+
}
|
|
1612
|
+
this.originalCameraStream = currentStream;
|
|
1613
|
+
logger.debug("[RecordingManager] Creating new StreamProcessor");
|
|
1614
|
+
this.streamProcessor = new StreamProcessor;
|
|
1615
|
+
logger.debug("[RecordingManager] StreamProcessor created:", !!this.streamProcessor);
|
|
1616
|
+
const configResult = await this.callbacks.onGetConfig().then((config) => ({ config, error: null })).catch((error) => ({ config: null, error }));
|
|
1617
|
+
if (configResult.error) {
|
|
1618
|
+
this.handleError(configResult.error);
|
|
1619
|
+
this.recordingState = RECORDING_STATE_IDLE;
|
|
1620
|
+
this.callbacks.onStateChange(this.recordingState);
|
|
1621
|
+
return;
|
|
1622
|
+
}
|
|
1623
|
+
if (!configResult.config) {
|
|
1624
|
+
this.handleError(new Error("Failed to get recording config"));
|
|
1287
1625
|
this.recordingState = RECORDING_STATE_IDLE;
|
|
1288
1626
|
this.callbacks.onStateChange(this.recordingState);
|
|
1627
|
+
return;
|
|
1628
|
+
}
|
|
1629
|
+
logger.debug("[RecordingManager] Starting recording with stream manager");
|
|
1630
|
+
const recordingError = await this.streamManager.startRecording(this.streamProcessor, configResult.config).then(() => {
|
|
1631
|
+
logger.info("[RecordingManager] Recording started successfully");
|
|
1632
|
+
return null;
|
|
1633
|
+
}).catch((error) => {
|
|
1634
|
+
logger.error("[RecordingManager] Error starting recording:", error);
|
|
1635
|
+
return error;
|
|
1636
|
+
});
|
|
1637
|
+
if (recordingError) {
|
|
1638
|
+
this.handleError(recordingError);
|
|
1639
|
+
this.recordingState = RECORDING_STATE_IDLE;
|
|
1640
|
+
this.callbacks.onStateChange(this.recordingState);
|
|
1641
|
+
return;
|
|
1642
|
+
}
|
|
1643
|
+
this.startRecordingTimer();
|
|
1644
|
+
if (this.maxRecordingTime && this.maxRecordingTime > 0) {
|
|
1645
|
+
this.maxTimeTimer = window.setTimeout(async () => {
|
|
1646
|
+
if (this.recordingState === RECORDING_STATE_RECORDING) {
|
|
1647
|
+
await this.stopRecording();
|
|
1648
|
+
}
|
|
1649
|
+
}, this.maxRecordingTime);
|
|
1289
1650
|
}
|
|
1290
1651
|
}
|
|
1291
1652
|
async stopRecording() {
|
|
1653
|
+
logger.debug("[RecordingManager] stopRecording called");
|
|
1292
1654
|
try {
|
|
1293
1655
|
this.cancelCountdown();
|
|
1294
1656
|
this.clearTimer(this.recordingIntervalId, clearInterval);
|
|
@@ -1297,7 +1659,9 @@ class RecordingManager {
|
|
|
1297
1659
|
this.maxTimeTimer = null;
|
|
1298
1660
|
this.resetPauseState();
|
|
1299
1661
|
this.callbacks.onStopAudioTracking();
|
|
1662
|
+
logger.debug("[RecordingManager] Stopping recording in stream manager");
|
|
1300
1663
|
const blob = await this.streamManager.stopRecording();
|
|
1664
|
+
logger.info("[RecordingManager] Recording stopped, blob size:", blob.size);
|
|
1301
1665
|
this.recordingState = RECORDING_STATE_IDLE;
|
|
1302
1666
|
this.callbacks.onStateChange(this.recordingState);
|
|
1303
1667
|
this.recordingSeconds = 0;
|
|
@@ -2630,14 +2994,23 @@ class CameraStreamManager {
|
|
|
2630
2994
|
this.mediaRecorder.stop();
|
|
2631
2995
|
}
|
|
2632
2996
|
async startRecording(processor, config) {
|
|
2997
|
+
logger.debug("[CameraStreamManager] startRecording called", {
|
|
2998
|
+
hasMediaStream: !!this.mediaStream,
|
|
2999
|
+
isRecording: this.isRecording(),
|
|
3000
|
+
hasProcessor: !!processor,
|
|
3001
|
+
audioTracks: this.mediaStream?.getAudioTracks().length || 0
|
|
3002
|
+
});
|
|
2633
3003
|
if (!this.mediaStream) {
|
|
2634
3004
|
throw new Error("Stream must be started before recording");
|
|
2635
3005
|
}
|
|
2636
3006
|
if (this.isRecording()) {
|
|
3007
|
+
logger.debug("[CameraStreamManager] Already recording, returning");
|
|
2637
3008
|
return;
|
|
2638
3009
|
}
|
|
2639
3010
|
this.streamProcessor = processor;
|
|
3011
|
+
logger.debug("[CameraStreamManager] StreamProcessor assigned, starting processing");
|
|
2640
3012
|
await processor.startProcessing(this.mediaStream, config);
|
|
3013
|
+
logger.info("[CameraStreamManager] Processing started");
|
|
2641
3014
|
this.bufferSizeUpdateInterval = window.setInterval(() => {
|
|
2642
3015
|
if (!this.streamProcessor) {
|
|
2643
3016
|
return;
|
|
@@ -2658,6 +3031,10 @@ class CameraStreamManager {
|
|
|
2658
3031
|
this.startRecordingTimer();
|
|
2659
3032
|
}
|
|
2660
3033
|
async stopRecording() {
|
|
3034
|
+
logger.debug("[CameraStreamManager] stopRecording called", {
|
|
3035
|
+
hasStreamProcessor: !!this.streamProcessor,
|
|
3036
|
+
isRecording: this.isRecording()
|
|
3037
|
+
});
|
|
2661
3038
|
if (!(this.streamProcessor && this.isRecording())) {
|
|
2662
3039
|
throw new Error("Not currently recording");
|
|
2663
3040
|
}
|
|
@@ -2665,13 +3042,19 @@ class CameraStreamManager {
|
|
|
2665
3042
|
this.clearRecordingTimer();
|
|
2666
3043
|
this.clearBufferSizeInterval();
|
|
2667
3044
|
this.resetPauseState();
|
|
3045
|
+
logger.debug("[CameraStreamManager] Finalizing stream processor");
|
|
2668
3046
|
const result = await this.streamProcessor.finalize();
|
|
3047
|
+
logger.info("[CameraStreamManager] Stream processor finalized", {
|
|
3048
|
+
blobSize: result.blob.size,
|
|
3049
|
+
hasBlob: !!result.blob
|
|
3050
|
+
});
|
|
2669
3051
|
this.setState("active");
|
|
2670
3052
|
this.emit("recordingstop", {
|
|
2671
3053
|
blob: result.blob,
|
|
2672
3054
|
mimeType: "video/mp4"
|
|
2673
3055
|
});
|
|
2674
3056
|
this.streamProcessor = null;
|
|
3057
|
+
logger.debug("[CameraStreamManager] StreamProcessor cleared");
|
|
2675
3058
|
return result.blob;
|
|
2676
3059
|
}
|
|
2677
3060
|
pauseRecording() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vidtreo/recorder",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.4",
|
|
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",
|