@vidtreo/recorder 0.8.1 → 0.8.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts 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,17 +728,69 @@ class AudioTrackManager {
560
728
  sampleRate,
561
729
  timestamp: this.lastAudioTimestamp
562
730
  });
563
- this.audioSource.add(audioSample);
564
- 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
+ });
565
740
  };
741
+ logger.debug("[AudioTrackManager] onmessage handler set");
742
+ }
743
+ setupAudioNodes() {
744
+ if (!(this.audioContext && this.audioWorkletNode && this.mediaStreamSource)) {
745
+ throw new Error("Audio nodes not initialized");
746
+ }
566
747
  this.gainNode = this.audioContext.createGain();
567
748
  this.gainNode.gain.value = 0;
749
+ logger.debug("[AudioTrackManager] GainNode created");
568
750
  this.mediaStreamSource.connect(this.audioWorkletNode);
569
751
  this.audioWorkletNode.connect(this.gainNode);
570
752
  this.gainNode.connect(this.audioContext.destination);
571
- if (this.audioContext.state === "suspended") {
572
- 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");
573
774
  }
775
+ const audioOnlyStream = new MediaStream([this.originalAudioTrack]);
776
+ this.mediaStreamSource = audioContext.createMediaStreamSource(audioOnlyStream);
777
+ logger.debug("[AudioTrackManager] MediaStreamSource created:", !!this.mediaStreamSource);
778
+ try {
779
+ await this.addWorkletModule();
780
+ this.createWorkletNode();
781
+ } catch (error) {
782
+ logger.error("[AudioTrackManager] Error creating AudioWorkletNode:", error);
783
+ this.audioSource = null;
784
+ this.mediaStreamSource = null;
785
+ throw error;
786
+ }
787
+ this.setupWorkletMessageHandler();
788
+ this.setupAudioNodes();
789
+ if (audioContext.state === "suspended") {
790
+ logger.debug("[AudioTrackManager] Resuming AudioContext after setup");
791
+ await audioContext.resume();
792
+ }
793
+ logger.debug("[AudioTrackManager] setupAudioTrack completed, returning audioSource:", !!this.audioSource);
574
794
  return this.audioSource;
575
795
  }
576
796
  toggleMute() {
@@ -623,32 +843,50 @@ class AudioTrackManager {
623
843
  this.onMuteStateChange = callback;
624
844
  }
625
845
  cleanup() {
846
+ logger.debug("[AudioTrackManager] cleanup called", {
847
+ hasAudioSource: !!this.audioSource,
848
+ hasAudioContext: !!this.audioContext,
849
+ audioContextState: this.audioContext?.state,
850
+ hasWorkletNode: !!this.audioWorkletNode
851
+ });
626
852
  this.audioSource = null;
853
+ this.pendingAddOperation = null;
854
+ this.isMuted = false;
855
+ this.isPaused = false;
627
856
  if (this.audioWorkletNode) {
857
+ logger.debug("[AudioTrackManager] Disconnecting and closing worklet node");
628
858
  this.audioWorkletNode.disconnect();
629
859
  this.audioWorkletNode.port.close();
630
860
  this.audioWorkletNode = null;
631
861
  }
632
862
  if (this.gainNode) {
863
+ logger.debug("[AudioTrackManager] Disconnecting gain node");
633
864
  this.gainNode.disconnect();
634
865
  this.gainNode = null;
635
866
  }
636
867
  if (this.mediaStreamSource) {
868
+ logger.debug("[AudioTrackManager] Disconnecting media stream source");
637
869
  this.mediaStreamSource.disconnect();
638
870
  this.mediaStreamSource = null;
639
871
  }
640
- if (this.audioContext) {
641
- this.audioContext.close();
642
- 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
+ }
643
879
  }
644
880
  if (this.originalAudioTrack) {
645
- 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
+ }
646
888
  this.originalAudioTrack = null;
647
889
  }
648
- if (workletBlobUrl) {
649
- URL.revokeObjectURL(workletBlobUrl);
650
- workletBlobUrl = null;
651
- }
652
890
  this.lastAudioTimestamp = 0;
653
891
  }
654
892
  }
