@vidtreo/recorder 0.8.2 → 0.8.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts 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
- 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,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
- async setupAudioTrack(stream, config) {
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
- this.originalAudioTrack = audioTrack;
519
- this.lastAudioTimestamp = 0;
520
- const AudioContextClass = window.AudioContext || window.webkitAudioContext;
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
- this.audioContext = new AudioContextClass;
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
- this.audioSource = new AudioSampleSource({
670
+ logger.debug("[AudioTrackManager] Creating AudioSampleSource", {
542
671
  codec: audioCodec,
543
672
  bitrate: audioBitrate
544
673
  });
545
- const audioOnlyStream = new MediaStream([this.originalAudioTrack]);
546
- this.mediaStreamSource = this.audioContext.createMediaStreamSource(audioOnlyStream);
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
- await this.audioContext.audioWorklet.addModule(processorUrl);
686
+ logger.debug("[AudioTrackManager] Adding worklet module");
687
+ try {
688
+ await this.audioContext.audioWorklet.addModule(processorUrl);
689
+ logger.debug("[AudioTrackManager] Worklet module added successfully");
690
+ } catch (error) {
691
+ const errorMessage = error instanceof Error ? error.message : String(error);
692
+ logger.warn("[AudioTrackManager] Error adding worklet module:", errorMessage);
693
+ if (!(errorMessage.includes("already") || errorMessage.includes("duplicate") || errorMessage.includes("InvalidStateError"))) {
694
+ logger.error("[AudioTrackManager] Fatal error adding module, cleaning up");
695
+ this.audioSource = null;
696
+ this.mediaStreamSource = null;
697
+ throw error;
698
+ }
699
+ logger.debug("[AudioTrackManager] Module already loaded, continuing");
700
+ }
701
+ }
702
+ createWorkletNode() {
703
+ if (!this.audioContext) {
704
+ throw new Error("AudioContext not initialized");
705
+ }
706
+ logger.debug("[AudioTrackManager] Creating AudioWorkletNode");
549
707
  this.audioWorkletNode = new AudioWorkletNode(this.audioContext, "audio-capture-processor");
708
+ logger.debug("[AudioTrackManager] AudioWorkletNode created:", !!this.audioWorkletNode);
709
+ }
710
+ setupWorkletMessageHandler() {
711
+ if (!this.audioWorkletNode) {
712
+ throw new Error("AudioWorkletNode not initialized");
713
+ }
550
714
  this.audioWorkletNode.port.onmessage = (event) => {
551
715
  if (this.isPaused || this.isMuted || !this.audioSource) {
552
716
  return;
553
717
  }
554
718
  const { data, sampleRate, numberOfChannels, duration, bufferLength } = event.data;
555
719
  const float32Data = new Float32Array(data, 0, bufferLength);
720
+ if (!this.audioSource) {
721
+ return;
722
+ }
723
+ const audioSource = this.audioSource;
556
724
  const audioSample = new AudioSample({
557
725
  data: float32Data,
558
726
  format: "f32-planar",
@@ -560,18 +728,69 @@ class AudioTrackManager {
560
728
  sampleRate,
561
729
  timestamp: this.lastAudioTimestamp
562
730
  });
563
- this.audioSource.add(audioSample);
564
- audioSample.close();
565
- this.lastAudioTimestamp += duration;
731
+ const currentTimestamp = this.lastAudioTimestamp;
732
+ const addOperation = this.pendingAddOperation ? this.pendingAddOperation.then(() => audioSource.add(audioSample)).catch(() => audioSource.add(audioSample)) : audioSource.add(audioSample);
733
+ this.pendingAddOperation = addOperation.then(() => {
734
+ audioSample.close();
735
+ this.lastAudioTimestamp = currentTimestamp + duration;
736
+ }).catch(() => {
737
+ audioSample.close();
738
+ this.lastAudioTimestamp = currentTimestamp + duration;
739
+ });
566
740
  };
741
+ logger.debug("[AudioTrackManager] onmessage handler set");
742
+ }
743
+ setupAudioNodes() {
744
+ if (!(this.audioContext && this.audioWorkletNode && this.mediaStreamSource)) {
745
+ throw new Error("Audio nodes not initialized");
746
+ }
567
747
  this.gainNode = this.audioContext.createGain();
568
748
  this.gainNode.gain.value = 0;
749
+ logger.debug("[AudioTrackManager] GainNode created");
569
750
  this.mediaStreamSource.connect(this.audioWorkletNode);
570
751
  this.audioWorkletNode.connect(this.gainNode);
571
752
  this.gainNode.connect(this.audioContext.destination);
572
- if (this.audioContext.state === "suspended") {
573
- await this.audioContext.resume();
753
+ logger.debug("[AudioTrackManager] Audio nodes connected");
754
+ }
755
+ async setupAudioTrack(stream, config) {
756
+ logger.debug("[AudioTrackManager] setupAudioTrack called", {
757
+ hasStream: !!stream,
758
+ audioContextState: this.audioContext?.state,
759
+ hasAudioContext: !!this.audioContext,
760
+ hasAudioSource: !!this.audioSource
761
+ });
762
+ const audioTrack = this.validateStreamAndConfig(stream, config);
763
+ if (!audioTrack) {
764
+ return null;
765
+ }
766
+ this.prepareAudioTrack(audioTrack);
767
+ const audioContext = await this.setupAudioContext();
768
+ if (!audioContext) {
769
+ return null;
770
+ }
771
+ this.audioSource = this.createAudioSource(config);
772
+ if (!this.originalAudioTrack) {
773
+ throw new Error("Original audio track not set");
574
774
  }
775
+ const audioOnlyStream = new MediaStream([this.originalAudioTrack]);
776
+ this.mediaStreamSource = audioContext.createMediaStreamSource(audioOnlyStream);
777
+ logger.debug("[AudioTrackManager] MediaStreamSource created:", !!this.mediaStreamSource);
778
+ try {
779
+ await this.addWorkletModule();
780
+ this.createWorkletNode();
781
+ } catch (error) {
782
+ logger.error("[AudioTrackManager] Error creating AudioWorkletNode:", error);
783
+ this.audioSource = null;
784
+ this.mediaStreamSource = null;
785
+ throw error;
786
+ }
787
+ this.setupWorkletMessageHandler();
788
+ this.setupAudioNodes();
789
+ if (audioContext.state === "suspended") {
790
+ logger.debug("[AudioTrackManager] Resuming AudioContext after setup");
791
+ await audioContext.resume();
792
+ }
793
+ logger.debug("[AudioTrackManager] setupAudioTrack completed, returning audioSource:", !!this.audioSource);
575
794
  return this.audioSource;
576
795
  }
577
796
  toggleMute() {
@@ -624,32 +843,50 @@ class AudioTrackManager {
624
843
  this.onMuteStateChange = callback;
625
844
  }
626
845
  cleanup() {
846
+ logger.debug("[AudioTrackManager] cleanup called", {
847
+ hasAudioSource: !!this.audioSource,
848
+ hasAudioContext: !!this.audioContext,
849
+ audioContextState: this.audioContext?.state,
850
+ hasWorkletNode: !!this.audioWorkletNode
851
+ });
627
852
  this.audioSource = null;
853
+ this.pendingAddOperation = null;
854
+ this.isMuted = false;
855
+ this.isPaused = false;
628
856
  if (this.audioWorkletNode) {
857
+ logger.debug("[AudioTrackManager] Disconnecting and closing worklet node");
629
858
  this.audioWorkletNode.disconnect();
630
859
  this.audioWorkletNode.port.close();
631
860
  this.audioWorkletNode = null;
632
861
  }
633
862
  if (this.gainNode) {
863
+ logger.debug("[AudioTrackManager] Disconnecting gain node");
634
864
  this.gainNode.disconnect();
635
865
  this.gainNode = null;
636
866
  }
637
867
  if (this.mediaStreamSource) {
868
+ logger.debug("[AudioTrackManager] Disconnecting media stream source");
638
869
  this.mediaStreamSource.disconnect();
639
870
  this.mediaStreamSource = null;
640
871
  }
641
- if (this.audioContext) {
642
- this.audioContext.close();
643
- this.audioContext = null;
872
+ if (this.audioContext && this.audioContext.state !== "closed") {
873
+ logger.debug("[AudioTrackManager] Suspending AudioContext (not closing)");
874
+ if (typeof this.audioContext.suspend === "function") {
875
+ this.audioContext.suspend().catch((err) => {
876
+ logger.warn("[AudioTrackManager] Error suspending AudioContext:", err);
877
+ });
878
+ }
644
879
  }
645
880
  if (this.originalAudioTrack) {
646
- this.originalAudioTrack.stop();
881
+ logger.debug("[AudioTrackManager] Stopping cloned audio track", {
882
+ trackState: this.originalAudioTrack.readyState
883
+ });
884
+ if (this.originalAudioTrack.readyState === "live" && typeof this.originalAudioTrack.stop === "function") {
885
+ this.originalAudioTrack.stop();
886
+ logger.debug("[AudioTrackManager] Cloned audio track stopped");
887
+ }
647
888
  this.originalAudioTrack = null;
648
889
  }
649
- if (workletBlobUrl) {
650
- URL.revokeObjectURL(workletBlobUrl);
651
- workletBlobUrl = null;
652
- }
653
890
  this.lastAudioTimestamp = 0;
654
891
  }
655
892
  }
@@ -916,6 +1153,9 @@ class VideoElementManager {
916
1153
  isActive = false;
917
1154
  isIntentionallyPaused = false;
918
1155
  create(stream) {
1156
+ if (this.videoElement) {
1157
+ this.cleanup();
1158
+ }
919
1159
  const videoElement = document.createElement("video");
920
1160
  videoElement.srcObject = stream;
921
1161
  videoElement.autoplay = true;
@@ -965,9 +1205,17 @@ class VideoElementManager {
965
1205
  }
966
1206
  cleanup() {
967
1207
  if (this.videoElement) {
1208
+ if (typeof this.videoElement.pause === "function") {
1209
+ this.videoElement.pause();
1210
+ }
968
1211
  this.videoElement.srcObject = null;
1212
+ if (typeof this.videoElement.remove === "function") {
1213
+ this.videoElement.remove();
1214
+ }
969
1215
  this.videoElement = null;
970
1216
  }
1217
+ this.isActive = false;
1218
+ this.isIntentionallyPaused = false;
971
1219
  }
972
1220
  waitForVideoReady(shouldPlay) {
973
1221
  if (!this.videoElement) {
@@ -975,21 +1223,30 @@ class VideoElementManager {
975
1223
  }
976
1224
  const videoElement = this.videoElement;
977
1225
  return new Promise((resolve, reject) => {
1226
+ let timeoutId;
978
1227
  const cleanup = () => {
979
1228
  videoElement.removeEventListener("loadedmetadata", onLoadedMetadata);
980
1229
  videoElement.removeEventListener("error", onError);
1230
+ if (timeoutId !== undefined) {
1231
+ clearTimeout(timeoutId);
1232
+ timeoutId = undefined;
1233
+ }
981
1234
  };
982
1235
  const onLoadedMetadata = () => {
983
1236
  cleanup();
984
1237
  if (shouldPlay) {
985
- videoElement.play().then(resolve).catch(reject);
1238
+ videoElement.play().then(() => {
1239
+ resolve();
1240
+ }).catch((err) => {
1241
+ reject(err);
1242
+ });
986
1243
  } else {
987
1244
  resolve();
988
1245
  }
989
1246
  };
990
- const onError = () => {
1247
+ const onError = (event) => {
991
1248
  cleanup();
992
- reject(new Error("Failed to load video metadata"));
1249
+ reject(new Error(`Failed to load video metadata: ${event.type}`));
993
1250
  };
994
1251
  if (videoElement.readyState >= READY_STATE_HAVE_CURRENT_DATA2) {
995
1252
  if (shouldPlay) {
@@ -999,14 +1256,16 @@ class VideoElementManager {
999
1256
  }
1000
1257
  return;
1001
1258
  }
1002
- videoElement.addEventListener("loadedmetadata", onLoadedMetadata);
1003
- videoElement.addEventListener("error", onError);
1004
- if (!shouldPlay) {
1259
+ videoElement.addEventListener("loadedmetadata", onLoadedMetadata, {
1260
+ once: true
1261
+ });
1262
+ videoElement.addEventListener("error", onError, { once: true });
1263
+ if (shouldPlay) {
1005
1264
  videoElement.play().catch(reject);
1006
1265
  }
1007
- setTimeout(() => {
1266
+ timeoutId = window.setTimeout(() => {
1008
1267
  cleanup();
1009
- reject(new Error("Timeout waiting for video to load"));
1268
+ reject(new Error(`Timeout waiting for video to load. ReadyState: ${videoElement.readyState}, SrcObject: ${!!videoElement.srcObject}`));
1010
1269
  }, VIDEO_LOAD_TIMEOUT);
1011
1270
  });
1012
1271
  }
@@ -1066,9 +1325,20 @@ class StreamProcessor {
1066
1325
  latencyMode: REALTIME_LATENCY
1067
1326
  });
1068
1327
  output.addVideoTrack(this.canvasSource);
1328
+ logger.debug("[StreamProcessor] Setting up audio track", {
1329
+ hasStream: !!stream,
1330
+ audioTracks: stream.getAudioTracks().length
1331
+ });
1069
1332
  const audioSource = await this.audioTrackManager.setupAudioTrack(stream, config);
1333
+ logger.debug("[StreamProcessor] Audio track setup result:", {
1334
+ hasAudioSource: !!audioSource,
1335
+ audioSourceType: audioSource?.constructor?.name
1336
+ });
1070
1337
  if (audioSource) {
1338
+ logger.debug("[StreamProcessor] Adding audio track to output");
1071
1339
  output.addAudioTrack(audioSource);
1340
+ } else {
1341
+ logger.warn("[StreamProcessor] No audio source, skipping audio track");
1072
1342
  }
1073
1343
  await output.start();
1074
1344
  if (!this.canvasRenderer) {
@@ -1100,9 +1370,12 @@ class StreamProcessor {
1100
1370
  return this.frameCapturer.isPausedState();
1101
1371
  }
1102
1372
  async finalize() {
1373
+ logger.debug("[StreamProcessor] finalize called");
1103
1374
  this.frameCapturer.stop();
1104
1375
  await this.frameCapturer.waitForPendingFrames();
1376
+ logger.debug("[StreamProcessor] Cleaning up video element manager");
1105
1377
  this.videoElementManager.cleanup();
1378
+ logger.debug("[StreamProcessor] Cleaning up audio track manager");
1106
1379
  this.audioTrackManager.cleanup();
1107
1380
  if (this.canvasSource) {
1108
1381
  this.canvasSource.close();
@@ -1257,38 +1530,70 @@ class RecordingManager {
1257
1530
  this.callbacks.onCountdownUpdate(this.recordingState, this.countdownRemaining);
1258
1531
  }, COUNTDOWN_UPDATE_INTERVAL);
1259
1532
  this.countdownTimeoutId = window.setTimeout(async () => {
1260
- await this.doStartRecording();
1533
+ await this.doStartRecording().catch(() => {});
1261
1534
  }, this.countdownDuration);
1262
1535
  }
1263
1536
  async doStartRecording() {
1264
- try {
1265
- this.cancelCountdown();
1266
- this.recordingState = RECORDING_STATE_RECORDING;
1537
+ logger.debug("[RecordingManager] doStartRecording called");
1538
+ this.cancelCountdown();
1539
+ this.recordingState = RECORDING_STATE_RECORDING;
1540
+ this.callbacks.onStateChange(this.recordingState);
1541
+ this.resetRecordingState();
1542
+ const currentStream = this.streamManager.getStream();
1543
+ logger.debug("[RecordingManager] Current stream:", {
1544
+ hasStream: !!currentStream,
1545
+ audioTracks: currentStream?.getAudioTracks().length || 0,
1546
+ videoTracks: currentStream?.getVideoTracks().length || 0
1547
+ });
1548
+ if (!currentStream) {
1549
+ logger.warn("[RecordingManager] No stream available");
1550
+ this.handleError(new Error("No stream available for recording"));
1551
+ this.recordingState = RECORDING_STATE_IDLE;
1267
1552
  this.callbacks.onStateChange(this.recordingState);
1268
- 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);
1553
+ return;
1554
+ }
1555
+ this.originalCameraStream = currentStream;
1556
+ logger.debug("[RecordingManager] Creating new StreamProcessor");
1557
+ this.streamProcessor = new StreamProcessor;
1558
+ logger.debug("[RecordingManager] StreamProcessor created:", !!this.streamProcessor);
1559
+ const configResult = await this.callbacks.onGetConfig().then((config) => ({ config, error: null })).catch((error) => ({ config: null, error }));
1560
+ if (configResult.error) {
1561
+ this.handleError(configResult.error);
1287
1562
  this.recordingState = RECORDING_STATE_IDLE;
1288
1563
  this.callbacks.onStateChange(this.recordingState);
1564
+ return;
1565
+ }
1566
+ if (!configResult.config) {
1567
+ this.handleError(new Error("Failed to get recording config"));
1568
+ this.recordingState = RECORDING_STATE_IDLE;
1569
+ this.callbacks.onStateChange(this.recordingState);
1570
+ return;
1571
+ }
1572
+ logger.debug("[RecordingManager] Starting recording with stream manager");
1573
+ const recordingError = await this.streamManager.startRecording(this.streamProcessor, configResult.config).then(() => {
1574
+ logger.info("[RecordingManager] Recording started successfully");
1575
+ return null;
1576
+ }).catch((error) => {
1577
+ logger.error("[RecordingManager] Error starting recording:", error);
1578
+ return error;
1579
+ });
1580
+ if (recordingError) {
1581
+ this.handleError(recordingError);
1582
+ this.recordingState = RECORDING_STATE_IDLE;
1583
+ this.callbacks.onStateChange(this.recordingState);
1584
+ return;
1585
+ }
1586
+ this.startRecordingTimer();
1587
+ if (this.maxRecordingTime && this.maxRecordingTime > 0) {
1588
+ this.maxTimeTimer = window.setTimeout(async () => {
1589
+ if (this.recordingState === RECORDING_STATE_RECORDING) {
1590
+ await this.stopRecording();
1591
+ }
1592
+ }, this.maxRecordingTime);
1289
1593
  }
1290
1594
  }
1291
1595
  async stopRecording() {
1596
+ logger.debug("[RecordingManager] stopRecording called");
1292
1597
  try {
1293
1598
  this.cancelCountdown();
1294
1599
  this.clearTimer(this.recordingIntervalId, clearInterval);
@@ -1297,7 +1602,9 @@ class RecordingManager {
1297
1602
  this.maxTimeTimer = null;
1298
1603
  this.resetPauseState();
1299
1604
  this.callbacks.onStopAudioTracking();
1605
+ logger.debug("[RecordingManager] Stopping recording in stream manager");
1300
1606
  const blob = await this.streamManager.stopRecording();
1607
+ logger.info("[RecordingManager] Recording stopped, blob size:", blob.size);
1301
1608
  this.recordingState = RECORDING_STATE_IDLE;
1302
1609
  this.callbacks.onStateChange(this.recordingState);
1303
1610
  this.recordingSeconds = 0;
@@ -2630,14 +2937,23 @@ class CameraStreamManager {
2630
2937
  this.mediaRecorder.stop();
2631
2938
  }
2632
2939
  async startRecording(processor, config) {
2940
+ logger.debug("[CameraStreamManager] startRecording called", {
2941
+ hasMediaStream: !!this.mediaStream,
2942
+ isRecording: this.isRecording(),
2943
+ hasProcessor: !!processor,
2944
+ audioTracks: this.mediaStream?.getAudioTracks().length || 0
2945
+ });
2633
2946
  if (!this.mediaStream) {
2634
2947
  throw new Error("Stream must be started before recording");
2635
2948
  }
2636
2949
  if (this.isRecording()) {
2950
+ logger.debug("[CameraStreamManager] Already recording, returning");
2637
2951
  return;
2638
2952
  }
2639
2953
  this.streamProcessor = processor;
2954
+ logger.debug("[CameraStreamManager] StreamProcessor assigned, starting processing");
2640
2955
  await processor.startProcessing(this.mediaStream, config);
2956
+ logger.info("[CameraStreamManager] Processing started");
2641
2957
  this.bufferSizeUpdateInterval = window.setInterval(() => {
2642
2958
  if (!this.streamProcessor) {
2643
2959
  return;
@@ -2658,6 +2974,10 @@ class CameraStreamManager {
2658
2974
  this.startRecordingTimer();
2659
2975
  }
2660
2976
  async stopRecording() {
2977
+ logger.debug("[CameraStreamManager] stopRecording called", {
2978
+ hasStreamProcessor: !!this.streamProcessor,
2979
+ isRecording: this.isRecording()
2980
+ });
2661
2981
  if (!(this.streamProcessor && this.isRecording())) {
2662
2982
  throw new Error("Not currently recording");
2663
2983
  }
@@ -2665,13 +2985,19 @@ class CameraStreamManager {
2665
2985
  this.clearRecordingTimer();
2666
2986
  this.clearBufferSizeInterval();
2667
2987
  this.resetPauseState();
2988
+ logger.debug("[CameraStreamManager] Finalizing stream processor");
2668
2989
  const result = await this.streamProcessor.finalize();
2990
+ logger.info("[CameraStreamManager] Stream processor finalized", {
2991
+ blobSize: result.blob.size,
2992
+ hasBlob: !!result.blob
2993
+ });
2669
2994
  this.setState("active");
2670
2995
  this.emit("recordingstop", {
2671
2996
  blob: result.blob,
2672
2997
  mimeType: "video/mp4"
2673
2998
  });
2674
2999
  this.streamProcessor = null;
3000
+ logger.debug("[CameraStreamManager] StreamProcessor cleared");
2675
3001
  return result.blob;
2676
3002
  }
2677
3003
  pauseRecording() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vidtreo/recorder",
3
- "version": "0.8.2",
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",