@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 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
- this.audioContext = new AudioContextClass;
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
- async setupAudioTrack(stream, config) {
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
- this.originalAudioTrack = audioTrack;
519
- this.lastAudioTimestamp = 0;
520
- const AudioContextClass = window.AudioContext || window.webkitAudioContext;
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
- this.audioContext = new AudioContextClass;
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
- this.audioSource = new AudioSampleSource({
673
+ logger.debug("[AudioTrackManager] Creating AudioSampleSource", {
542
674
  codec: audioCodec,
543
675
  bitrate: audioBitrate
544
676
  });
545
- const audioOnlyStream = new MediaStream([this.originalAudioTrack]);
546
- this.mediaStreamSource = this.audioContext.createMediaStreamSource(audioOnlyStream);
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
- await this.audioContext.audioWorklet.addModule(processorUrl);
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.audioSource.add(audioSample);
564
- audioSample.close();
565
- this.lastAudioTimestamp += duration;
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
- if (this.audioContext.state === "suspended") {
573
- await this.audioContext.resume();
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
- if (this.audioWorkletNode) {
629
- this.audioWorkletNode.disconnect();
630
- this.audioWorkletNode.port.close();
631
- this.audioWorkletNode = null;
632
- }
633
- if (this.gainNode) {
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(resolve).catch(reject);
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("Failed to load video metadata"));
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
- videoElement.addEventListener("error", onError);
1004
- if (!shouldPlay) {
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("Timeout waiting for video to load"));
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
- try {
1265
- this.cancelCountdown();
1266
- this.recordingState = RECORDING_STATE_RECORDING;
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
- this.resetRecordingState();
1269
- const currentStream = this.streamManager.getStream();
1270
- if (!currentStream) {
1271
- throw new Error("No stream available for recording");
1272
- }
1273
- this.originalCameraStream = currentStream;
1274
- this.streamProcessor = new StreamProcessor;
1275
- const config = await this.callbacks.onGetConfig();
1276
- await this.streamManager.startRecording(this.streamProcessor, config);
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);
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.2",
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",