@vidtreo/recorder 1.6.3 → 1.7.1

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.
Files changed (3) hide show
  1. package/dist/index.d.ts +1963 -1946
  2. package/dist/index.js +237 -23
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1476,6 +1476,7 @@ async function buildDeviceCapabilityProfile(options = {}) {
1476
1476
  // src/core/routing/recording-route-decision.ts
1477
1477
  var DEFAULT_MIN_DEVICE_MEMORY_GB = 4;
1478
1478
  var DEFAULT_MIN_HARDWARE_CONCURRENCY = 5;
1479
+ var PLATFORM_KEYWORD_ANDROID2 = "android";
1479
1480
  var UNSUPPORTED_GUIDANCE_MESSAGE = "Recording is not supported in this browser context. Use a secure connection, go online, and try a current Chrome or Edge browser.";
1480
1481
  function hasSufficientDeviceMemory(profile, minDeviceMemory) {
1481
1482
  const deviceMemory = profile.resources.deviceMemory;
@@ -1530,6 +1531,17 @@ function buildFallbackRouteBlockers(profile) {
1530
1531
  }
1531
1532
  return reasonCodes;
1532
1533
  }
1534
+ function isAndroidProfile(profile) {
1535
+ return profile.os.name.toLowerCase().includes(PLATFORM_KEYWORD_ANDROID2);
1536
+ }
1537
+ function buildPostRecordingTranscodeReasonCodes(profile, localRouteBlockers) {
1538
+ const reasonCodes = [...localRouteBlockers];
1539
+ if (isAndroidProfile(profile)) {
1540
+ reasonCodes.push("android-post-recording-transcode");
1541
+ }
1542
+ reasonCodes.push("mediarecorder-supported");
1543
+ return reasonCodes;
1544
+ }
1533
1545
  function hasSupportedEncodingCapability(selectedCodec) {
1534
1546
  const encodingCapability = selectedCodec?.encodingCapability;
1535
1547
  return encodingCapability === undefined || encodingCapability.errorCode !== undefined || encodingCapability.supported === true;
@@ -1554,6 +1566,13 @@ function decideRecordingRoute(options) {
1554
1566
  const hasSmoothEncoding = hasSmoothEncodingCapability(selectedCodec);
1555
1567
  const hasPowerEfficientEncoding = hasPowerEfficientEncodingCapability(selectedCodec);
1556
1568
  const localRouteBlockers = buildLocalRouteBlockers(options.profile, hasSupportedTargetCodec, hasEnoughDeviceMemory, hasEnoughHardwareConcurrency, hasSupportedEncoding, hasSmoothEncoding, hasPowerEfficientEncoding);
1569
+ if (isAndroidProfile(options.profile) && canUsePostRecordingTranscodeRoute(options.profile, hasSupportedTargetCodec)) {
1570
+ return {
1571
+ route: "safe-capture-post-recording-transcode",
1572
+ reasonCodes: buildPostRecordingTranscodeReasonCodes(options.profile, localRouteBlockers),
1573
+ selectedCodecId
1574
+ };
1575
+ }
1557
1576
  if (options.profile.features.isSecureContext && options.profile.features.hasVideoEncoder && hasSupportedTargetCodec && options.profile.network.online !== false && hasEnoughDeviceMemory && hasEnoughHardwareConcurrency && hasSupportedEncoding && hasSmoothEncoding && hasPowerEfficientEncoding) {
1558
1577
  return {
1559
1578
  route: "local-webcodecs",
@@ -1564,7 +1583,7 @@ function decideRecordingRoute(options) {
1564
1583
  if (canUsePostRecordingTranscodeRoute(options.profile, hasSupportedTargetCodec)) {
1565
1584
  return {
1566
1585
  route: "safe-capture-post-recording-transcode",
1567
- reasonCodes: [...localRouteBlockers, "mediarecorder-supported"],
1586
+ reasonCodes: buildPostRecordingTranscodeReasonCodes(options.profile, localRouteBlockers),
1568
1587
  selectedCodecId
1569
1588
  };
1570
1589
  }
@@ -1758,6 +1777,12 @@ async function resolveAudioCodecFromPolicy(options) {
1758
1777
  if (options.bitrate !== undefined) {
1759
1778
  audioCodecOptions.bitrate = options.bitrate;
1760
1779
  }
1780
+ if (options.numberOfChannels !== undefined) {
1781
+ audioCodecOptions.numberOfChannels = options.numberOfChannels;
1782
+ }
1783
+ if (options.sampleRate !== undefined) {
1784
+ audioCodecOptions.sampleRate = options.sampleRate;
1785
+ }
1761
1786
  const probeResults = await Promise.allSettled([
1762
1787
  getFirstEncodableAudioCodec(candidates, audioCodecOptions)
1763
1788
  ]);
@@ -5246,6 +5271,16 @@ var LOW_RESOURCE_SAFE_CAPTURE_CONSTRAINTS = {
5246
5271
  height: { ideal: 480, max: 480 },
5247
5272
  frameRate: { ideal: 20, max: 20 }
5248
5273
  };
5274
+ var ANDROID_SAFE_CAPTURE_CONSTRAINTS = {
5275
+ width: { ideal: 1280, max: 1280 },
5276
+ height: { ideal: 720, max: 720 },
5277
+ frameRate: { ideal: 24, max: 24 }
5278
+ };
5279
+ var SAFE_CAPTURE_TIMESLICE_MS = 1000;
5280
+ var MEDIA_RECORDER_ERROR_FINALIZE_TIMEOUT_MS = 5000;
5281
+ var ANDROID_POST_RECORDING_TRANSCODE_REASON = "android-post-recording-transcode";
5282
+ var MEDIA_RECORDER_ERROR_CODE = "recording.mediarecorder-error";
5283
+ var MEDIA_RECORDER_EMPTY_ERROR_CODE = "recording.mediarecorder-empty";
5249
5284
  function createUnsupportedRouteError(routeDecision) {
5250
5285
  const guidance = routeDecision.guidance?.message ?? "This browser or device cannot safely record video. Try a supported browser or upload an existing video file.";
5251
5286
  const error = new Error(guidance);
@@ -5277,6 +5312,8 @@ class StreamRecordingState {
5277
5312
  mediaRecorderStopPromise = null;
5278
5313
  mediaRecorderStopResolve = null;
5279
5314
  mediaRecorderStopReject = null;
5315
+ mediaRecorderError = null;
5316
+ mediaRecorderErrorTimeoutId = null;
5280
5317
  streamManager;
5281
5318
  dependencies;
5282
5319
  constructor(streamManager, dependencies) {
@@ -5449,9 +5486,11 @@ class StreamRecordingState {
5449
5486
  }
5450
5487
  let result;
5451
5488
  if (this.activeRouteDecision.route === "safe-capture-post-recording-transcode") {
5489
+ const safeCaptureResult = await this.stopSafeCaptureRecording();
5452
5490
  logger.debug("[StreamRecordingState] Finalizing MediaRecorder capture");
5453
5491
  result = {
5454
- blob: await this.stopSafeCaptureRecording()
5492
+ blob: safeCaptureResult.blob,
5493
+ mediaRecorderDiagnostics: safeCaptureResult.diagnostics
5455
5494
  };
5456
5495
  } else {
5457
5496
  logger.debug("[StreamRecordingState] Finalizing stream processor");
@@ -5470,7 +5509,8 @@ class StreamRecordingState {
5470
5509
  blob: result.blob,
5471
5510
  tabVisibilityIntervals,
5472
5511
  recordingStats: result.recordingStats,
5473
- encoderAcceleration: result.encoderAcceleration
5512
+ encoderAcceleration: result.encoderAcceleration,
5513
+ mediaRecorderDiagnostics: result.mediaRecorderDiagnostics
5474
5514
  };
5475
5515
  }
5476
5516
  pauseRecording() {
@@ -5481,6 +5521,9 @@ class StreamRecordingState {
5481
5521
  if (this.tabVisibilityTracker) {
5482
5522
  this.tabVisibilityTracker.pause();
5483
5523
  }
5524
+ if (this.mediaRecorder?.state === "recording") {
5525
+ this.mediaRecorder.pause?.();
5526
+ }
5484
5527
  if (this.streamProcessor && this.isRecording()) {
5485
5528
  this.streamProcessor.pause();
5486
5529
  }
@@ -5494,6 +5537,9 @@ class StreamRecordingState {
5494
5537
  if (this.tabVisibilityTracker) {
5495
5538
  this.tabVisibilityTracker.resume();
5496
5539
  }
5540
+ if (this.mediaRecorder?.state === "paused") {
5541
+ this.mediaRecorder.resume?.();
5542
+ }
5497
5543
  this.startRecordingTimer();
5498
5544
  if (this.streamProcessor && this.isRecording()) {
5499
5545
  this.streamProcessor.resume();
@@ -5593,6 +5639,7 @@ class StreamRecordingState {
5593
5639
  }
5594
5640
  await this.applySafeCaptureTrackConstraints(mediaStream);
5595
5641
  this.mediaRecorderChunks = [];
5642
+ this.mediaRecorderError = null;
5596
5643
  const preferredMimeType = this.resolvePreferredSafeCaptureMimeType(config);
5597
5644
  const recorderOptions = preferredMimeType === undefined ? undefined : { mimeType: preferredMimeType };
5598
5645
  const recorder = new MediaRecorder(mediaStream, recorderOptions);
@@ -5601,20 +5648,34 @@ class StreamRecordingState {
5601
5648
  this.mediaRecorderStopResolve = resolve;
5602
5649
  this.mediaRecorderStopReject = reject;
5603
5650
  });
5651
+ this.mediaRecorderStopPromise.catch(() => {
5652
+ return;
5653
+ });
5604
5654
  recorder.ondataavailable = (event) => {
5605
5655
  if (event.data.size > 0) {
5606
5656
  this.mediaRecorderChunks.push(event.data);
5607
5657
  }
5608
5658
  };
5609
5659
  recorder.onstop = () => {
5660
+ this.clearMediaRecorderErrorTimeout();
5661
+ if (this.mediaRecorderError !== null) {
5662
+ this.updateMediaRecorderErrorDiagnostics(recorder);
5663
+ this.mediaRecorderStopReject?.(this.mediaRecorderError);
5664
+ return;
5665
+ }
5610
5666
  const mimeType = recorder.mimeType || this.resolveSafeCaptureMimeType();
5611
5667
  const blob = new Blob(this.mediaRecorderChunks, { type: mimeType });
5612
- this.mediaRecorderStopResolve?.(blob);
5668
+ const diagnostics = this.buildMediaRecorderDiagnostics(recorder);
5669
+ if (blob.size > 0) {
5670
+ this.mediaRecorderStopResolve?.({ blob, diagnostics });
5671
+ return;
5672
+ }
5673
+ this.mediaRecorderStopReject?.(this.createEmptyMediaRecorderError(recorder));
5613
5674
  };
5614
- recorder.onerror = () => {
5615
- this.mediaRecorderStopReject?.(new Error("Browser recording failed before upload could start"));
5675
+ recorder.onerror = (event) => {
5676
+ this.mediaRecorderError = this.createMediaRecorderError(event, recorder);
5616
5677
  };
5617
- recorder.start();
5678
+ recorder.start(SAFE_CAPTURE_TIMESLICE_MS);
5618
5679
  }
5619
5680
  async applySafeCaptureTrackConstraints(mediaStream) {
5620
5681
  const constraints = this.resolveSafeCaptureVideoConstraints();
@@ -5635,6 +5696,9 @@ class StreamRecordingState {
5635
5696
  if (reasonCodes.includes("insufficient-device-memory") || reasonCodes.includes("insufficient-hardware-concurrency") || reasonCodes.includes("encoding-capability-unsupported") || reasonCodes.includes("encoding-not-smooth") || reasonCodes.includes("encoding-not-power-efficient")) {
5636
5697
  return LOW_RESOURCE_SAFE_CAPTURE_CONSTRAINTS;
5637
5698
  }
5699
+ if (reasonCodes.includes(ANDROID_POST_RECORDING_TRANSCODE_REASON)) {
5700
+ return ANDROID_SAFE_CAPTURE_CONSTRAINTS;
5701
+ }
5638
5702
  return null;
5639
5703
  }
5640
5704
  async stopSafeCaptureRecording() {
@@ -5646,6 +5710,7 @@ class StreamRecordingState {
5646
5710
  if (recorder.state !== "inactive") {
5647
5711
  recorder.stop();
5648
5712
  }
5713
+ this.scheduleMediaRecorderErrorFinalizeTimeout(recorder);
5649
5714
  try {
5650
5715
  return await stopPromise;
5651
5716
  } finally {
@@ -5654,6 +5719,8 @@ class StreamRecordingState {
5654
5719
  this.mediaRecorderStopPromise = null;
5655
5720
  this.mediaRecorderStopResolve = null;
5656
5721
  this.mediaRecorderStopReject = null;
5722
+ this.mediaRecorderError = null;
5723
+ this.clearMediaRecorderErrorTimeout();
5657
5724
  this.activeRouteDecision = DEFAULT_ROUTE_DECISION;
5658
5725
  }
5659
5726
  }
@@ -5669,17 +5736,110 @@ class StreamRecordingState {
5669
5736
  }
5670
5737
  return MediaRecorder.isTypeSupported(mimeType);
5671
5738
  };
5672
- const candidateMimeTypes = config.format === "mp4" ? [
5673
- "video/mp4;codecs=avc1.42E01E,mp4a.40.2",
5674
- "video/mp4;codecs=avc1.42001f,mp4a.40.2",
5675
- "video/mp4"
5676
- ] : [
5739
+ const webmCandidateMimeTypes = [
5677
5740
  "video/webm;codecs=vp8,opus",
5678
5741
  "video/webm;codecs=vp9,opus",
5679
5742
  "video/webm"
5680
5743
  ];
5744
+ const mp4CandidateMimeTypes = [
5745
+ "video/mp4;codecs=avc1.42E01E,mp4a.40.2",
5746
+ "video/mp4;codecs=avc1.42001f,mp4a.40.2",
5747
+ "video/mp4"
5748
+ ];
5749
+ let candidateMimeTypes = mp4CandidateMimeTypes;
5750
+ if (this.shouldPreferWebmSafeCapture(config)) {
5751
+ candidateMimeTypes = [
5752
+ ...webmCandidateMimeTypes,
5753
+ ...mp4CandidateMimeTypes
5754
+ ];
5755
+ } else if (config.format !== "mp4") {
5756
+ candidateMimeTypes = webmCandidateMimeTypes;
5757
+ }
5681
5758
  return candidateMimeTypes.find(canUseMimeType);
5682
5759
  }
5760
+ shouldPreferWebmSafeCapture(config) {
5761
+ if (config.format !== "mp4") {
5762
+ return false;
5763
+ }
5764
+ return this.activeRouteDecision.reasonCodes.includes(ANDROID_POST_RECORDING_TRANSCODE_REASON);
5765
+ }
5766
+ scheduleMediaRecorderErrorFinalizeTimeout(recorder) {
5767
+ if (this.mediaRecorderErrorTimeoutId !== null) {
5768
+ return;
5769
+ }
5770
+ this.mediaRecorderErrorTimeoutId = window.setTimeout(() => {
5771
+ if (this.mediaRecorderError !== null) {
5772
+ this.mediaRecorderStopReject?.(this.mediaRecorderError);
5773
+ return;
5774
+ }
5775
+ this.mediaRecorderStopReject?.(this.createEmptyMediaRecorderError(recorder));
5776
+ }, MEDIA_RECORDER_ERROR_FINALIZE_TIMEOUT_MS);
5777
+ }
5778
+ clearMediaRecorderErrorTimeout() {
5779
+ if (this.mediaRecorderErrorTimeoutId === null) {
5780
+ return;
5781
+ }
5782
+ window.clearTimeout(this.mediaRecorderErrorTimeoutId);
5783
+ this.mediaRecorderErrorTimeoutId = null;
5784
+ }
5785
+ createMediaRecorderError(event, recorder) {
5786
+ const mediaRecorderEvent = event;
5787
+ const browserError = mediaRecorderEvent.error;
5788
+ const error = new Error("Browser recording failed before upload could start");
5789
+ error.name = "MediaRecorderRecordingError";
5790
+ error.code = MEDIA_RECORDER_ERROR_CODE;
5791
+ error.mediaRecorderMimeType = recorder.mimeType;
5792
+ error.mediaRecorderChunkCount = this.mediaRecorderChunks.length;
5793
+ error.mediaRecorderChunkSize = this.getMediaRecorderChunkSize();
5794
+ if (browserError?.name) {
5795
+ error.mediaRecorderErrorName = browserError.name;
5796
+ }
5797
+ if (browserError?.message) {
5798
+ error.mediaRecorderErrorMessage = browserError.message;
5799
+ }
5800
+ return error;
5801
+ }
5802
+ createEmptyMediaRecorderError(recorder) {
5803
+ const error = new Error("Browser recording produced no data before upload could start");
5804
+ error.name = "MediaRecorderEmptyDataError";
5805
+ error.code = MEDIA_RECORDER_EMPTY_ERROR_CODE;
5806
+ error.mediaRecorderMimeType = recorder.mimeType;
5807
+ error.mediaRecorderChunkCount = this.mediaRecorderChunks.length;
5808
+ error.mediaRecorderChunkSize = this.getMediaRecorderChunkSize();
5809
+ if (this.mediaRecorderError?.mediaRecorderErrorName) {
5810
+ error.mediaRecorderErrorName = this.mediaRecorderError.mediaRecorderErrorName;
5811
+ }
5812
+ if (this.mediaRecorderError?.mediaRecorderErrorMessage) {
5813
+ error.mediaRecorderErrorMessage = this.mediaRecorderError.mediaRecorderErrorMessage;
5814
+ }
5815
+ return error;
5816
+ }
5817
+ updateMediaRecorderErrorDiagnostics(recorder) {
5818
+ if (this.mediaRecorderError === null) {
5819
+ return;
5820
+ }
5821
+ this.mediaRecorderError.mediaRecorderMimeType = recorder.mimeType;
5822
+ this.mediaRecorderError.mediaRecorderChunkCount = this.mediaRecorderChunks.length;
5823
+ this.mediaRecorderError.mediaRecorderChunkSize = this.getMediaRecorderChunkSize();
5824
+ }
5825
+ buildMediaRecorderDiagnostics(recorder) {
5826
+ const diagnostics = {
5827
+ mediaRecorderMimeType: recorder.mimeType,
5828
+ mediaRecorderChunkCount: this.mediaRecorderChunks.length,
5829
+ mediaRecorderChunkSize: this.getMediaRecorderChunkSize(),
5830
+ mediaRecorderHadError: this.mediaRecorderError !== null
5831
+ };
5832
+ if (this.mediaRecorderError?.mediaRecorderErrorName) {
5833
+ diagnostics.mediaRecorderErrorName = this.mediaRecorderError.mediaRecorderErrorName;
5834
+ }
5835
+ if (this.mediaRecorderError?.mediaRecorderErrorMessage) {
5836
+ diagnostics.mediaRecorderErrorMessage = this.mediaRecorderError.mediaRecorderErrorMessage;
5837
+ }
5838
+ return diagnostics;
5839
+ }
5840
+ getMediaRecorderChunkSize() {
5841
+ return this.mediaRecorderChunks.reduce((total, chunk) => total + chunk.size, 0);
5842
+ }
5683
5843
  resetPauseState() {
5684
5844
  this.totalPausedTime = 0;
5685
5845
  this.pauseStartTime = null;
@@ -5759,6 +5919,8 @@ class StreamRecordingState {
5759
5919
  this.mediaRecorderStopPromise = null;
5760
5920
  this.mediaRecorderStopResolve = null;
5761
5921
  this.mediaRecorderStopReject = null;
5922
+ this.mediaRecorderError = null;
5923
+ this.clearMediaRecorderErrorTimeout();
5762
5924
  this.activeRouteDecision = DEFAULT_ROUTE_DECISION;
5763
5925
  if (this.streamProcessor) {
5764
5926
  this.streamProcessor.destroy();
@@ -5906,6 +6068,8 @@ var RECORDING_CODEC_UNSUPPORTED_CODES = new Set([
5906
6068
  ]);
5907
6069
  var RECORDING_SERVER_TRANSCODE_FALLBACK_CODES = new Set([
5908
6070
  "recording.post-recording-transcode-fallback",
6071
+ "recording.mediarecorder-empty",
6072
+ "recording.mediarecorder-error",
5909
6073
  "transcode.post-recording-fallback",
5910
6074
  "route.post-recording-transcode-fallback"
5911
6075
  ]);
@@ -6141,7 +6305,7 @@ function resolveDeviceError(input) {
6141
6305
  // package.json
6142
6306
  var package_default = {
6143
6307
  name: "@vidtreo/recorder",
6144
- version: "1.6.3",
6308
+ version: "1.7.1",
6145
6309
  type: "module",
6146
6310
  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.",
6147
6311
  main: "./dist/index.js",
@@ -6226,6 +6390,13 @@ var MAX_RETRY_ATTEMPTS = 3;
6226
6390
  var MAX_PENDING_EVENTS = 100;
6227
6391
  var BRACKET_ERROR_CODE_PATTERN = /\[([a-z]+(?:[.-][a-z0-9]+)+)\]/i;
6228
6392
  var INLINE_ERROR_CODE_PATTERN = /\b([a-z]+(?:[.-][a-z0-9]+)+)\b/i;
6393
+ var MEDIA_RECORDER_ERROR_PROPERTY_KEYS = [
6394
+ "mediaRecorderErrorName",
6395
+ "mediaRecorderErrorMessage",
6396
+ "mediaRecorderMimeType",
6397
+ "mediaRecorderChunkCount",
6398
+ "mediaRecorderChunkSize"
6399
+ ];
6229
6400
  function resolveInstallationId(dependencies) {
6230
6401
  const storageProvider = dependencies.storageProvider;
6231
6402
  const stored = storageProvider?.getItem(TELEMETRY_STORAGE_KEY);
@@ -6567,6 +6738,23 @@ class TelemetryClient {
6567
6738
  recordingPipelineErrorCode: recordingPipelineCode
6568
6739
  };
6569
6740
  }
6741
+ return {
6742
+ ...properties,
6743
+ ...this.buildMediaRecorderErrorProperties(error)
6744
+ };
6745
+ }
6746
+ buildMediaRecorderErrorProperties(error) {
6747
+ if (!(error && typeof error === "object")) {
6748
+ return {};
6749
+ }
6750
+ const candidate = error;
6751
+ let properties = {};
6752
+ for (const key of MEDIA_RECORDER_ERROR_PROPERTY_KEYS) {
6753
+ const value = candidate[key];
6754
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
6755
+ properties = { ...properties, [key]: value };
6756
+ }
6757
+ }
6570
6758
  return properties;
6571
6759
  }
6572
6760
  buildFingerprint() {
@@ -20980,6 +21168,8 @@ var CODEC_CACHE_AUDIO_OVERRIDE = "audioOverride";
20980
21168
  var CODEC_CACHE_VIDEO_OVERRIDE = "videoOverride";
20981
21169
  var CODEC_CACHE_AUDIO_BITRATE = "audioBitrate";
20982
21170
  var CODEC_CACHE_VIDEO_BITRATE = "videoBitrate";
21171
+ var CODEC_CACHE_AUDIO_CHANNELS = "audioChannels";
21172
+ var CODEC_CACHE_AUDIO_SAMPLE_RATE = "audioSampleRate";
20983
21173
  var CODEC_CACHE_WIDTH = "width";
20984
21174
  var CODEC_CACHE_HEIGHT = "height";
20985
21175
  var CODEC_CACHE_POLICY_PREFERRED_AUDIO = "policyPreferredAudio";
@@ -21241,7 +21431,6 @@ class WorkerProcessor {
21241
21431
  isLinuxPlatform: this.isLinuxPlatformFn()
21242
21432
  });
21243
21433
  const audioBitrate = this.resolveAudioBitrate(config, format);
21244
- const audioCodec = await this.resolveAudioCodecWithCache(config, format, policy, audioBitrate);
21245
21434
  const codec = await this.resolveVideoCodecWithCache(config, format, policy);
21246
21435
  const isScreenCapture = isScreenCaptureStream(stream);
21247
21436
  logger.debug("[WorkerProcessor] Starting processing", {
@@ -21250,7 +21439,6 @@ class WorkerProcessor {
21250
21439
  codec,
21251
21440
  bitrate: config.bitrate
21252
21441
  });
21253
- const workerConfig = this.buildWorkerTranscodeConfig(config, audioCodec, audioBitrate, codec, format);
21254
21442
  if (typeof config.fps === "number" && config.fps > 0) {
21255
21443
  this.lastConfigFps = config.fps;
21256
21444
  }
@@ -21286,6 +21474,8 @@ class WorkerProcessor {
21286
21474
  };
21287
21475
  }
21288
21476
  const { audioConfig, audioStream, shouldStartAudioWorklet } = await this.prepareAudioPipeline(audioTrack, workerProbeResult);
21477
+ const audioCodec = await this.resolveAudioCodecWithCache(config, format, policy, audioBitrate, audioConfig);
21478
+ const workerConfig = this.buildWorkerTranscodeConfig(config, audioCodec, audioBitrate, codec, format);
21289
21479
  const overlayConfigToSend = this.buildOverlayConfigToSend();
21290
21480
  const message = createStartMessage({
21291
21481
  videoTrack: videoInput.videoTrack,
@@ -21545,12 +21735,14 @@ class WorkerProcessor {
21545
21735
  }
21546
21736
  return getPresetAudioBitrateForFormat(format);
21547
21737
  }
21548
- async resolveAudioCodec(config, format, policy, audioBitrate) {
21738
+ async resolveAudioCodec(config, format, policy, audioBitrate, audioConfig) {
21549
21739
  return await resolveAudioCodecFromPolicy({
21550
21740
  format,
21551
21741
  overrideCodec: config.audioCodec,
21552
21742
  policy,
21553
- bitrate: audioBitrate
21743
+ bitrate: audioBitrate,
21744
+ numberOfChannels: audioConfig?.numberOfChannels,
21745
+ sampleRate: audioConfig?.sampleRate
21554
21746
  });
21555
21747
  }
21556
21748
  async resolveVideoCodec(config, format, policy) {
@@ -21563,13 +21755,13 @@ class WorkerProcessor {
21563
21755
  bitrate: config.bitrate
21564
21756
  });
21565
21757
  }
21566
- async resolveAudioCodecWithCache(config, format, policy, audioBitrate) {
21567
- const audioCodecCacheKey = this.buildAudioCodecCacheKey(config, format, policy, audioBitrate);
21758
+ async resolveAudioCodecWithCache(config, format, policy, audioBitrate, audioConfig) {
21759
+ const audioCodecCacheKey = this.buildAudioCodecCacheKey(config, format, policy, audioBitrate, audioConfig);
21568
21760
  const cachedAudioCodec = resolvedAudioCodecCache.get(audioCodecCacheKey);
21569
21761
  if (cachedAudioCodec) {
21570
21762
  return cachedAudioCodec;
21571
21763
  }
21572
- const resolvedAudioCodec = await this.resolveAudioCodec(config, format, policy, audioBitrate);
21764
+ const resolvedAudioCodec = await this.resolveAudioCodec(config, format, policy, audioBitrate, audioConfig);
21573
21765
  setCodecCacheValue(resolvedAudioCodecCache, audioCodecCacheKey, resolvedAudioCodec);
21574
21766
  return resolvedAudioCodec;
21575
21767
  }
@@ -21583,15 +21775,19 @@ class WorkerProcessor {
21583
21775
  setCodecCacheValue(resolvedVideoCodecCache, videoCodecCacheKey, resolvedVideoCodec);
21584
21776
  return resolvedVideoCodec;
21585
21777
  }
21586
- buildAudioCodecCacheKey(config, format, policy, audioBitrate) {
21778
+ buildAudioCodecCacheKey(config, format, policy, audioBitrate, audioConfig) {
21587
21779
  const audioOverride = formatCacheValue(config.audioCodec);
21588
21780
  const preferredAudioCodec = formatCacheValue(policy.preferredAudioCodec);
21589
21781
  const audioFallbackOrder = formatCacheArray(policy.audioCodecFallbackOrder);
21590
21782
  const audioBitrateValue = formatCacheValue(audioBitrate);
21783
+ const audioChannelsValue = formatCacheValue(audioConfig?.numberOfChannels);
21784
+ const audioSampleRateValue = formatCacheValue(audioConfig?.sampleRate);
21591
21785
  return [
21592
21786
  buildCacheEntry(CODEC_CACHE_FORMAT, formatCacheValue(format)),
21593
21787
  buildCacheEntry(CODEC_CACHE_AUDIO_OVERRIDE, audioOverride),
21594
21788
  buildCacheEntry(CODEC_CACHE_AUDIO_BITRATE, audioBitrateValue),
21789
+ buildCacheEntry(CODEC_CACHE_AUDIO_CHANNELS, audioChannelsValue),
21790
+ buildCacheEntry(CODEC_CACHE_AUDIO_SAMPLE_RATE, audioSampleRateValue),
21595
21791
  buildCacheEntry(CODEC_CACHE_POLICY_PREFERRED_AUDIO, preferredAudioCodec),
21596
21792
  buildCacheEntry(CODEC_CACHE_POLICY_AUDIO_FALLBACK, audioFallbackOrder)
21597
21793
  ].join(CODEC_CACHE_KEY_SEPARATOR);
@@ -22020,7 +22216,7 @@ class WorkerProcessor {
22020
22216
  isLinuxPlatform: this.isLinuxPlatformFn()
22021
22217
  });
22022
22218
  const cacheAudioBitrate = this.resolveAudioBitrate(config, format);
22023
- this.resolveAudioCodecWithCache(config, format, cachePolicy, cacheAudioBitrate).catch(this.handleWarmupCacheError);
22219
+ this.resolveAudioCodecWithCache(config, format, cachePolicy, cacheAudioBitrate, null).catch(this.handleWarmupCacheError);
22024
22220
  this.resolveVideoCodecWithCache(config, format, cachePolicy).catch(this.handleWarmupCacheError);
22025
22221
  const workerConfig = this.buildWorkerTranscodeConfig(config, policy.preferredAudioCodec, audioBitrate, codec, format);
22026
22222
  const message = {
@@ -22637,6 +22833,9 @@ class RecordingManager {
22637
22833
  if (stopResult.encoderAcceleration !== undefined) {
22638
22834
  telemetryProperties.encoderAcceleration = stopResult.encoderAcceleration;
22639
22835
  }
22836
+ if (stopResult.mediaRecorderDiagnostics !== undefined) {
22837
+ Object.assign(telemetryProperties, stopResult.mediaRecorderDiagnostics);
22838
+ }
22640
22839
  return {
22641
22840
  blob: finalBlob,
22642
22841
  telemetryProperties
@@ -22646,6 +22845,10 @@ class RecordingManager {
22646
22845
  this.handleError(normalizedError);
22647
22846
  this.recordingState = RECORDING_STATE_IDLE;
22648
22847
  this.callbacks.onStateChange(this.recordingState);
22848
+ if (this.streamProcessor) {
22849
+ this.streamProcessor.destroy();
22850
+ this.streamProcessor = null;
22851
+ }
22649
22852
  throw normalizedError;
22650
22853
  } finally {
22651
22854
  this.activeRecordingConfig = null;
@@ -23105,6 +23308,7 @@ class RecorderController {
23105
23308
  }
23106
23309
  async stopRecording() {
23107
23310
  const sourceType = this.getCurrentSourceType();
23311
+ let shouldStopStream = true;
23108
23312
  try {
23109
23313
  const stopResult = await this.telemetryManager.executeActionWithResult({
23110
23314
  requestedEvent: "recording.stop.requested",
@@ -23115,7 +23319,6 @@ class RecorderController {
23115
23319
  await this.sourceSwitchManager.handleRecordingStop().catch(() => {
23116
23320
  throw new Error("Source switch cleanup failed");
23117
23321
  });
23118
- this.streamManager.stopStream();
23119
23322
  return recordingStopResult;
23120
23323
  },
23121
23324
  properties: {
@@ -23127,11 +23330,22 @@ class RecorderController {
23127
23330
  });
23128
23331
  return stopResult.blob;
23129
23332
  } catch (error) {
23333
+ shouldStopStream = this.shouldStopStreamAfterStopFailure(error);
23130
23334
  if (isAudioMissingError(error)) {
23131
23335
  this.sendAudioMissingTelemetry(error);
23132
23336
  }
23133
23337
  throw error;
23338
+ } finally {
23339
+ if (shouldStopStream) {
23340
+ this.streamManager.stopStream();
23341
+ }
23342
+ }
23343
+ }
23344
+ shouldStopStreamAfterStopFailure(error) {
23345
+ if (!(error && typeof error === "object" && ("code" in error))) {
23346
+ return true;
23134
23347
  }
23348
+ return error.code !== ERROR_RECORDING_STOP_NOT_READY;
23135
23349
  }
23136
23350
  sendAudioMissingTelemetry(error) {
23137
23351
  const properties = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vidtreo/recorder",
3
- "version": "1.6.3",
3
+ "version": "1.7.1",
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",