@@ -915,6 +1153,9 @@ class VideoElementManager {
915
1153
  isActive = false;
916
1154
  isIntentionallyPaused = false;
917
1155
  create(stream) {
1156
+ if (this.videoElement) {
1157
+ this.cleanup();
1158
+ }
918
1159
  const videoElement = document.createElement("video");
919
1160
  videoElement.srcObject = stream;
920
1161
  videoElement.autoplay = true;
@@ -964,9 +1205,17 @@ class VideoElementManager {
964
1205
  }
965
1206
  cleanup() {
966
1207
  if (this.videoElement) {
1208
+ if (typeof this.videoElement.pause === "function") {
1209
+ this.videoElement.pause();
1210
+ }
967
1211
  this.videoElement.srcObject = null;
1212
+ if (typeof this.videoElement.remove === "function") {
1213
+ this.videoElement.remove();
1214
+ }
968
1215
  this.videoElement = null;
969
1216
  }
1217
+ this.isActive = false;
1218
+ this.isIntentionallyPaused = false;
970
1219
  }
971
1220
  waitForVideoReady(shouldPlay) {
972
1221
  if (!this.videoElement) {
@@ -974,21 +1223,30 @@ class VideoElementManager {
974
1223
  }
975
1224
  const videoElement = this.videoElement;
976
1225
  return new Promise((resolve, reject) => {
1226
+ let timeoutId;
977
1227
  const cleanup = () => {
978
1228
  videoElement.removeEventListener("loadedmetadata", onLoadedMetadata);
979
1229
  videoElement.removeEventListener("error", onError);
1230
+ if (timeoutId !== undefined) {
1231
+ clearTimeout(timeoutId);
1232
+ timeoutId = undefined;
1233
+ }
980
1234
  };
981
1235
  const onLoadedMetadata = () => {
982
1236
  cleanup();
983
1237
  if (shouldPlay) {
984
- videoElement.play().then(resolve).catch(reject);
1238
+ videoElement.play().then(() => {
1239
+ resolve();
1240
+ }).catch((err) => {
1241
+ reject(err);
1242
+ });
985
1243
  } else {
986
1244
  resolve();
987
1245
  }
988
1246
  };
989
- const onError = () => {
1247
+ const onError = (event) => {
990
1248
  cleanup();
991
- reject(new Error("Failed to load video metadata"));
1249
+ reject(new Error(`Failed to load video metadata: ${event.type}`));
992
1250
  };
993
1251
  if (videoElement.readyState >= READY_STATE_HAVE_CURRENT_DATA2) {
994
1252
  if (shouldPlay) {
@@ -998,14 +1256,16 @@ class VideoElementManager {
998
1256
  }
999
1257
  return;
1000
1258
  }
1001
- videoElement.addEventListener("loadedmetadata", onLoadedMetadata);
1002
- videoElement.addEventListener("error", onError);
1003
- if (!shouldPlay) {
1259
+ videoElement.addEventListener("loadedmetadata", onLoadedMetadata, {
1260
+ once: true
1261
+ });
1262
+ videoElement.addEventListener("error", onError, { once: true });
1263
+ if (shouldPlay) {
1004
1264
  videoElement.play().catch(reject);
1005
1265
  }
1006
- setTimeout(() => {
1266
+ timeoutId = window.setTimeout(() => {
1007
1267
  cleanup();
1008
- reject(new Error("Timeout waiting for video to load"));
1268
+ reject(new Error(`Timeout waiting for video to load. ReadyState: ${videoElement.readyState}, SrcObject: ${!!videoElement.srcObject}`));
1009
1269
  }, VIDEO_LOAD_TIMEOUT);
1010
1270
  });
1011
1271
  }
@@ -1065,9 +1325,20 @@ class StreamProcessor {
1065
1325
  latencyMode: REALTIME_LATENCY
1066
1326
  });
1067
1327
  output.addVideoTrack(this.canvasSource);
1328
+ logger.debug("[StreamProcessor] Setting up audio track", {
1329
+ hasStream: !!stream,
1330
+ audioTracks: stream.getAudioTracks().length
1331
+ });
1068
1332
  const audioSource = await this.audioTrackManager.setupAudioTrack(stream, config);
1333
+ logger.debug("[StreamProcessor] Audio track setup result:", {
1334
+ hasAudioSource: !!audioSource,
1335
+ audioSourceType: audioSource?.constructor?.name
1336
+ });
1069
1337
  if (audioSource) {
1338
+ logger.debug("[StreamProcessor] Adding audio track to output");
1070
1339
  output.addAudioTrack(audioSource);
1340
+ } else {
1341
+ logger.warn("[StreamProcessor] No audio source, skipping audio track");
1071
1342
  }
1072
1343
  await output.start();
1073
1344
  if (!this.canvasRenderer) {
@@ -1099,9 +1370,12 @@ class StreamProcessor {
1099
1370
  return this.frameCapturer.isPausedState();
1100
1371
  }
1101
1372
  async finalize() {
1373
+ logger.debug("[StreamProcessor] finalize called");
1102
1374
  this.frameCapturer.stop();
1103
1375
  await this.frameCapturer.waitForPendingFrames();
1376
+ logger.debug("[StreamProcessor] Cleaning up video element manager");
1104
1377
  this.videoElementManager.cleanup();
1378
+ logger.debug("[StreamProcessor] Cleaning up audio track manager");
1105
1379
  this.audioTrackManager.cleanup();
1106
1380
  if (this.canvasSource) {
1107
1381
  this.canvasSource.close();
@@ -1256,38 +1530,70 @@ class RecordingManager {
1256
1530
  this.callbacks.onCountdownUpdate(this.recordingState, this.countdownRemaining);
1257
1531
  }, COUNTDOWN_UPDATE_INTERVAL);
1258
1532
  this.countdownTimeoutId = window.setTimeout(async () => {
1259
- await this.doStartRecording();
1533
+ await this.doStartRecording().catch(() => {});
1260
1534
  }, this.countdownDuration);
1261
1535
  }
1262
1536
  async doStartRecording() {
1263
- try {
1264
- this.cancelCountdown();
1265
- 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;
1266
1552
  this.callbacks.onStateChange(this.recordingState);
1267
- this.resetRecordingState();
1268
- const currentStream = this.streamManager.getStream();
1269
- if (!currentStream) {
1270
- throw new Error("No stream available for recording");
1271
- }
1272
- this.originalCameraStream = currentStream;
1273
- this.streamProcessor = new StreamProcessor;
1274
- const config = await this.callbacks.onGetConfig();
1275
- await this.streamManager.startRecording(this.streamProcessor, config);
1276
- this.startRecordingTimer();
1277
- if (this.maxRecordingTime && this.maxRecordingTime > 0) {
1278
- this.maxTimeTimer = window.setTimeout(async () => {
1279
- if (this.recordingState === RECORDING_STATE_RECORDING) {
1280
- await this.stopRecording();
1281
- }
1282
- }, this.maxRecordingTime);
1283
- }
1284
- } catch (error) {
1285
- this.handleError(error);
1553
+ return;
1554
+ }
1555
+ this.originalCameraStream = currentStream;
1556
+ logger.debug("[RecordingManager] Creating new StreamProcessor");
1557
+ this.streamProcessor = new StreamProcessor;
1558
+ logger.debug("[RecordingManager] StreamProcessor created:", !!this.streamProcessor);
1559
+ const configResult = await this.callbacks.onGetConfig().then((config) => ({ config, error: null })).catch((error) => ({ config: null, error }));
1560
+ if (configResult.error) {
1561
+ this.handleError(configResult.error);
1286
1562
  this.recordingState = RECORDING_STATE_IDLE;
1287
1563
  this.callbacks.onStateChange(this.recordingState);
1564
+ return;
1565
+ }
1566
+ if (!configResult.config) {
1567
+ this.handleError(new Error("Failed to get recording config"));
1568
+ this.recordingState = RECORDING_STATE_IDLE;
1569
+ this.callbacks.onStateChange(this.recordingState);
1570
+ return;
1571
+ }
1572
+ logger.debug("[RecordingManager] Starting recording with stream manager");
1573
+ const recordingError = await this.streamManager.startRecording(this.streamProcessor, configResult.config).then(() => {
1574
+ logger.info("[RecordingManager] Recording started successfully");
1575
+ return null;
1576
+ }).catch((error) => {
1577
+ logger.error("[RecordingManager] Error starting recording:", error);
1578
+ return error;
1579
+ });
1580
+ if (recordingError) {
1581
+ this.handleError(recordingError);
1582
+ this.recordingState = RECORDING_STATE_IDLE;
1583
+ this.callbacks.onStateChange(this.recordingState);
1584
+ return;
1585
+ }
1586
+ this.startRecordingTimer();
1587
+ if (this.maxRecordingTime && this.maxRecordingTime > 0) {
1588
+ this.maxTimeTimer = window.setTimeout(async () => {
1589
+ if (this.recordingState === RECORDING_STATE_RECORDING) {
1590
+ await this.stopRecording();
1591
+ }
1592
+ }, this.maxRecordingTime);
1288
1593
  }
1289
1594
  }
1290
1595
  async stopRecording() {
1596
+ logger.debug("[RecordingManager] stopRecording called");
1291
1597
  try {
1292
1598
  this.cancelCountdown();
1293
1599
  this.clearTimer(this.recordingIntervalId, clearInterval);
@@ -1296,7 +1602,9 @@ class RecordingManager {
1296
1602
  this.maxTimeTimer = null;
1297
1603
  this.resetPauseState();
1298
1604
  this.callbacks.onStopAudioTracking();
1605
+ logger.debug("[RecordingManager] Stopping recording in stream manager");
1299
1606
  const blob = await this.streamManager.stopRecording();
1607
+ logger.info("[RecordingManager] Recording stopped, blob size:", blob.size);
1300
1608
  this.recordingState = RECORDING_STATE_IDLE;
1301
1609
  this.callbacks.onStateChange(this.recordingState);
1302
1610
  this.recordingSeconds = 0;
@@ -2629,14 +2937,23 @@ class CameraStreamManager {
2629
2937
  this.mediaRecorder.stop();
2630
2938
  }
2631
2939
  async startRecording(processor, config) {
2940
+ logger.debug("[CameraStreamManager] startRecording called", {
2941
+ hasMediaStream: !!this.mediaStream,
2942
+ isRecording: this.isRecording(),
2943
+ hasProcessor: !!processor,
2944
+ audioTracks: this.mediaStream?.getAudioTracks().length || 0
2945
+ });
2632
2946
  if (!this.mediaStream) {
2633
2947
  throw new Error("Stream must be started before recording");
2634
2948
  }
2635
2949
  if (this.isRecording()) {
2950
+ logger.debug("[CameraStreamManager] Already recording, returning");
2636
2951
  return;
2637
2952
  }
2638
2953
  this.streamProcessor = processor;
2954
+ logger.debug("[CameraStreamManager] StreamProcessor assigned, starting processing");
2639
2955
  await processor.startProcessing(this.mediaStream, config);
2956
+ logger.info("[CameraStreamManager] Processing started");
2640
2957
  this.bufferSizeUpdateInterval = window.setInterval(() => {
2641
2958
  if (!this.streamProcessor) {
2642
2959
  return;
@@ -2657,6 +2974,10 @@ class CameraStreamManager {
2657
2974
  this.startRecordingTimer();
2658
2975
  }
2659
2976
  async stopRecording() {
2977
+ logger.debug("[CameraStreamManager] stopRecording called", {
2978
+ hasStreamProcessor: !!this.streamProcessor,
2979
+ isRecording: this.isRecording()
2980
+ });
2660
2981
  if (!(this.streamProcessor && this.isRecording())) {
2661
2982
  throw new Error("Not currently recording");
2662
2983
  }
@@ -2664,13 +2985,19 @@ class CameraStreamManager {
2664
2985
  this.clearRecordingTimer();
2665
2986
  this.clearBufferSizeInterval();
2666
2987
  this.resetPauseState();
2988
+ logger.debug("[CameraStreamManager] Finalizing stream processor");
2667
2989
  const result = await this.streamProcessor.finalize();
2990
+ logger.info("[CameraStreamManager] Stream processor finalized", {
2991
+ blobSize: result.blob.size,
2992
+ hasBlob: !!result.blob
2993
+ });
2668
2994
  this.setState("active");
2669
2995
  this.emit("recordingstop", {
2670
2996
  blob: result.blob,
2671
2997
  mimeType: "video/mp4"
2672
2998
  });
2673
2999
  this.streamProcessor = null;
3000
+ logger.debug("[CameraStreamManager] StreamProcessor cleared");
2674
3001
  return result.blob;
2675
3002
  }
2676
3003
  pauseRecording() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vidtreo/recorder",
3
- "version": "0.8.1",
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",