@vidtreo/recorder 1.7.10 → 1.7.12

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 +3024 -2977
  2. package/dist/index.js +685 -178
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -5287,6 +5287,7 @@ var ANDROID_SAFE_CAPTURE_CONSTRAINTS = {
5287
5287
  };
5288
5288
  var LOW_RESOURCE_SAFE_CAPTURE_FRAME_RATE = 20;
5289
5289
  var ANDROID_SAFE_CAPTURE_FRAME_RATE = 24;
5290
+ var SAFE_CAPTURE_TIMESLICE_MS = 1000;
5290
5291
  var MEDIA_RECORDER_ERROR_FINALIZE_TIMEOUT_MS = 5000;
5291
5292
  var ANDROID_POST_RECORDING_TRANSCODE_REASON = "android-post-recording-transcode";
5292
5293
  var MEDIA_RECORDER_ERROR_CODE = "recording.mediarecorder-error";
@@ -5324,6 +5325,11 @@ class StreamRecordingState {
5324
5325
  mediaRecorderStopReject = null;
5325
5326
  mediaRecorderError = null;
5326
5327
  safeCaptureFrameRate = null;
5328
+ safeCaptureStartTime = null;
5329
+ safeCaptureExpectedDurationSeconds = null;
5330
+ mediaRecorderRequestedMimeType = null;
5331
+ mediaRecorderGeneration = 0;
5332
+ isSafeCaptureSettled = false;
5327
5333
  mediaRecorderErrorTimeoutId = null;
5328
5334
  streamManager;
5329
5335
  dependencies;
@@ -5466,7 +5472,8 @@ class StreamRecordingState {
5466
5472
  this.startRecordingTimer();
5467
5473
  }
5468
5474
  async stopRecording() {
5469
- const recordingElapsedSeconds = (this.dependencies.getCurrentTimestamp() - this.recordingStartTime - this.totalPausedTime) / MILLISECONDS_PER_SECOND2;
5475
+ const stopTimestamp = this.dependencies.getCurrentTimestamp();
5476
+ const recordingElapsedSeconds = this.resolveActiveRecordingDurationSeconds(stopTimestamp);
5470
5477
  logger.debug("[StreamRecordingState] stopRecording called", {
5471
5478
  hasStreamProcessor: !!this.streamProcessor,
5472
5479
  isRecording: this.isRecording(),
@@ -5497,11 +5504,12 @@ class StreamRecordingState {
5497
5504
  }
5498
5505
  let result;
5499
5506
  if (this.activeRouteDecision.route === "safe-capture-post-recording-transcode") {
5500
- const safeCaptureResult = await this.stopSafeCaptureRecording();
5507
+ const safeCaptureResult = await this.stopSafeCaptureRecording(recordingElapsedSeconds);
5501
5508
  logger.debug("[StreamRecordingState] Finalizing MediaRecorder capture");
5502
5509
  result = {
5503
5510
  blob: safeCaptureResult.blob,
5504
5511
  captureFrameRate: safeCaptureResult.captureFrameRate,
5512
+ expectedDurationSeconds: safeCaptureResult.expectedDurationSeconds,
5505
5513
  mediaRecorderDiagnostics: safeCaptureResult.diagnostics
5506
5514
  };
5507
5515
  } else {
@@ -5520,6 +5528,7 @@ class StreamRecordingState {
5520
5528
  return {
5521
5529
  blob: result.blob,
5522
5530
  captureFrameRate: result.captureFrameRate,
5531
+ expectedDurationSeconds: result.expectedDurationSeconds,
5523
5532
  tabVisibilityIntervals,
5524
5533
  recordingStats: result.recordingStats,
5525
5534
  encoderAcceleration: result.encoderAcceleration,
@@ -5544,7 +5553,7 @@ class StreamRecordingState {
5544
5553
  resumeRecording() {
5545
5554
  if (this.pauseStartTime !== null) {
5546
5555
  const pausedDuration = this.dependencies.getCurrentTimestamp() - this.pauseStartTime;
5547
- this.totalPausedTime += pausedDuration;
5556
+ this.totalPausedTime += Math.max(0, pausedDuration);
5548
5557
  this.pauseStartTime = null;
5549
5558
  }
5550
5559
  if (this.tabVisibilityTracker) {
@@ -5639,6 +5648,18 @@ class StreamRecordingState {
5639
5648
  this.totalPausedTime = 0;
5640
5649
  this.pauseStartTime = null;
5641
5650
  }
5651
+ resolveActiveRecordingDurationSeconds(timestamp) {
5652
+ let startTime = this.recordingStartTime;
5653
+ if (this.safeCaptureStartTime !== null) {
5654
+ startTime = this.safeCaptureStartTime;
5655
+ }
5656
+ let pausedTime = this.totalPausedTime;
5657
+ if (this.pauseStartTime !== null) {
5658
+ pausedTime += Math.max(0, timestamp - this.pauseStartTime);
5659
+ }
5660
+ const activeDuration = Math.max(0, timestamp - startTime - pausedTime);
5661
+ return activeDuration / MILLISECONDS_PER_SECOND2;
5662
+ }
5642
5663
  async startSafeCaptureRecording(mediaStream, config) {
5643
5664
  if (typeof MediaRecorder === "undefined") {
5644
5665
  throw createUnsupportedRouteError({
@@ -5654,9 +5675,13 @@ class StreamRecordingState {
5654
5675
  this.safeCaptureFrameRate = this.resolveEffectiveSafeCaptureFrameRate(mediaStream, config);
5655
5676
  this.mediaRecorderChunks = [];
5656
5677
  this.mediaRecorderError = null;
5657
- const preferredMimeType = this.resolvePreferredSafeCaptureMimeType(config);
5658
- const recorderOptions = preferredMimeType === undefined ? undefined : { mimeType: preferredMimeType };
5659
- const recorder = new MediaRecorder(mediaStream, recorderOptions);
5678
+ this.safeCaptureExpectedDurationSeconds = null;
5679
+ this.isSafeCaptureSettled = false;
5680
+ const generation = this.mediaRecorderGeneration + 1;
5681
+ this.mediaRecorderGeneration = generation;
5682
+ const selection = await this.createSafeCaptureRecorder(mediaStream);
5683
+ const recorder = selection.recorder;
5684
+ this.mediaRecorderRequestedMimeType = selection.requestedMimeType === undefined ? null : selection.requestedMimeType;
5660
5685
  this.mediaRecorder = recorder;
5661
5686
  this.mediaRecorderStopPromise = new Promise((resolve, reject) => {
5662
5687
  this.mediaRecorderStopResolve = resolve;
@@ -5666,40 +5691,76 @@ class StreamRecordingState {
5666
5691
  return;
5667
5692
  });
5668
5693
  recorder.ondataavailable = (event) => {
5694
+ if (!this.isActiveSafeCaptureGeneration(generation)) {
5695
+ return;
5696
+ }
5669
5697
  if (event.data.size > 0) {
5670
5698
  this.mediaRecorderChunks.push(event.data);
5671
5699
  }
5672
5700
  };
5673
5701
  recorder.onstop = () => {
5702
+ if (!this.isActiveSafeCaptureGeneration(generation)) {
5703
+ return;
5704
+ }
5705
+ this.isSafeCaptureSettled = true;
5674
5706
  this.clearMediaRecorderErrorTimeout();
5675
5707
  if (this.mediaRecorderError !== null) {
5676
5708
  this.updateMediaRecorderErrorDiagnostics(recorder);
5677
5709
  this.mediaRecorderStopReject?.(this.mediaRecorderError);
5678
5710
  return;
5679
5711
  }
5680
- const mimeType = recorder.mimeType || this.resolveSafeCaptureMimeType();
5712
+ const mimeType = this.resolveRecordedMimeType(recorder);
5681
5713
  const blob = new Blob(this.mediaRecorderChunks, { type: mimeType });
5682
5714
  const diagnostics = this.buildMediaRecorderDiagnostics(recorder);
5683
5715
  const captureFrameRate = this.safeCaptureFrameRate;
5716
+ let expectedDurationSeconds = this.safeCaptureExpectedDurationSeconds;
5717
+ if (expectedDurationSeconds === null) {
5718
+ expectedDurationSeconds = this.resolveActiveRecordingDurationSeconds(this.dependencies.getCurrentTimestamp());
5719
+ }
5684
5720
  if (captureFrameRate === null) {
5685
5721
  this.mediaRecorderStopReject?.(new Error("Safe capture frame rate is unavailable"));
5686
5722
  return;
5687
5723
  }
5688
5724
  diagnostics.mediaRecorderCaptureFrameRate = captureFrameRate;
5725
+ diagnostics.mediaRecorderExpectedDurationSeconds = expectedDurationSeconds;
5689
5726
  if (blob.size > 0) {
5690
5727
  this.mediaRecorderStopResolve?.({
5691
5728
  blob,
5692
5729
  captureFrameRate,
5693
- diagnostics
5730
+ diagnostics,
5731
+ expectedDurationSeconds
5694
5732
  });
5695
5733
  return;
5696
5734
  }
5697
5735
  this.mediaRecorderStopReject?.(this.createEmptyMediaRecorderError(recorder));
5698
5736
  };
5699
5737
  recorder.onerror = (event) => {
5738
+ if (!this.isActiveSafeCaptureGeneration(generation)) {
5739
+ return;
5740
+ }
5700
5741
  this.mediaRecorderError = this.createMediaRecorderError(event, recorder);
5701
5742
  };
5702
- recorder.start();
5743
+ this.safeCaptureStartTime = this.dependencies.getCurrentTimestamp();
5744
+ recorder.start(SAFE_CAPTURE_TIMESLICE_MS);
5745
+ }
5746
+ isActiveSafeCaptureGeneration(generation) {
5747
+ return generation === this.mediaRecorderGeneration && !this.isSafeCaptureSettled;
5748
+ }
5749
+ async createSafeCaptureRecorder(mediaStream, candidateIndex = 0) {
5750
+ const candidateMimeTypes = this.resolveSafeCaptureMimeTypeCandidates();
5751
+ const requestedMimeType = candidateMimeTypes[candidateIndex];
5752
+ if (requestedMimeType === undefined) {
5753
+ return {
5754
+ recorder: new MediaRecorder(mediaStream)
5755
+ };
5756
+ }
5757
+ const recorder = await Promise.resolve().then(() => new MediaRecorder(mediaStream, {
5758
+ mimeType: requestedMimeType
5759
+ })).catch(() => null);
5760
+ if (recorder !== null) {
5761
+ return { recorder, requestedMimeType };
5762
+ }
5763
+ return await this.createSafeCaptureRecorder(mediaStream, candidateIndex + 1);
5703
5764
  }
5704
5765
  resolveEffectiveSafeCaptureFrameRate(mediaStream, config) {
5705
5766
  const videoTrack = mediaStream.getVideoTracks()[0];
@@ -5728,6 +5789,9 @@ class StreamRecordingState {
5728
5789
  }
5729
5790
  const videoTracks = mediaStream.getVideoTracks();
5730
5791
  await Promise.all(videoTracks.map(async (track) => {
5792
+ if (typeof track.applyConstraints !== "function") {
5793
+ return;
5794
+ }
5731
5795
  try {
5732
5796
  await track.applyConstraints(constraints);
5733
5797
  } catch (error) {
@@ -5745,36 +5809,62 @@ class StreamRecordingState {
5745
5809
  }
5746
5810
  return null;
5747
5811
  }
5748
- async stopSafeCaptureRecording() {
5812
+ async stopSafeCaptureRecording(expectedDurationSeconds) {
5749
5813
  const recorder = this.mediaRecorder;
5750
5814
  const stopPromise = this.mediaRecorderStopPromise;
5751
5815
  if (!(recorder && stopPromise)) {
5752
5816
  throw new Error("Safe capture recorder was not started");
5753
5817
  }
5754
- if (recorder.state !== "inactive") {
5755
- recorder.stop();
5756
- }
5757
- this.scheduleMediaRecorderErrorFinalizeTimeout(recorder);
5758
- try {
5759
- return await stopPromise;
5760
- } finally {
5761
- this.mediaRecorder = null;
5762
- this.mediaRecorderChunks = [];
5763
- this.mediaRecorderStopPromise = null;
5764
- this.mediaRecorderStopResolve = null;
5765
- this.mediaRecorderStopReject = null;
5766
- this.mediaRecorderError = null;
5767
- this.safeCaptureFrameRate = null;
5768
- this.clearMediaRecorderErrorTimeout();
5769
- this.activeRouteDecision = DEFAULT_ROUTE_DECISION;
5770
- }
5818
+ this.safeCaptureExpectedDurationSeconds = expectedDurationSeconds;
5819
+ return await Promise.resolve().then(() => {
5820
+ if (recorder.state !== "inactive") {
5821
+ recorder.stop();
5822
+ }
5823
+ this.scheduleMediaRecorderErrorFinalizeTimeout(recorder);
5824
+ return stopPromise;
5825
+ }).then((result) => {
5826
+ result.expectedDurationSeconds = expectedDurationSeconds;
5827
+ result.diagnostics.mediaRecorderExpectedDurationSeconds = expectedDurationSeconds;
5828
+ return result;
5829
+ }).finally(() => {
5830
+ this.cleanupSafeCaptureRecording();
5831
+ });
5832
+ }
5833
+ cleanupSafeCaptureRecording() {
5834
+ this.mediaRecorder = null;
5835
+ this.mediaRecorderChunks = [];
5836
+ this.mediaRecorderStopPromise = null;
5837
+ this.mediaRecorderStopResolve = null;
5838
+ this.mediaRecorderStopReject = null;
5839
+ this.mediaRecorderError = null;
5840
+ this.safeCaptureFrameRate = null;
5841
+ this.safeCaptureStartTime = null;
5842
+ this.safeCaptureExpectedDurationSeconds = null;
5843
+ this.mediaRecorderRequestedMimeType = null;
5844
+ this.mediaRecorderGeneration += 1;
5845
+ this.isSafeCaptureSettled = false;
5846
+ this.clearMediaRecorderErrorTimeout();
5847
+ this.activeRouteDecision = DEFAULT_ROUTE_DECISION;
5771
5848
  }
5772
5849
  resolveSafeCaptureMimeType() {
5773
5850
  const mediaStream = this.streamManager.getStream();
5774
5851
  const hasVideo = (mediaStream?.getVideoTracks().length ?? 0) > 0;
5775
5852
  return hasVideo ? "video/webm" : "audio/webm";
5776
5853
  }
5777
- resolvePreferredSafeCaptureMimeType(config) {
5854
+ resolveRecordedMimeType(recorder) {
5855
+ if (recorder.mimeType.trim().length > 0) {
5856
+ return recorder.mimeType;
5857
+ }
5858
+ const chunkWithMimeType = this.mediaRecorderChunks.find((chunk) => chunk.type.trim().length > 0);
5859
+ if (chunkWithMimeType !== undefined) {
5860
+ return chunkWithMimeType.type;
5861
+ }
5862
+ if (this.mediaRecorderRequestedMimeType !== null) {
5863
+ return this.mediaRecorderRequestedMimeType;
5864
+ }
5865
+ return this.resolveSafeCaptureMimeType();
5866
+ }
5867
+ resolveSafeCaptureMimeTypeCandidates() {
5778
5868
  const canUseMimeType = (mimeType) => {
5779
5869
  if (typeof MediaRecorder.isTypeSupported !== "function") {
5780
5870
  return false;
@@ -5791,22 +5881,7 @@ class StreamRecordingState {
5791
5881
  "video/mp4;codecs=avc1.42001f,mp4a.40.2",
5792
5882
  "video/mp4"
5793
5883
  ];
5794
- let candidateMimeTypes = mp4CandidateMimeTypes;
5795
- if (this.shouldPreferWebmSafeCapture(config)) {
5796
- candidateMimeTypes = [
5797
- ...webmCandidateMimeTypes,
5798
- ...mp4CandidateMimeTypes
5799
- ];
5800
- } else if (config.format !== "mp4") {
5801
- candidateMimeTypes = webmCandidateMimeTypes;
5802
- }
5803
- return candidateMimeTypes.find(canUseMimeType);
5804
- }
5805
- shouldPreferWebmSafeCapture(config) {
5806
- if (config.format !== "mp4") {
5807
- return false;
5808
- }
5809
- return this.activeRouteDecision.reasonCodes.includes(ANDROID_POST_RECORDING_TRANSCODE_REASON);
5884
+ return [...webmCandidateMimeTypes, ...mp4CandidateMimeTypes].filter(canUseMimeType);
5810
5885
  }
5811
5886
  scheduleMediaRecorderErrorFinalizeTimeout(recorder) {
5812
5887
  if (this.mediaRecorderErrorTimeoutId !== null) {
@@ -5833,9 +5908,10 @@ class StreamRecordingState {
5833
5908
  const error = new Error("Browser recording failed before upload could start");
5834
5909
  error.name = "MediaRecorderRecordingError";
5835
5910
  error.code = MEDIA_RECORDER_ERROR_CODE;
5836
- error.mediaRecorderMimeType = recorder.mimeType;
5911
+ error.mediaRecorderMimeType = this.resolveRecordedMimeType(recorder);
5837
5912
  error.mediaRecorderChunkCount = this.mediaRecorderChunks.length;
5838
5913
  error.mediaRecorderChunkSize = this.getMediaRecorderChunkSize();
5914
+ this.assignSafeCaptureErrorDiagnostics(error);
5839
5915
  if (browserError?.name) {
5840
5916
  error.mediaRecorderErrorName = browserError.name;
5841
5917
  }
@@ -5848,9 +5924,10 @@ class StreamRecordingState {
5848
5924
  const error = new Error("Browser recording produced no data before upload could start");
5849
5925
  error.name = "MediaRecorderEmptyDataError";
5850
5926
  error.code = MEDIA_RECORDER_EMPTY_ERROR_CODE;
5851
- error.mediaRecorderMimeType = recorder.mimeType;
5927
+ error.mediaRecorderMimeType = this.resolveRecordedMimeType(recorder);
5852
5928
  error.mediaRecorderChunkCount = this.mediaRecorderChunks.length;
5853
5929
  error.mediaRecorderChunkSize = this.getMediaRecorderChunkSize();
5930
+ this.assignSafeCaptureErrorDiagnostics(error);
5854
5931
  if (this.mediaRecorderError?.mediaRecorderErrorName) {
5855
5932
  error.mediaRecorderErrorName = this.mediaRecorderError.mediaRecorderErrorName;
5856
5933
  }
@@ -5863,17 +5940,21 @@ class StreamRecordingState {
5863
5940
  if (this.mediaRecorderError === null) {
5864
5941
  return;
5865
5942
  }
5866
- this.mediaRecorderError.mediaRecorderMimeType = recorder.mimeType;
5943
+ this.mediaRecorderError.mediaRecorderMimeType = this.resolveRecordedMimeType(recorder);
5867
5944
  this.mediaRecorderError.mediaRecorderChunkCount = this.mediaRecorderChunks.length;
5868
5945
  this.mediaRecorderError.mediaRecorderChunkSize = this.getMediaRecorderChunkSize();
5946
+ this.assignSafeCaptureErrorDiagnostics(this.mediaRecorderError);
5869
5947
  }
5870
5948
  buildMediaRecorderDiagnostics(recorder) {
5871
5949
  const diagnostics = {
5872
- mediaRecorderMimeType: recorder.mimeType,
5950
+ mediaRecorderMimeType: this.resolveRecordedMimeType(recorder),
5873
5951
  mediaRecorderChunkCount: this.mediaRecorderChunks.length,
5874
5952
  mediaRecorderChunkSize: this.getMediaRecorderChunkSize(),
5875
5953
  mediaRecorderHadError: this.mediaRecorderError !== null
5876
5954
  };
5955
+ if (this.mediaRecorderRequestedMimeType !== null) {
5956
+ diagnostics.mediaRecorderRequestedMimeType = this.mediaRecorderRequestedMimeType;
5957
+ }
5877
5958
  if (this.mediaRecorderError?.mediaRecorderErrorName) {
5878
5959
  diagnostics.mediaRecorderErrorName = this.mediaRecorderError.mediaRecorderErrorName;
5879
5960
  }
@@ -5882,6 +5963,15 @@ class StreamRecordingState {
5882
5963
  }
5883
5964
  return diagnostics;
5884
5965
  }
5966
+ assignSafeCaptureErrorDiagnostics(error) {
5967
+ error.deferredIntegrityStage = "capture";
5968
+ if (this.mediaRecorderRequestedMimeType !== null) {
5969
+ error.mediaRecorderRequestedMimeType = this.mediaRecorderRequestedMimeType;
5970
+ }
5971
+ if (this.safeCaptureExpectedDurationSeconds !== null) {
5972
+ error.mediaRecorderExpectedDurationSeconds = this.safeCaptureExpectedDurationSeconds;
5973
+ }
5974
+ }
5885
5975
  getMediaRecorderChunkSize() {
5886
5976
  return this.mediaRecorderChunks.reduce((total, chunk) => total + chunk.size, 0);
5887
5977
  }
@@ -5965,6 +6055,12 @@ class StreamRecordingState {
5965
6055
  this.mediaRecorderStopResolve = null;
5966
6056
  this.mediaRecorderStopReject = null;
5967
6057
  this.mediaRecorderError = null;
6058
+ this.safeCaptureFrameRate = null;
6059
+ this.safeCaptureStartTime = null;
6060
+ this.safeCaptureExpectedDurationSeconds = null;
6061
+ this.mediaRecorderRequestedMimeType = null;
6062
+ this.mediaRecorderGeneration += 1;
6063
+ this.isSafeCaptureSettled = false;
5968
6064
  this.clearMediaRecorderErrorTimeout();
5969
6065
  this.activeRouteDecision = DEFAULT_ROUTE_DECISION;
5970
6066
  if (this.streamProcessor) {
@@ -6112,6 +6208,7 @@ var RECORDING_CODEC_UNSUPPORTED_CODES = new Set([
6112
6208
  "recording.video-codec-unavailable"
6113
6209
  ]);
6114
6210
  var RECORDING_SERVER_TRANSCODE_FALLBACK_CODES = new Set([
6211
+ "recording.deferred-transcode-integrity",
6115
6212
  "recording.post-recording-transcode-fallback",
6116
6213
  "transcode.post-recording-fallback",
6117
6214
  "route.post-recording-transcode-fallback"
@@ -6354,7 +6451,7 @@ function resolveDeviceError(input) {
6354
6451
  // package.json
6355
6452
  var package_default = {
6356
6453
  name: "@vidtreo/recorder",
6357
- version: "1.7.10",
6454
+ version: "1.7.12",
6358
6455
  type: "module",
6359
6456
  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.",
6360
6457
  main: "./dist/index.js",
@@ -6439,12 +6536,23 @@ var MAX_RETRY_ATTEMPTS = 3;
6439
6536
  var MAX_PENDING_EVENTS = 100;
6440
6537
  var BRACKET_ERROR_CODE_PATTERN = /\[([a-z]+(?:[.-][a-z0-9]+)+)\]/i;
6441
6538
  var INLINE_ERROR_CODE_PATTERN = /\b([a-z]+(?:[.-][a-z0-9]+)+)\b/i;
6442
- var MEDIA_RECORDER_ERROR_PROPERTY_KEYS = [
6539
+ var RECORDING_INTEGRITY_ERROR_PROPERTY_KEYS = [
6443
6540
  "mediaRecorderErrorName",
6444
6541
  "mediaRecorderErrorMessage",
6445
6542
  "mediaRecorderMimeType",
6446
6543
  "mediaRecorderChunkCount",
6447
- "mediaRecorderChunkSize"
6544
+ "mediaRecorderChunkSize",
6545
+ "mediaRecorderExpectedDurationSeconds",
6546
+ "mediaRecorderRequestedMimeType",
6547
+ "deferredIntegrityStage",
6548
+ "deferredSourceDurationSeconds",
6549
+ "deferredSourcePacketCount",
6550
+ "deferredSourcePacketRate",
6551
+ "deferredSourceMaximumPacketGap",
6552
+ "deferredOutputDurationSeconds",
6553
+ "deferredOutputPacketCount",
6554
+ "deferredOutputPacketRate",
6555
+ "deferredOutputMaximumPacketGap"
6448
6556
  ];
6449
6557
  function resolveInstallationId(dependencies) {
6450
6558
  const storageProvider = dependencies.storageProvider;
@@ -6800,7 +6908,7 @@ class TelemetryClient {
6800
6908
  }
6801
6909
  const candidate = error;
6802
6910
  let properties = {};
6803
- for (const key of MEDIA_RECORDER_ERROR_PROPERTY_KEYS) {
6911
+ for (const key of RECORDING_INTEGRITY_ERROR_PROPERTY_KEYS) {
6804
6912
  const value = candidate[key];
6805
6913
  if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
6806
6914
  properties = { ...properties, [key]: value };
@@ -8773,12 +8881,14 @@ var workerCode = `(() => {
8773
8881
  }
8774
8882
  target = mod != null ? __create(__getProtoOf(mod)) : {};
8775
8883
  const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
8776
- for (let key of __getOwnPropNames(mod))
8777
- if (!__hasOwnProp.call(to, key))
8778
- __defProp(to, key, {
8779
- get: __accessProp.bind(mod, key),
8780
- enumerable: true
8781
- });
8884
+ if (mod && typeof mod === "object" || typeof mod === "function") {
8885
+ for (let key of __getOwnPropNames(mod))
8886
+ if (!__hasOwnProp.call(to, key))
8887
+ __defProp(to, key, {
8888
+ get: __accessProp.bind(mod, key),
8889
+ enumerable: true
8890
+ });
8891
+ }
8782
8892
  if (canCache)
8783
8893
  cache.set(mod, to);
8784
8894
  return to;
@@ -8824,8 +8934,8 @@ var workerCode = `(() => {
8824
8934
  // src/core/processor/worker/recorder-worker.ts
8825
8935
  var exports_recorder_worker = {};
8826
8936
  __export(exports_recorder_worker, {
8827
- initializeRecorderWorker: () => initializeRecorderWorker,
8828
- RecorderWorker: () => RecorderWorker
8937
+ RecorderWorker: () => RecorderWorker,
8938
+ initializeRecorderWorker: () => initializeRecorderWorker
8829
8939
  });
8830
8940
 
8831
8941
  // ../../node_modules/.bun/mediabunny@1.45.2/node_modules/mediabunny/dist/modules/src/misc.js
@@ -22791,76 +22901,404 @@ class StreamProcessor {
22791
22901
  }
22792
22902
 
22793
22903
  // src/core/transcode/deferred-video-transcoder.ts
22794
- import { ALL_FORMATS as ALL_FORMATS2, BlobSource as BlobSource3, EncodedPacketSink, Input as Input3 } from "mediabunny";
22904
+ import { ALL_FORMATS as ALL_FORMATS3, BlobSource as BlobSource4, EncodedPacketSink as EncodedPacketSink2, Input as Input4 } from "mediabunny";
22905
+
22906
+ // src/core/transcode/deferred-video-timing-repair.ts
22907
+ import {
22908
+ ALL_FORMATS as ALL_FORMATS2,
22909
+ BlobSource as BlobSource3,
22910
+ BufferTarget as BufferTarget2,
22911
+ EncodedAudioPacketSource,
22912
+ EncodedPacketSink,
22913
+ EncodedVideoPacketSource,
22914
+ Input as Input3,
22915
+ Mp4OutputFormat as Mp4OutputFormat2,
22916
+ Output as Output2
22917
+ } from "mediabunny";
22918
+ var MINIMUM_IMPLAUSIBLE_DURATION_SECONDS = 60;
22919
+ var EXPECTED_DURATION_MULTIPLIER = 2;
22920
+ var MAXIMUM_REPAIR_RATIO = 0.05;
22921
+ var MINIMUM_REPAIR_LIMIT = 2;
22922
+ var MAXIMUM_BOUNDARY_REGRESSION_MULTIPLIER = 2;
22923
+ var MINIMUM_BOUNDARY_REGRESSION_TOLERANCE_SECONDS = 0.25;
22924
+ function resolveImplausibleDurationThreshold(expectedDurationSeconds) {
22925
+ return Math.max(MINIMUM_IMPLAUSIBLE_DURATION_SECONDS, expectedDurationSeconds * EXPECTED_DURATION_MULTIPLIER);
22926
+ }
22927
+ function resolveMedian(values) {
22928
+ values.sort((left, right) => left - right);
22929
+ return values[Math.floor(values.length / 2)];
22930
+ }
22931
+ function validatePacketTimings(packets, expectedDurationSeconds) {
22932
+ if (!Number.isFinite(expectedDurationSeconds) || expectedDurationSeconds < 0) {
22933
+ throw new Error("Timing repair requires a valid recording duration");
22934
+ }
22935
+ if (packets.length === 0) {
22936
+ throw new Error("Timing repair requires video packets");
22937
+ }
22938
+ for (const packet of packets) {
22939
+ if (!Number.isFinite(packet.timestamp)) {
22940
+ throw new Error("Timing repair requires finite packet timestamps");
22941
+ }
22942
+ if (!Number.isFinite(packet.duration) || packet.duration < 0) {
22943
+ throw new Error("Timing repair requires valid packet durations");
22944
+ }
22945
+ }
22946
+ }
22947
+ function createDeferredVideoTimingRepairPlan(packets, expectedDurationSeconds) {
22948
+ validatePacketTimings(packets, expectedDurationSeconds);
22949
+ const threshold = resolveImplausibleDurationThreshold(expectedDurationSeconds);
22950
+ const packetTimings = packets.map((packet) => ({
22951
+ duration: packet.duration,
22952
+ isDurationRepaired: false,
22953
+ isTimestampRepaired: false,
22954
+ timestamp: packet.timestamp
22955
+ }));
22956
+ const repairIndexes = [];
22957
+ const validDurations = [];
22958
+ for (let index = 0;index < packets.length; index += 1) {
22959
+ const duration = packets[index].duration;
22960
+ if (duration > threshold) {
22961
+ repairIndexes.push(index);
22962
+ continue;
22963
+ }
22964
+ if (duration > 0) {
22965
+ validDurations.push(duration);
22966
+ }
22967
+ }
22968
+ if (repairIndexes.length === 0) {
22969
+ return {
22970
+ nominalDurationSeconds: null,
22971
+ packetTimings,
22972
+ repairedDurationCount: 0,
22973
+ repairedTimestampCount: 0
22974
+ };
22975
+ }
22976
+ const maximumRepairCount = Math.max(MINIMUM_REPAIR_LIMIT, Math.floor(packets.length * MAXIMUM_REPAIR_RATIO));
22977
+ if (repairIndexes.length > maximumRepairCount) {
22978
+ throw new Error("Video timing corruption is not isolated");
22979
+ }
22980
+ if (validDurations.length === 0) {
22981
+ throw new Error("Video timing recovery requires a valid source cadence");
22982
+ }
22983
+ for (let index = 1;index < repairIndexes.length; index += 1) {
22984
+ if (repairIndexes[index] - repairIndexes[index - 1] === 1) {
22985
+ throw new Error("Video timing corruption is not isolated");
22986
+ }
22987
+ }
22988
+ const nominalDurationSeconds = resolveMedian(validDurations);
22989
+ for (const index of repairIndexes) {
22990
+ packetTimings[index].duration = nominalDurationSeconds;
22991
+ packetTimings[index].isDurationRepaired = true;
22992
+ }
22993
+ let repairedTimestampCount = 0;
22994
+ const maximumBoundaryRegression = Math.max(MINIMUM_BOUNDARY_REGRESSION_TOLERANCE_SECONDS, nominalDurationSeconds * MAXIMUM_BOUNDARY_REGRESSION_MULTIPLIER);
22995
+ for (const index of repairIndexes) {
22996
+ const boundaryIndex = index + 1;
22997
+ if (boundaryIndex >= packetTimings.length) {
22998
+ continue;
22999
+ }
23000
+ const repairedPacket = packetTimings[index];
23001
+ const boundaryPacket = packetTimings[boundaryIndex];
23002
+ if (boundaryPacket.timestamp >= repairedPacket.timestamp) {
23003
+ continue;
23004
+ }
23005
+ const regression = repairedPacket.timestamp - boundaryPacket.timestamp;
23006
+ if (regression > maximumBoundaryRegression) {
23007
+ throw new Error("Video timing boundary regression is not recoverable");
23008
+ }
23009
+ boundaryPacket.timestamp = repairedPacket.timestamp + repairedPacket.duration;
23010
+ boundaryPacket.isTimestampRepaired = true;
23011
+ repairedTimestampCount += 1;
23012
+ }
23013
+ return {
23014
+ nominalDurationSeconds,
23015
+ packetTimings,
23016
+ repairedDurationCount: repairIndexes.length,
23017
+ repairedTimestampCount
23018
+ };
23019
+ }
23020
+ async function readVideoPacketTimings(videoTrack) {
23021
+ const packets = [];
23022
+ const sink = new EncodedPacketSink(videoTrack);
23023
+ for await (const packet of sink.packets(undefined, undefined, {
23024
+ metadataOnly: true
23025
+ })) {
23026
+ packets.push({
23027
+ duration: packet.duration,
23028
+ timestamp: packet.timestamp
23029
+ });
23030
+ }
23031
+ return packets;
23032
+ }
23033
+ async function addVideoTrack(output, videoTrack, videoSource) {
23034
+ const name = await videoTrack.getName();
23035
+ const metadata = {
23036
+ languageCode: await videoTrack.getLanguageCode(),
23037
+ rotation: await videoTrack.getRotation()
23038
+ };
23039
+ if (name !== null) {
23040
+ metadata.name = name;
23041
+ }
23042
+ output.addVideoTrack(videoSource, metadata);
23043
+ }
23044
+ async function addAudioTrack(output, audioTrack, audioSource) {
23045
+ const name = await audioTrack.getName();
23046
+ const metadata = {
23047
+ languageCode: await audioTrack.getLanguageCode()
23048
+ };
23049
+ if (name !== null) {
23050
+ metadata.name = name;
23051
+ }
23052
+ output.addAudioTrack(audioSource, metadata);
23053
+ }
23054
+ async function writeRepairedVideoPackets(videoTrack, videoSource, plan) {
23055
+ const decoderConfig = await videoTrack.getDecoderConfig();
23056
+ const metadata = {};
23057
+ if (decoderConfig !== null) {
23058
+ metadata.decoderConfig = decoderConfig;
23059
+ }
23060
+ let packetIndex = 0;
23061
+ const sink = new EncodedPacketSink(videoTrack);
23062
+ for await (const packet of sink.packets(undefined, undefined, {
23063
+ verifyKeyPackets: true
23064
+ })) {
23065
+ const timing = plan.packetTimings[packetIndex];
23066
+ if (timing === undefined) {
23067
+ throw new Error("Video packet count changed during timing repair");
23068
+ }
23069
+ const outputPacket = timing.isDurationRepaired || timing.isTimestampRepaired ? packet.clone({
23070
+ duration: timing.duration,
23071
+ timestamp: timing.timestamp
23072
+ }) : packet;
23073
+ await videoSource.add(outputPacket, metadata);
23074
+ packetIndex += 1;
23075
+ }
23076
+ if (packetIndex !== plan.packetTimings.length) {
23077
+ throw new Error("Video packet count changed during timing repair");
23078
+ }
23079
+ videoSource.close();
23080
+ }
23081
+ async function writeAudioPackets(audioTrack, audioSource) {
23082
+ const decoderConfig = await audioTrack.getDecoderConfig();
23083
+ const metadata = {};
23084
+ if (decoderConfig !== null) {
23085
+ metadata.decoderConfig = decoderConfig;
23086
+ }
23087
+ const sink = new EncodedPacketSink(audioTrack);
23088
+ for await (const packet of sink.packets()) {
23089
+ await audioSource.add(packet, metadata);
23090
+ }
23091
+ audioSource.close();
23092
+ }
23093
+ async function remuxRepairedVideo(input, videoTrack, plan) {
23094
+ const videoCodec = await videoTrack.getCodec();
23095
+ if (videoCodec === null) {
23096
+ throw new Error("Timing repair requires a supported video codec");
23097
+ }
23098
+ const audioTrack = await input.getPrimaryAudioTrack();
23099
+ let audioCodec = null;
23100
+ if (audioTrack !== null) {
23101
+ audioCodec = await audioTrack.getCodec();
23102
+ if (audioCodec === null) {
23103
+ throw new Error("Timing repair requires a supported audio codec");
23104
+ }
23105
+ }
23106
+ const target = new BufferTarget2;
23107
+ const output = new Output2({
23108
+ format: new Mp4OutputFormat2({ fastStart: "in-memory" }),
23109
+ target
23110
+ });
23111
+ const videoSource = new EncodedVideoPacketSource(videoCodec);
23112
+ await addVideoTrack(output, videoTrack, videoSource);
23113
+ let audioSource = null;
23114
+ if (audioTrack !== null && audioCodec !== null) {
23115
+ audioSource = new EncodedAudioPacketSource(audioCodec);
23116
+ await addAudioTrack(output, audioTrack, audioSource);
23117
+ }
23118
+ await output.start();
23119
+ const writers = [
23120
+ writeRepairedVideoPackets(videoTrack, videoSource, plan)
23121
+ ];
23122
+ if (audioTrack !== null && audioSource !== null) {
23123
+ writers.push(writeAudioPackets(audioTrack, audioSource));
23124
+ }
23125
+ await Promise.all(writers);
23126
+ await output.finalize();
23127
+ if (target.buffer === null) {
23128
+ throw new Error("Timing repair did not produce an output buffer");
23129
+ }
23130
+ return new Blob([target.buffer], { type: "video/mp4" });
23131
+ }
23132
+ async function repairDeferredVideoTiming(blob, expectedDurationSeconds) {
23133
+ const input = new Input3({
23134
+ formats: ALL_FORMATS2,
23135
+ source: new BlobSource3(blob)
23136
+ });
23137
+ try {
23138
+ const videoTrack = await input.getPrimaryVideoTrack();
23139
+ if (videoTrack === null) {
23140
+ return {
23141
+ blob,
23142
+ diagnostics: {
23143
+ deferredTimingRepairedDurationCount: 0,
23144
+ deferredTimingRepairedTimestampCount: 0
23145
+ }
23146
+ };
23147
+ }
23148
+ const packetTimings = await readVideoPacketTimings(videoTrack);
23149
+ const plan = createDeferredVideoTimingRepairPlan(packetTimings, expectedDurationSeconds);
23150
+ const diagnostics = {
23151
+ deferredTimingRepairedDurationCount: plan.repairedDurationCount,
23152
+ deferredTimingRepairedTimestampCount: plan.repairedTimestampCount
23153
+ };
23154
+ if (plan.nominalDurationSeconds !== null) {
23155
+ diagnostics.deferredTimingNominalDurationSeconds = plan.nominalDurationSeconds;
23156
+ }
23157
+ if (plan.repairedDurationCount === 0) {
23158
+ return { blob, diagnostics };
23159
+ }
23160
+ return {
23161
+ blob: await remuxRepairedVideo(input, videoTrack, plan),
23162
+ diagnostics
23163
+ };
23164
+ } finally {
23165
+ input.dispose();
23166
+ }
23167
+ }
23168
+
23169
+ // src/core/transcode/deferred-video-transcoder.ts
22795
23170
  var MINIMUM_FRAME_DENSITY_RATIO = 0.9;
22796
23171
  var MAXIMUM_DURATION_DIFFERENCE_RATIO = 0.05;
22797
23172
  var MINIMUM_DURATION_TOLERANCE_SECONDS = 0.25;
22798
23173
  var MAXIMUM_FRAME_GAP_MULTIPLIER = 5;
22799
23174
  var MINIMUM_FRAME_GAP_TOLERANCE_SECONDS = 0.25;
23175
+ var LONG_RECORDING_THRESHOLD_SECONDS = 3;
23176
+ var LONG_RECORDING_DURATION_TOLERANCE_SECONDS = 1.5;
23177
+ var LONG_RECORDING_DURATION_TOLERANCE_RATIO = 0.15;
23178
+ var SHORT_RECORDING_DURATION_TOLERANCE_SECONDS = 0.75;
23179
+ var SHORT_RECORDING_DURATION_TOLERANCE_RATIO = 0.5;
22800
23180
  var INTEGRITY_ERROR_CODE = "recording.deferred-transcode-integrity";
22801
- function createIntegrityError(message) {
23181
+ function createIntegrityError(message, diagnostics = {}) {
22802
23182
  const error = new Error(message);
22803
23183
  error.name = "DeferredTranscodeIntegrityError";
22804
23184
  error.code = INTEGRITY_ERROR_CODE;
22805
23185
  error.isRecoverable = true;
23186
+ Object.assign(error, diagnostics);
22806
23187
  return error;
22807
23188
  }
22808
- function validatePacketSummary(summary, label) {
23189
+ function validatePacketSummary(summary, label, diagnostics) {
22809
23190
  if (!Number.isFinite(summary.averagePacketRate)) {
22810
- throw createIntegrityError(`${label} video packet rate is invalid`);
23191
+ throw createIntegrityError(`${label} video packet rate is invalid`, diagnostics);
22811
23192
  }
22812
23193
  if (!Number.isFinite(summary.duration)) {
22813
- throw createIntegrityError(`${label} video duration is invalid`);
23194
+ throw createIntegrityError(`${label} video duration is invalid`, diagnostics);
22814
23195
  }
22815
23196
  if (!Number.isFinite(summary.maximumPacketGap)) {
22816
- throw createIntegrityError(`${label} video maximum packet gap is invalid`);
23197
+ throw createIntegrityError(`${label} video maximum packet gap is invalid`, diagnostics);
22817
23198
  }
22818
23199
  if (summary.averagePacketRate <= 0 || summary.duration <= 0) {
22819
- throw createIntegrityError(`${label} video packet statistics are empty`);
23200
+ throw createIntegrityError(`${label} video packet statistics are empty`, diagnostics);
22820
23201
  }
22821
23202
  if (summary.maximumPacketGap < 0) {
22822
- throw createIntegrityError(`${label} video maximum packet gap is invalid`);
23203
+ throw createIntegrityError(`${label} video maximum packet gap is invalid`, diagnostics);
22823
23204
  }
22824
23205
  if (!Number.isInteger(summary.packetCount) || summary.packetCount <= 0) {
22825
- throw createIntegrityError(`${label} video packet count is invalid`);
23206
+ throw createIntegrityError(`${label} video packet count is invalid`, diagnostics);
23207
+ }
23208
+ }
23209
+ function validateMaximumFrameGap(summary, label, targetFrameRate, diagnostics) {
23210
+ const maximumPacketGap = Math.max(MINIMUM_FRAME_GAP_TOLERANCE_SECONDS, MAXIMUM_FRAME_GAP_MULTIPLIER / targetFrameRate);
23211
+ if (summary.maximumPacketGap > maximumPacketGap) {
23212
+ throw createIntegrityError(`${label} video contains a material frame gap`, diagnostics);
22826
23213
  }
22827
23214
  }
22828
- function validateFrameCadence(summary, label, targetFrameRate) {
23215
+ function validateOutputFrameDensity(summary, targetFrameRate, diagnostics) {
22829
23216
  const minimumPacketRate = targetFrameRate * MINIMUM_FRAME_DENSITY_RATIO;
22830
23217
  if (summary.averagePacketRate < minimumPacketRate) {
22831
- throw createIntegrityError(`${label} video frame density is below the capture target`);
23218
+ throw createIntegrityError("Output video frame density is below the capture target", diagnostics);
22832
23219
  }
22833
- const maximumPacketGap = Math.max(MINIMUM_FRAME_GAP_TOLERANCE_SECONDS, MAXIMUM_FRAME_GAP_MULTIPLIER / targetFrameRate);
22834
- if (summary.maximumPacketGap > maximumPacketGap) {
22835
- throw createIntegrityError(`${label} video contains a material frame gap`);
23220
+ }
23221
+ function buildIntegrityDiagnostics(validation, additionalDiagnostics = {}) {
23222
+ let stage = "output";
23223
+ if (validation.stage !== undefined) {
23224
+ stage = validation.stage;
23225
+ }
23226
+ const diagnostics = {
23227
+ ...additionalDiagnostics,
23228
+ deferredIntegrityStage: stage
23229
+ };
23230
+ assignFiniteDiagnostic(diagnostics, "mediaRecorderExpectedDurationSeconds", validation.expectedDurationSeconds);
23231
+ if (validation.source !== null) {
23232
+ assignFiniteDiagnostic(diagnostics, "deferredSourceDurationSeconds", validation.source.duration);
23233
+ assignFiniteDiagnostic(diagnostics, "deferredSourcePacketCount", validation.source.packetCount);
23234
+ assignFiniteDiagnostic(diagnostics, "deferredSourcePacketRate", validation.source.averagePacketRate);
23235
+ assignFiniteDiagnostic(diagnostics, "deferredSourceMaximumPacketGap", validation.source.maximumPacketGap);
23236
+ }
23237
+ if (validation.output !== null) {
23238
+ assignFiniteDiagnostic(diagnostics, "deferredOutputDurationSeconds", validation.output.duration);
23239
+ assignFiniteDiagnostic(diagnostics, "deferredOutputPacketCount", validation.output.packetCount);
23240
+ assignFiniteDiagnostic(diagnostics, "deferredOutputPacketRate", validation.output.averagePacketRate);
23241
+ assignFiniteDiagnostic(diagnostics, "deferredOutputMaximumPacketGap", validation.output.maximumPacketGap);
22836
23242
  }
23243
+ return diagnostics;
22837
23244
  }
22838
- function validateDeferredVideoStats(validation) {
23245
+ function assignFiniteDiagnostic(diagnostics, key, value) {
23246
+ if (Number.isFinite(value)) {
23247
+ diagnostics[key] = value;
23248
+ }
23249
+ }
23250
+ function resolveLifecycleDurationTolerance(expectedDurationSeconds) {
23251
+ if (expectedDurationSeconds >= LONG_RECORDING_THRESHOLD_SECONDS) {
23252
+ return Math.max(LONG_RECORDING_DURATION_TOLERANCE_SECONDS, expectedDurationSeconds * LONG_RECORDING_DURATION_TOLERANCE_RATIO);
23253
+ }
23254
+ return Math.max(SHORT_RECORDING_DURATION_TOLERANCE_SECONDS, expectedDurationSeconds * SHORT_RECORDING_DURATION_TOLERANCE_RATIO);
23255
+ }
23256
+ function validateLifecycleCoverage(summary, label, expectedDurationSeconds, diagnostics) {
23257
+ const durationDifference = Math.abs(summary.duration - expectedDurationSeconds);
23258
+ const tolerance = resolveLifecycleDurationTolerance(expectedDurationSeconds);
23259
+ if (durationDifference > tolerance) {
23260
+ throw createIntegrityError(`${label} video duration does not cover the active recording`, diagnostics);
23261
+ }
23262
+ }
23263
+ function validateDeferredVideoStatsWithDiagnostics(validation, additionalDiagnostics) {
23264
+ const diagnostics = buildIntegrityDiagnostics(validation, additionalDiagnostics);
23265
+ if (!Number.isFinite(validation.expectedDurationSeconds) || validation.expectedDurationSeconds < 0) {
23266
+ throw createIntegrityError("Deferred transcode requires a valid active recording duration", diagnostics);
23267
+ }
22839
23268
  if (!Number.isFinite(validation.targetFrameRate) || validation.targetFrameRate <= 0) {
22840
- throw createIntegrityError("Deferred transcode requires a valid capture frame rate");
23269
+ throw createIntegrityError("Deferred transcode requires a valid capture frame rate", diagnostics);
22841
23270
  }
22842
- if (!(validation.source && validation.output)) {
22843
- throw createIntegrityError("Deferred transcode source or output has no primary video track");
23271
+ if (validation.source === null) {
23272
+ throw createIntegrityError("Deferred transcode source has no primary video track", diagnostics);
22844
23273
  }
22845
- validatePacketSummary(validation.source, "Source");
22846
- validatePacketSummary(validation.output, "Output");
22847
- validateFrameCadence(validation.source, "Source", validation.targetFrameRate);
22848
- validateFrameCadence(validation.output, "Output", validation.targetFrameRate);
23274
+ validatePacketSummary(validation.source, "Source", diagnostics);
23275
+ validateMaximumFrameGap(validation.source, "Source", validation.targetFrameRate, diagnostics);
23276
+ validateLifecycleCoverage(validation.source, "Source", validation.expectedDurationSeconds, diagnostics);
23277
+ if (validation.stage === "source") {
23278
+ return;
23279
+ }
23280
+ if (validation.output === null) {
23281
+ throw createIntegrityError("Deferred transcode output has no primary video track", diagnostics);
23282
+ }
23283
+ validatePacketSummary(validation.output, "Output", diagnostics);
23284
+ validateOutputFrameDensity(validation.output, validation.targetFrameRate, diagnostics);
23285
+ validateMaximumFrameGap(validation.output, "Output", validation.targetFrameRate, diagnostics);
23286
+ validateLifecycleCoverage(validation.output, "Output", validation.expectedDurationSeconds, diagnostics);
22849
23287
  const durationDifference = Math.abs(validation.source.duration - validation.output.duration);
22850
23288
  const durationTolerance = Math.max(MINIMUM_DURATION_TOLERANCE_SECONDS, validation.source.duration * MAXIMUM_DURATION_DIFFERENCE_RATIO);
22851
23289
  if (durationDifference > durationTolerance) {
22852
- throw createIntegrityError("Deferred transcode output duration differs materially from source");
23290
+ throw createIntegrityError("Deferred transcode output duration differs materially from source", diagnostics);
22853
23291
  }
22854
23292
  const minimumPacketRate = validation.targetFrameRate * MINIMUM_FRAME_DENSITY_RATIO;
22855
23293
  const minimumPacketCount = Math.floor(validation.source.duration * minimumPacketRate);
22856
23294
  if (validation.output.averagePacketRate < minimumPacketRate || validation.output.packetCount < minimumPacketCount) {
22857
- throw createIntegrityError("Deferred transcode output lost material video frame density");
23295
+ throw createIntegrityError("Deferred transcode output lost material video frame density", diagnostics);
22858
23296
  }
22859
23297
  }
22860
23298
  async function inspectVideoPackets(blob) {
22861
- const input = new Input3({
22862
- formats: ALL_FORMATS2,
22863
- source: new BlobSource3(blob)
23299
+ const input = new Input4({
23300
+ formats: ALL_FORMATS3,
23301
+ source: new BlobSource4(blob)
22864
23302
  });
22865
23303
  const videoTrack = await input.getPrimaryVideoTrack();
22866
23304
  if (videoTrack === null) {
@@ -22868,7 +23306,7 @@ async function inspectVideoPackets(blob) {
22868
23306
  return null;
22869
23307
  }
22870
23308
  try {
22871
- const packetSink = new EncodedPacketSink(videoTrack);
23309
+ const packetSink = new EncodedPacketSink2(videoTrack);
22872
23310
  const timestamps = [];
22873
23311
  let firstTimestamp = Number.POSITIVE_INFINITY;
22874
23312
  let lastEndTimestamp = Number.NEGATIVE_INFINITY;
@@ -22897,27 +23335,86 @@ async function inspectVideoPackets(blob) {
22897
23335
  input.dispose();
22898
23336
  }
22899
23337
  }
22900
- async function inspectDeferredVideo(blob, label) {
23338
+ async function inspectDeferredVideo(blob, label, diagnostics) {
22901
23339
  return await inspectVideoPackets(blob).catch((error) => {
22902
23340
  const message = extractErrorMessage(error);
22903
- throw createIntegrityError(`Could not inspect ${label} deferred video: ${message}`);
23341
+ throw createIntegrityError(`Could not inspect ${label} deferred video: ${message}`, diagnostics);
23342
+ });
23343
+ }
23344
+ async function repairDeferredVideoSource(input, expectedDurationSeconds) {
23345
+ return await repairDeferredVideoTiming(input, expectedDurationSeconds).catch((error) => {
23346
+ const message = extractErrorMessage(error);
23347
+ throw createIntegrityError(`Could not repair deferred video timing: ${message}`, {
23348
+ deferredIntegrityStage: "source-repair",
23349
+ mediaRecorderExpectedDurationSeconds: expectedDurationSeconds
23350
+ });
22904
23351
  });
22905
23352
  }
22906
- async function transcodeDeferredVideo(input, config, onProgress) {
23353
+ async function transcodeDeferredVideo(input, config, onProgress, context) {
22907
23354
  const targetFrameRate = config.fps;
22908
23355
  if (typeof targetFrameRate !== "number" || !Number.isFinite(targetFrameRate) || targetFrameRate <= 0) {
22909
- throw createIntegrityError("Deferred transcode requires a valid capture frame rate");
23356
+ throw createIntegrityError("Deferred transcode requires a valid capture frame rate", {
23357
+ deferredIntegrityStage: "source",
23358
+ mediaRecorderExpectedDurationSeconds: context.expectedDurationSeconds
23359
+ });
22910
23360
  }
22911
- const source = await inspectDeferredVideo(input, "source");
22912
- validateDeferredVideoStats({
22913
- output: source,
23361
+ const timingRepair = await repairDeferredVideoSource(input, context.expectedDurationSeconds);
23362
+ const source = await inspectDeferredVideo(timingRepair.blob, "source", {
23363
+ deferredIntegrityStage: "source",
23364
+ mediaRecorderExpectedDurationSeconds: context.expectedDurationSeconds,
23365
+ ...timingRepair.diagnostics
23366
+ });
23367
+ validateDeferredVideoStatsWithDiagnostics({
23368
+ expectedDurationSeconds: context.expectedDurationSeconds,
23369
+ output: null,
22914
23370
  source,
23371
+ stage: "source",
23372
+ targetFrameRate
23373
+ }, timingRepair.diagnostics);
23374
+ const transcodeDiagnostics = buildIntegrityDiagnostics({
23375
+ expectedDurationSeconds: context.expectedDurationSeconds,
23376
+ output: null,
23377
+ source,
23378
+ stage: "source",
22915
23379
  targetFrameRate
22916
23380
  });
22917
- const result = await transcodeVideo(input, config, onProgress);
22918
- const output = await inspectDeferredVideo(result.blob, "output");
22919
- validateDeferredVideoStats({ output, source, targetFrameRate });
22920
- return result;
23381
+ transcodeDiagnostics.deferredIntegrityStage = "transcode";
23382
+ Object.assign(transcodeDiagnostics, timingRepair.diagnostics);
23383
+ const result = await transcodeVideo(timingRepair.blob, config, onProgress).catch((error) => {
23384
+ let transcodeError;
23385
+ if (error instanceof Error) {
23386
+ transcodeError = error;
23387
+ } else {
23388
+ transcodeError = new Error(extractErrorMessage(error));
23389
+ }
23390
+ Object.assign(transcodeError, transcodeDiagnostics);
23391
+ throw transcodeError;
23392
+ });
23393
+ const outputDiagnostics = {
23394
+ deferredIntegrityStage: "output",
23395
+ mediaRecorderExpectedDurationSeconds: context.expectedDurationSeconds,
23396
+ ...timingRepair.diagnostics
23397
+ };
23398
+ if (source !== null) {
23399
+ outputDiagnostics.deferredSourceDurationSeconds = source.duration;
23400
+ outputDiagnostics.deferredSourcePacketCount = source.packetCount;
23401
+ outputDiagnostics.deferredSourcePacketRate = source.averagePacketRate;
23402
+ outputDiagnostics.deferredSourceMaximumPacketGap = source.maximumPacketGap;
23403
+ }
23404
+ const output = await inspectDeferredVideo(result.blob, "output", outputDiagnostics);
23405
+ const validation = {
23406
+ expectedDurationSeconds: context.expectedDurationSeconds,
23407
+ output,
23408
+ source,
23409
+ stage: "output",
23410
+ targetFrameRate
23411
+ };
23412
+ validateDeferredVideoStatsWithDiagnostics(validation, timingRepair.diagnostics);
23413
+ const diagnostics = buildIntegrityDiagnostics(validation, timingRepair.diagnostics);
23414
+ return {
23415
+ ...result,
23416
+ diagnostics
23417
+ };
22921
23418
  }
22922
23419
 
22923
23420
  // src/core/recording/recording-manager.ts
@@ -23171,7 +23668,8 @@ class RecordingManager {
23171
23668
  this.callbacks.onStopAudioTracking();
23172
23669
  logger.debug("[RecordingManager] Stopping recording in stream manager");
23173
23670
  const stopResult = await this.streamManager.stopRecording();
23174
- const finalBlob = await this.resolveFinalRecordingBlob(stopResult.blob, stopResult.captureFrameRate);
23671
+ const finalResult = await this.resolveFinalRecordingBlob(stopResult.blob, stopResult.captureFrameRate, stopResult.expectedDurationSeconds);
23672
+ const finalBlob = finalResult.blob;
23175
23673
  logger.info("[RecordingManager] Recording stopped, blob size:", finalBlob.size);
23176
23674
  this.recordingState = RECORDING_STATE_IDLE;
23177
23675
  this.callbacks.onStateChange(this.recordingState);
@@ -23193,6 +23691,9 @@ class RecordingManager {
23193
23691
  if (stopResult.mediaRecorderDiagnostics !== undefined) {
23194
23692
  Object.assign(telemetryProperties, stopResult.mediaRecorderDiagnostics);
23195
23693
  }
23694
+ if (finalResult.diagnostics !== undefined) {
23695
+ Object.assign(telemetryProperties, finalResult.diagnostics);
23696
+ }
23196
23697
  return {
23197
23698
  blob: finalBlob,
23198
23699
  telemetryProperties
@@ -23273,13 +23774,16 @@ class RecordingManager {
23273
23774
  this.recordingStartTime = null;
23274
23775
  this.maxTimeRemaining = null;
23275
23776
  }
23276
- async resolveFinalRecordingBlob(blob, captureFrameRate) {
23777
+ async resolveFinalRecordingBlob(blob, captureFrameRate, expectedDurationSeconds) {
23277
23778
  if (this.activeRouteDecision.route !== "safe-capture-post-recording-transcode") {
23278
- return blob;
23779
+ return { blob };
23279
23780
  }
23280
23781
  if (!this.activeRecordingConfig) {
23281
23782
  throw new Error("Recording config unavailable for final video processing");
23282
23783
  }
23784
+ if (typeof expectedDurationSeconds !== "number" || !Number.isFinite(expectedDurationSeconds) || expectedDurationSeconds < 0) {
23785
+ throw new Error("Recording active duration unavailable for final video processing");
23786
+ }
23283
23787
  const transcodeConfig = {
23284
23788
  ...this.activeRecordingConfig,
23285
23789
  fps: this.resolveDeferredCaptureFrameRate(captureFrameRate),
@@ -23288,9 +23792,12 @@ class RecordingManager {
23288
23792
  this.callbacks.onTranscodingProgress?.(0);
23289
23793
  const result = await this.dependencies.transcodeVideo(blob, transcodeConfig, (progress) => {
23290
23794
  this.callbacks.onTranscodingProgress?.(progress);
23291
- });
23795
+ }, { expectedDurationSeconds });
23292
23796
  this.callbacks.onTranscodingProgress?.(1);
23293
- return result.blob;
23797
+ return {
23798
+ blob: result.blob,
23799
+ diagnostics: result.diagnostics
23800
+ };
23294
23801
  }
23295
23802
  resolveDeferredCaptureFrameRate(captureFrameRate) {
23296
23803
  if (typeof captureFrameRate === "number" && Number.isFinite(captureFrameRate) && captureFrameRate > 0) {
@@ -24743,79 +25250,79 @@ class VidtreoRecorder {
24743
25250
  }
24744
25251
  }
24745
25252
  export {
24746
- validateFile,
24747
- validateBrowserSupport,
24748
- transcodeVideoForNativeCamera,
24749
- transcodeVideo,
24750
- resolveDeviceError,
24751
- requireStream,
24752
- requireProcessor,
24753
- requireNonNull,
24754
- requireMediaDevices,
24755
- requireInitialized,
24756
- requireDefined,
24757
- requireActive,
24758
- recoverPendingUpload,
24759
- parseBrowserErrorLinkContent,
24760
- mapPresetToConfig,
24761
- logger,
24762
- isMobileDevice2 as isMobileDevice,
24763
- isAudioRuntimeMissingCode,
24764
- isAudioMissingError,
24765
- getPresetVideoBitrateForFormat,
24766
- getPresetTotalBitrate,
24767
- getPresetAudioBitrateForFormat,
24768
- getFormatCompatibilityPolicy,
24769
- getDefaultConfigForFormat,
24770
- getDefaultAudioCodecForFormat,
24771
- getCameraErrorText,
24772
- getBrowserName,
24773
- getBrowserInfo,
24774
- getBrowserErrorText,
24775
- getAudioErrorText,
24776
- getAudioCodecForFormat,
24777
- formatTime,
24778
- formatFileSize,
24779
- extractVideoDuration,
24780
- extractLastFrame,
24781
- extractErrorMessage,
24782
- evaluateAudioHealth,
24783
- createPermissionFlowOrchestratorDependencies,
24784
- createAudioNoTrackAtStartError,
24785
- createAudioMissingError,
24786
- checkRecorderSupport,
24787
- calculateVideoBitrate,
24788
- calculateTotalBitrateFromMbPerMinute,
24789
- calculateBarColor,
24790
- VidtreoRecorder,
24791
- VideoUploadService,
24792
- VideoStorageService,
24793
- UploadQueueManager,
24794
- StreamProcessor,
24795
- StorageManager,
24796
- RecordingManager,
24797
- RecorderController,
24798
- QuotaManager,
24799
- PermissionFlowOrchestrator,
24800
- PRESET_SIZE_LIMIT_MB_PER_MINUTE,
24801
- NativeCameraHandler,
24802
- FORMAT_DEFAULT_CODECS,
24803
- FORMAT_COMPATIBILITY_POLICY,
24804
- ERROR_RECORDING_NO_AUDIO_TRACK,
24805
- ERROR_RECORDING_NO_AUDIO_FRAMES,
24806
- ERROR_RECORDING_AUDIO_ZERO_CHANNELS,
24807
- ERROR_RECORDING_AUDIO_TRACK_ENDED,
24808
- ERROR_RECORDING_AUDIO_SILENT,
24809
- ERROR_RECORDING_AUDIO_NO_CHUNKS,
24810
- ERROR_RECORDING_AUDIO_INVALID_SAMPLE_RATE,
24811
- ERROR_AUDIO_NO_TRACK_AT_START,
24812
- DeviceManager,
24813
- DEFAULT_TRANSCODE_CONFIG,
24814
- DEFAULT_STREAM_CONFIG,
24815
- DEFAULT_RECORDING_OPTIONS,
24816
- DEFAULT_CAMERA_CONSTRAINTS,
24817
- ConfigService,
24818
- ConfigManager,
25253
+ AudioLevelAnalyzer,
24819
25254
  CameraStreamManager,
24820
- AudioLevelAnalyzer
25255
+ ConfigManager,
25256
+ ConfigService,
25257
+ DEFAULT_CAMERA_CONSTRAINTS,
25258
+ DEFAULT_RECORDING_OPTIONS,
25259
+ DEFAULT_STREAM_CONFIG,
25260
+ DEFAULT_TRANSCODE_CONFIG,
25261
+ DeviceManager,
25262
+ ERROR_AUDIO_NO_TRACK_AT_START,
25263
+ ERROR_RECORDING_AUDIO_INVALID_SAMPLE_RATE,
25264
+ ERROR_RECORDING_AUDIO_NO_CHUNKS,
25265
+ ERROR_RECORDING_AUDIO_SILENT,
25266
+ ERROR_RECORDING_AUDIO_TRACK_ENDED,
25267
+ ERROR_RECORDING_AUDIO_ZERO_CHANNELS,
25268
+ ERROR_RECORDING_NO_AUDIO_FRAMES,
25269
+ ERROR_RECORDING_NO_AUDIO_TRACK,
25270
+ FORMAT_COMPATIBILITY_POLICY,
25271
+ FORMAT_DEFAULT_CODECS,
25272
+ NativeCameraHandler,
25273
+ PRESET_SIZE_LIMIT_MB_PER_MINUTE,
25274
+ PermissionFlowOrchestrator,
25275
+ QuotaManager,
25276
+ RecorderController,
25277
+ RecordingManager,
25278
+ StorageManager,
25279
+ StreamProcessor,
25280
+ UploadQueueManager,
25281
+ VideoStorageService,
25282
+ VideoUploadService,
25283
+ VidtreoRecorder,
25284
+ calculateBarColor,
25285
+ calculateTotalBitrateFromMbPerMinute,
25286
+ calculateVideoBitrate,
25287
+ checkRecorderSupport,
25288
+ createAudioMissingError,
25289
+ createAudioNoTrackAtStartError,
25290
+ createPermissionFlowOrchestratorDependencies,
25291
+ evaluateAudioHealth,
25292
+ extractErrorMessage,
25293
+ extractLastFrame,
25294
+ extractVideoDuration,
25295
+ formatFileSize,
25296
+ formatTime,
25297
+ getAudioCodecForFormat,
25298
+ getAudioErrorText,
25299
+ getBrowserErrorText,
25300
+ getBrowserInfo,
25301
+ getBrowserName,
25302
+ getCameraErrorText,
25303
+ getDefaultAudioCodecForFormat,
25304
+ getDefaultConfigForFormat,
25305
+ getFormatCompatibilityPolicy,
25306
+ getPresetAudioBitrateForFormat,
25307
+ getPresetTotalBitrate,
25308
+ getPresetVideoBitrateForFormat,
25309
+ isAudioMissingError,
25310
+ isAudioRuntimeMissingCode,
25311
+ isMobileDevice2 as isMobileDevice,
25312
+ logger,
25313
+ mapPresetToConfig,
25314
+ parseBrowserErrorLinkContent,
25315
+ recoverPendingUpload,
25316
+ requireActive,
25317
+ requireDefined,
25318
+ requireInitialized,
25319
+ requireMediaDevices,
25320
+ requireNonNull,
25321
+ requireProcessor,
25322
+ requireStream,
25323
+ resolveDeviceError,
25324
+ transcodeVideo,
25325
+ transcodeVideoForNativeCamera,
25326
+ validateBrowserSupport,
25327
+ validateFile
24821
25328
  };