@vidtreo/recorder 1.6.3 → 1.7.0

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
@@ -7,6 +7,7 @@ export type StopRecordingResult = {
7
7
  tabVisibilityIntervals: VisibilityInterval[];
8
8
  recordingStats?: StreamRecordingStats;
9
9
  encoderAcceleration?: VideoHardwareAcceleration;
10
+ mediaRecorderDiagnostics?: Record<string, unknown>;
10
11
  };
11
12
  type CheckRecorderSupportDependency = (options: SupportCheckOptions) => Promise<SupportReport>;
12
13
  type GetCurrentTimestampDependency = () => number;
@@ -32,6 +33,8 @@ export declare class StreamRecordingState {
32
33
  private mediaRecorderStopPromise;
33
34
  private mediaRecorderStopResolve;
34
35
  private mediaRecorderStopReject;
36
+ private mediaRecorderError;
37
+ private mediaRecorderErrorTimeoutId;
35
38
  private readonly streamManager;
36
39
  private readonly dependencies;
37
40
  constructor(streamManager: StreamManager, dependencies?: Partial<StreamRecordingStateDependencies>);
@@ -60,6 +63,14 @@ export declare class StreamRecordingState {
60
63
  private stopSafeCaptureRecording;
61
64
  private resolveSafeCaptureMimeType;
62
65
  private resolvePreferredSafeCaptureMimeType;
66
+ private shouldPreferWebmSafeCapture;
67
+ private scheduleMediaRecorderErrorFinalizeTimeout;
68
+ private clearMediaRecorderErrorTimeout;
69
+ private createMediaRecorderError;
70
+ private createEmptyMediaRecorderError;
71
+ private updateMediaRecorderErrorDiagnostics;
72
+ private buildMediaRecorderDiagnostics;
73
+ private getMediaRecorderChunkSize;
63
74
  private resetPauseState;
64
75
  private resolveTabVisibilityOverlayText;
65
76
  private setupVisibilityUpdates;
@@ -1336,6 +1347,7 @@ export declare class TelemetryClient {
1336
1347
  private isSafeCapabilityValue;
1337
1348
  private isRouteSourceType;
1338
1349
  private buildErrorProperties;
1350
+ private buildMediaRecorderErrorProperties;
1339
1351
  private buildFingerprint;
1340
1352
  private buildContext;
1341
1353
  private buildError;
@@ -2453,7 +2465,7 @@ export declare function deserializeBitrate(bitrate: number | string | undefined)
2453
2465
  import type { DeviceCapabilityProfile } from "../capabilities/device-capability-profile";
2454
2466
  export type RecordingCapabilityProfile = DeviceCapabilityProfile;
2455
2467
  export type RecordingRoute = "local-webcodecs" | "safe-capture-post-recording-transcode" | "unsupported-with-guidance";
2456
- export type RecordingRouteReasonCode = "webcodecs-supported" | "webcodecs-unavailable" | "target-codec-supported" | "target-codec-unsupported" | "encoding-capability-unsupported" | "encoding-not-smooth" | "encoding-not-power-efficient" | "insufficient-device-memory" | "insufficient-hardware-concurrency" | "insecure-context" | "mediarecorder-supported" | "mediarecorder-unavailable" | "offline";
2468
+ export type RecordingRouteReasonCode = "webcodecs-supported" | "webcodecs-unavailable" | "target-codec-supported" | "target-codec-unsupported" | "encoding-capability-unsupported" | "encoding-not-smooth" | "encoding-not-power-efficient" | "insufficient-device-memory" | "insufficient-hardware-concurrency" | "insecure-context" | "mediarecorder-supported" | "mediarecorder-unavailable" | "android-post-recording-transcode" | "offline";
2457
2469
  export type RecordingRouteDecision = {
2458
2470
  route: RecordingRoute;
2459
2471
  reasonCodes: RecordingRouteReasonCode[];
@@ -2781,6 +2793,7 @@ export declare class RecorderController {
2781
2793
  switchAudioDevice(deviceId: string | null): Promise<MediaStream>;
2782
2794
  startRecording(): Promise<void>;
2783
2795
  stopRecording(): Promise<Blob>;
2796
+ private shouldStopStreamAfterStopFailure;
2784
2797
  private sendAudioMissingTelemetry;
2785
2798
  getTabVisibilityOverlayConfig(): {
2786
2799
  enabled: boolean;
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
  }
@@ -5246,6 +5265,16 @@ var LOW_RESOURCE_SAFE_CAPTURE_CONSTRAINTS = {
5246
5265
  height: { ideal: 480, max: 480 },
5247
5266
  frameRate: { ideal: 20, max: 20 }
5248
5267
  };
5268
+ var ANDROID_SAFE_CAPTURE_CONSTRAINTS = {
5269
+ width: { ideal: 1280, max: 1280 },
5270
+ height: { ideal: 720, max: 720 },
5271
+ frameRate: { ideal: 24, max: 24 }
5272
+ };
5273
+ var SAFE_CAPTURE_TIMESLICE_MS = 1000;
5274
+ var MEDIA_RECORDER_ERROR_FINALIZE_TIMEOUT_MS = 5000;
5275
+ var ANDROID_POST_RECORDING_TRANSCODE_REASON = "android-post-recording-transcode";
5276
+ var MEDIA_RECORDER_ERROR_CODE = "recording.mediarecorder-error";
5277
+ var MEDIA_RECORDER_EMPTY_ERROR_CODE = "recording.mediarecorder-empty";
5249
5278
  function createUnsupportedRouteError(routeDecision) {
5250
5279
  const guidance = routeDecision.guidance?.message ?? "This browser or device cannot safely record video. Try a supported browser or upload an existing video file.";
5251
5280
  const error = new Error(guidance);
@@ -5277,6 +5306,8 @@ class StreamRecordingState {
5277
5306
  mediaRecorderStopPromise = null;
5278
5307
  mediaRecorderStopResolve = null;
5279
5308
  mediaRecorderStopReject = null;
5309
+ mediaRecorderError = null;
5310
+ mediaRecorderErrorTimeoutId = null;
5280
5311
  streamManager;
5281
5312
  dependencies;
5282
5313
  constructor(streamManager, dependencies) {
@@ -5449,9 +5480,11 @@ class StreamRecordingState {
5449
5480
  }
5450
5481
  let result;
5451
5482
  if (this.activeRouteDecision.route === "safe-capture-post-recording-transcode") {
5483
+ const safeCaptureResult = await this.stopSafeCaptureRecording();
5452
5484
  logger.debug("[StreamRecordingState] Finalizing MediaRecorder capture");
5453
5485
  result = {
5454
- blob: await this.stopSafeCaptureRecording()
5486
+ blob: safeCaptureResult.blob,
5487
+ mediaRecorderDiagnostics: safeCaptureResult.diagnostics
5455
5488
  };
5456
5489
  } else {
5457
5490
  logger.debug("[StreamRecordingState] Finalizing stream processor");
@@ -5470,7 +5503,8 @@ class StreamRecordingState {
5470
5503
  blob: result.blob,
5471
5504
  tabVisibilityIntervals,
5472
5505
  recordingStats: result.recordingStats,
5473
- encoderAcceleration: result.encoderAcceleration
5506
+ encoderAcceleration: result.encoderAcceleration,
5507
+ mediaRecorderDiagnostics: result.mediaRecorderDiagnostics
5474
5508
  };
5475
5509
  }
5476
5510
  pauseRecording() {
@@ -5481,6 +5515,9 @@ class StreamRecordingState {
5481
5515
  if (this.tabVisibilityTracker) {
5482
5516
  this.tabVisibilityTracker.pause();
5483
5517
  }
5518
+ if (this.mediaRecorder?.state === "recording") {
5519
+ this.mediaRecorder.pause?.();
5520
+ }
5484
5521
  if (this.streamProcessor && this.isRecording()) {
5485
5522
  this.streamProcessor.pause();
5486
5523
  }
@@ -5494,6 +5531,9 @@ class StreamRecordingState {
5494
5531
  if (this.tabVisibilityTracker) {
5495
5532
  this.tabVisibilityTracker.resume();
5496
5533
  }
5534
+ if (this.mediaRecorder?.state === "paused") {
5535
+ this.mediaRecorder.resume?.();
5536
+ }
5497
5537
  this.startRecordingTimer();
5498
5538
  if (this.streamProcessor && this.isRecording()) {
5499
5539
  this.streamProcessor.resume();
@@ -5593,6 +5633,7 @@ class StreamRecordingState {
5593
5633
  }
5594
5634
  await this.applySafeCaptureTrackConstraints(mediaStream);
5595
5635
  this.mediaRecorderChunks = [];
5636
+ this.mediaRecorderError = null;
5596
5637
  const preferredMimeType = this.resolvePreferredSafeCaptureMimeType(config);
5597
5638
  const recorderOptions = preferredMimeType === undefined ? undefined : { mimeType: preferredMimeType };
5598
5639
  const recorder = new MediaRecorder(mediaStream, recorderOptions);
@@ -5601,20 +5642,34 @@ class StreamRecordingState {
5601
5642
  this.mediaRecorderStopResolve = resolve;
5602
5643
  this.mediaRecorderStopReject = reject;
5603
5644
  });
5645
+ this.mediaRecorderStopPromise.catch(() => {
5646
+ return;
5647
+ });
5604
5648
  recorder.ondataavailable = (event) => {
5605
5649
  if (event.data.size > 0) {
5606
5650
  this.mediaRecorderChunks.push(event.data);
5607
5651
  }
5608
5652
  };
5609
5653
  recorder.onstop = () => {
5654
+ this.clearMediaRecorderErrorTimeout();
5655
+ if (this.mediaRecorderError !== null) {
5656
+ this.updateMediaRecorderErrorDiagnostics(recorder);
5657
+ this.mediaRecorderStopReject?.(this.mediaRecorderError);
5658
+ return;
5659
+ }
5610
5660
  const mimeType = recorder.mimeType || this.resolveSafeCaptureMimeType();
5611
5661
  const blob = new Blob(this.mediaRecorderChunks, { type: mimeType });
5612
- this.mediaRecorderStopResolve?.(blob);
5662
+ const diagnostics = this.buildMediaRecorderDiagnostics(recorder);
5663
+ if (blob.size > 0) {
5664
+ this.mediaRecorderStopResolve?.({ blob, diagnostics });
5665
+ return;
5666
+ }
5667
+ this.mediaRecorderStopReject?.(this.createEmptyMediaRecorderError(recorder));
5613
5668
  };
5614
- recorder.onerror = () => {
5615
- this.mediaRecorderStopReject?.(new Error("Browser recording failed before upload could start"));
5669
+ recorder.onerror = (event) => {
5670
+ this.mediaRecorderError = this.createMediaRecorderError(event, recorder);
5616
5671
  };
5617
- recorder.start();
5672
+ recorder.start(SAFE_CAPTURE_TIMESLICE_MS);
5618
5673
  }
5619
5674
  async applySafeCaptureTrackConstraints(mediaStream) {
5620
5675
  const constraints = this.resolveSafeCaptureVideoConstraints();
@@ -5635,6 +5690,9 @@ class StreamRecordingState {
5635
5690
  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
5691
  return LOW_RESOURCE_SAFE_CAPTURE_CONSTRAINTS;
5637
5692
  }
5693
+ if (reasonCodes.includes(ANDROID_POST_RECORDING_TRANSCODE_REASON)) {
5694
+ return ANDROID_SAFE_CAPTURE_CONSTRAINTS;
5695
+ }
5638
5696
  return null;
5639
5697
  }
5640
5698
  async stopSafeCaptureRecording() {
@@ -5646,6 +5704,7 @@ class StreamRecordingState {
5646
5704
  if (recorder.state !== "inactive") {
5647
5705
  recorder.stop();
5648
5706
  }
5707
+ this.scheduleMediaRecorderErrorFinalizeTimeout(recorder);
5649
5708
  try {
5650
5709
  return await stopPromise;
5651
5710
  } finally {
@@ -5654,6 +5713,8 @@ class StreamRecordingState {
5654
5713
  this.mediaRecorderStopPromise = null;
5655
5714
  this.mediaRecorderStopResolve = null;
5656
5715
  this.mediaRecorderStopReject = null;
5716
+ this.mediaRecorderError = null;
5717
+ this.clearMediaRecorderErrorTimeout();
5657
5718
  this.activeRouteDecision = DEFAULT_ROUTE_DECISION;
5658
5719
  }
5659
5720
  }
@@ -5669,17 +5730,110 @@ class StreamRecordingState {
5669
5730
  }
5670
5731
  return MediaRecorder.isTypeSupported(mimeType);
5671
5732
  };
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
- ] : [
5733
+ const webmCandidateMimeTypes = [
5677
5734
  "video/webm;codecs=vp8,opus",
5678
5735
  "video/webm;codecs=vp9,opus",
5679
5736
  "video/webm"
5680
5737
  ];
5738
+ const mp4CandidateMimeTypes = [
5739
+ "video/mp4;codecs=avc1.42E01E,mp4a.40.2",
5740
+ "video/mp4;codecs=avc1.42001f,mp4a.40.2",
5741
+ "video/mp4"
5742
+ ];
5743
+ let candidateMimeTypes = mp4CandidateMimeTypes;
5744
+ if (this.shouldPreferWebmSafeCapture(config)) {
5745
+ candidateMimeTypes = [
5746
+ ...webmCandidateMimeTypes,
5747
+ ...mp4CandidateMimeTypes
5748
+ ];
5749
+ } else if (config.format !== "mp4") {
5750
+ candidateMimeTypes = webmCandidateMimeTypes;
5751
+ }
5681
5752
  return candidateMimeTypes.find(canUseMimeType);
5682
5753
  }
5754
+ shouldPreferWebmSafeCapture(config) {
5755
+ if (config.format !== "mp4") {
5756
+ return false;
5757
+ }
5758
+ return this.activeRouteDecision.reasonCodes.includes(ANDROID_POST_RECORDING_TRANSCODE_REASON);
5759
+ }
5760
+ scheduleMediaRecorderErrorFinalizeTimeout(recorder) {
5761
+ if (this.mediaRecorderErrorTimeoutId !== null) {
5762
+ return;
5763
+ }
5764
+ this.mediaRecorderErrorTimeoutId = window.setTimeout(() => {
5765
+ if (this.mediaRecorderError !== null) {
5766
+ this.mediaRecorderStopReject?.(this.mediaRecorderError);
5767
+ return;
5768
+ }
5769
+ this.mediaRecorderStopReject?.(this.createEmptyMediaRecorderError(recorder));
5770
+ }, MEDIA_RECORDER_ERROR_FINALIZE_TIMEOUT_MS);
5771
+ }
5772
+ clearMediaRecorderErrorTimeout() {
5773
+ if (this.mediaRecorderErrorTimeoutId === null) {
5774
+ return;
5775
+ }
5776
+ window.clearTimeout(this.mediaRecorderErrorTimeoutId);
5777
+ this.mediaRecorderErrorTimeoutId = null;
5778
+ }
5779
+ createMediaRecorderError(event, recorder) {
5780
+ const mediaRecorderEvent = event;
5781
+ const browserError = mediaRecorderEvent.error;
5782
+ const error = new Error("Browser recording failed before upload could start");
5783
+ error.name = "MediaRecorderRecordingError";
5784
+ error.code = MEDIA_RECORDER_ERROR_CODE;
5785
+ error.mediaRecorderMimeType = recorder.mimeType;
5786
+ error.mediaRecorderChunkCount = this.mediaRecorderChunks.length;
5787
+ error.mediaRecorderChunkSize = this.getMediaRecorderChunkSize();
5788
+ if (browserError?.name) {
5789
+ error.mediaRecorderErrorName = browserError.name;
5790
+ }
5791
+ if (browserError?.message) {
5792
+ error.mediaRecorderErrorMessage = browserError.message;
5793
+ }
5794
+ return error;
5795
+ }
5796
+ createEmptyMediaRecorderError(recorder) {
5797
+ const error = new Error("Browser recording produced no data before upload could start");
5798
+ error.name = "MediaRecorderEmptyDataError";
5799
+ error.code = MEDIA_RECORDER_EMPTY_ERROR_CODE;
5800
+ error.mediaRecorderMimeType = recorder.mimeType;
5801
+ error.mediaRecorderChunkCount = this.mediaRecorderChunks.length;
5802
+ error.mediaRecorderChunkSize = this.getMediaRecorderChunkSize();
5803
+ if (this.mediaRecorderError?.mediaRecorderErrorName) {
5804
+ error.mediaRecorderErrorName = this.mediaRecorderError.mediaRecorderErrorName;
5805
+ }
5806
+ if (this.mediaRecorderError?.mediaRecorderErrorMessage) {
5807
+ error.mediaRecorderErrorMessage = this.mediaRecorderError.mediaRecorderErrorMessage;
5808
+ }
5809
+ return error;
5810
+ }
5811
+ updateMediaRecorderErrorDiagnostics(recorder) {
5812
+ if (this.mediaRecorderError === null) {
5813
+ return;
5814
+ }
5815
+ this.mediaRecorderError.mediaRecorderMimeType = recorder.mimeType;
5816
+ this.mediaRecorderError.mediaRecorderChunkCount = this.mediaRecorderChunks.length;
5817
+ this.mediaRecorderError.mediaRecorderChunkSize = this.getMediaRecorderChunkSize();
5818
+ }
5819
+ buildMediaRecorderDiagnostics(recorder) {
5820
+ const diagnostics = {
5821
+ mediaRecorderMimeType: recorder.mimeType,
5822
+ mediaRecorderChunkCount: this.mediaRecorderChunks.length,
5823
+ mediaRecorderChunkSize: this.getMediaRecorderChunkSize(),
5824
+ mediaRecorderHadError: this.mediaRecorderError !== null
5825
+ };
5826
+ if (this.mediaRecorderError?.mediaRecorderErrorName) {
5827
+ diagnostics.mediaRecorderErrorName = this.mediaRecorderError.mediaRecorderErrorName;
5828
+ }
5829
+ if (this.mediaRecorderError?.mediaRecorderErrorMessage) {
5830
+ diagnostics.mediaRecorderErrorMessage = this.mediaRecorderError.mediaRecorderErrorMessage;
5831
+ }
5832
+ return diagnostics;
5833
+ }
5834
+ getMediaRecorderChunkSize() {
5835
+ return this.mediaRecorderChunks.reduce((total, chunk) => total + chunk.size, 0);
5836
+ }
5683
5837
  resetPauseState() {
5684
5838
  this.totalPausedTime = 0;
5685
5839
  this.pauseStartTime = null;
@@ -5759,6 +5913,8 @@ class StreamRecordingState {
5759
5913
  this.mediaRecorderStopPromise = null;
5760
5914
  this.mediaRecorderStopResolve = null;
5761
5915
  this.mediaRecorderStopReject = null;
5916
+ this.mediaRecorderError = null;
5917
+ this.clearMediaRecorderErrorTimeout();
5762
5918
  this.activeRouteDecision = DEFAULT_ROUTE_DECISION;
5763
5919
  if (this.streamProcessor) {
5764
5920
  this.streamProcessor.destroy();
@@ -5906,6 +6062,8 @@ var RECORDING_CODEC_UNSUPPORTED_CODES = new Set([
5906
6062
  ]);
5907
6063
  var RECORDING_SERVER_TRANSCODE_FALLBACK_CODES = new Set([
5908
6064
  "recording.post-recording-transcode-fallback",
6065
+ "recording.mediarecorder-empty",
6066
+ "recording.mediarecorder-error",
5909
6067
  "transcode.post-recording-fallback",
5910
6068
  "route.post-recording-transcode-fallback"
5911
6069
  ]);
@@ -6141,7 +6299,7 @@ function resolveDeviceError(input) {
6141
6299
  // package.json
6142
6300
  var package_default = {
6143
6301
  name: "@vidtreo/recorder",
6144
- version: "1.6.3",
6302
+ version: "1.7.0",
6145
6303
  type: "module",
6146
6304
  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
6305
  main: "./dist/index.js",
@@ -6226,6 +6384,13 @@ var MAX_RETRY_ATTEMPTS = 3;
6226
6384
  var MAX_PENDING_EVENTS = 100;
6227
6385
  var BRACKET_ERROR_CODE_PATTERN = /\[([a-z]+(?:[.-][a-z0-9]+)+)\]/i;
6228
6386
  var INLINE_ERROR_CODE_PATTERN = /\b([a-z]+(?:[.-][a-z0-9]+)+)\b/i;
6387
+ var MEDIA_RECORDER_ERROR_PROPERTY_KEYS = [
6388
+ "mediaRecorderErrorName",
6389
+ "mediaRecorderErrorMessage",
6390
+ "mediaRecorderMimeType",
6391
+ "mediaRecorderChunkCount",
6392
+ "mediaRecorderChunkSize"
6393
+ ];
6229
6394
  function resolveInstallationId(dependencies) {
6230
6395
  const storageProvider = dependencies.storageProvider;
6231
6396
  const stored = storageProvider?.getItem(TELEMETRY_STORAGE_KEY);
@@ -6567,6 +6732,23 @@ class TelemetryClient {
6567
6732
  recordingPipelineErrorCode: recordingPipelineCode
6568
6733
  };
6569
6734
  }
6735
+ return {
6736
+ ...properties,
6737
+ ...this.buildMediaRecorderErrorProperties(error)
6738
+ };
6739
+ }
6740
+ buildMediaRecorderErrorProperties(error) {
6741
+ if (!(error && typeof error === "object")) {
6742
+ return {};
6743
+ }
6744
+ const candidate = error;
6745
+ let properties = {};
6746
+ for (const key of MEDIA_RECORDER_ERROR_PROPERTY_KEYS) {
6747
+ const value = candidate[key];
6748
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
6749
+ properties = { ...properties, [key]: value };
6750
+ }
6751
+ }
6570
6752
  return properties;
6571
6753
  }
6572
6754
  buildFingerprint() {
@@ -22637,6 +22819,9 @@ class RecordingManager {
22637
22819
  if (stopResult.encoderAcceleration !== undefined) {
22638
22820
  telemetryProperties.encoderAcceleration = stopResult.encoderAcceleration;
22639
22821
  }
22822
+ if (stopResult.mediaRecorderDiagnostics !== undefined) {
22823
+ Object.assign(telemetryProperties, stopResult.mediaRecorderDiagnostics);
22824
+ }
22640
22825
  return {
22641
22826
  blob: finalBlob,
22642
22827
  telemetryProperties
@@ -22646,6 +22831,10 @@ class RecordingManager {
22646
22831
  this.handleError(normalizedError);
22647
22832
  this.recordingState = RECORDING_STATE_IDLE;
22648
22833
  this.callbacks.onStateChange(this.recordingState);
22834
+ if (this.streamProcessor) {
22835
+ this.streamProcessor.destroy();
22836
+ this.streamProcessor = null;
22837
+ }
22649
22838
  throw normalizedError;
22650
22839
  } finally {
22651
22840
  this.activeRecordingConfig = null;
@@ -23105,6 +23294,7 @@ class RecorderController {
23105
23294
  }
23106
23295
  async stopRecording() {
23107
23296
  const sourceType = this.getCurrentSourceType();
23297
+ let shouldStopStream = true;
23108
23298
  try {
23109
23299
  const stopResult = await this.telemetryManager.executeActionWithResult({
23110
23300
  requestedEvent: "recording.stop.requested",
@@ -23115,7 +23305,6 @@ class RecorderController {
23115
23305
  await this.sourceSwitchManager.handleRecordingStop().catch(() => {
23116
23306
  throw new Error("Source switch cleanup failed");
23117
23307
  });
23118
- this.streamManager.stopStream();
23119
23308
  return recordingStopResult;
23120
23309
  },
23121
23310
  properties: {
@@ -23127,11 +23316,22 @@ class RecorderController {
23127
23316
  });
23128
23317
  return stopResult.blob;
23129
23318
  } catch (error) {
23319
+ shouldStopStream = this.shouldStopStreamAfterStopFailure(error);
23130
23320
  if (isAudioMissingError(error)) {
23131
23321
  this.sendAudioMissingTelemetry(error);
23132
23322
  }
23133
23323
  throw error;
23324
+ } finally {
23325
+ if (shouldStopStream) {
23326
+ this.streamManager.stopStream();
23327
+ }
23328
+ }
23329
+ }
23330
+ shouldStopStreamAfterStopFailure(error) {
23331
+ if (!(error && typeof error === "object" && ("code" in error))) {
23332
+ return true;
23134
23333
  }
23334
+ return error.code !== ERROR_RECORDING_STOP_NOT_READY;
23135
23335
  }
23136
23336
  sendAudioMissingTelemetry(error) {
23137
23337
  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.0",
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",