@vidtreo/recorder 1.7.9 → 1.7.11

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
@@ -194,7 +194,9 @@ export type MediaStats = {
194
194
  videoDuration: number;
195
195
  };
196
196
  export declare function inspectMedia(blob: Blob): Promise<MediaStats>;
197
- export declare function transcodePreservingCadence(blob: Blob): Promise<Blob>;
197
+ export declare function transcodePreservingCadence(blob: Blob, expectedDurationSeconds: number): Promise<Blob>;
198
+
199
+ export {};
198
200
 
199
201
  export {};
200
202
 
@@ -1557,6 +1559,8 @@ import type { TranscodeConfig } from "../transcode/transcode-types";
1557
1559
  import { type VisibilityInterval } from "../utils/tab-visibility-tracker";
1558
1560
  export type StopRecordingResult = {
1559
1561
  blob: Blob;
1562
+ captureFrameRate?: number;
1563
+ expectedDurationSeconds?: number;
1560
1564
  tabVisibilityIntervals: VisibilityInterval[];
1561
1565
  recordingStats?: StreamRecordingStats;
1562
1566
  encoderAcceleration?: VideoHardwareAcceleration;
@@ -1587,6 +1591,12 @@ export declare class StreamRecordingState {
1587
1591
  private mediaRecorderStopResolve;
1588
1592
  private mediaRecorderStopReject;
1589
1593
  private mediaRecorderError;
1594
+ private safeCaptureFrameRate;
1595
+ private safeCaptureStartTime;
1596
+ private safeCaptureExpectedDurationSeconds;
1597
+ private mediaRecorderRequestedMimeType;
1598
+ private mediaRecorderGeneration;
1599
+ private isSafeCaptureSettled;
1590
1600
  private mediaRecorderErrorTimeoutId;
1591
1601
  private readonly streamManager;
1592
1602
  private readonly dependencies;
@@ -1610,19 +1620,25 @@ export declare class StreamRecordingState {
1610
1620
  private clearRecordingTimer;
1611
1621
  private clearBufferSizeInterval;
1612
1622
  private resetRecordingState;
1623
+ private resolveActiveRecordingDurationSeconds;
1613
1624
  private startSafeCaptureRecording;
1625
+ private isActiveSafeCaptureGeneration;
1626
+ private createSafeCaptureRecorder;
1627
+ private resolveEffectiveSafeCaptureFrameRate;
1614
1628
  private applySafeCaptureTrackConstraints;
1615
1629
  private resolveSafeCaptureVideoConstraints;
1616
1630
  private stopSafeCaptureRecording;
1631
+ private cleanupSafeCaptureRecording;
1617
1632
  private resolveSafeCaptureMimeType;
1618
- private resolvePreferredSafeCaptureMimeType;
1619
- private shouldPreferWebmSafeCapture;
1633
+ private resolveRecordedMimeType;
1634
+ private resolveSafeCaptureMimeTypeCandidates;
1620
1635
  private scheduleMediaRecorderErrorFinalizeTimeout;
1621
1636
  private clearMediaRecorderErrorTimeout;
1622
1637
  private createMediaRecorderError;
1623
1638
  private createEmptyMediaRecorderError;
1624
1639
  private updateMediaRecorderErrorDiagnostics;
1625
1640
  private buildMediaRecorderDiagnostics;
1641
+ private assignSafeCaptureErrorDiagnostics;
1626
1642
  private getMediaRecorderChunkSize;
1627
1643
  private resetPauseState;
1628
1644
  private resolveTabVisibilityOverlayText;
@@ -1930,13 +1946,13 @@ export declare class RecorderController {
1930
1946
 
1931
1947
  import type { CameraStreamManager } from "../stream/stream";
1932
1948
  import { StreamProcessor } from "../stream/stream-processor";
1949
+ import { transcodeDeferredVideo } from "../transcode/deferred-video-transcoder";
1933
1950
  import type { TranscodeConfig } from "../transcode/transcode-types";
1934
- import { transcodeVideo } from "../transcode/video-transcoder";
1935
1951
  export type RecordingStopResult = {
1936
1952
  blob: Blob;
1937
1953
  telemetryProperties: Record<string, unknown>;
1938
1954
  };
1939
- type TranscodeVideoDependency = typeof transcodeVideo;
1955
+ type TranscodeVideoDependency = typeof transcodeDeferredVideo;
1940
1956
  type RecordingManagerDependencies = {
1941
1957
  transcodeVideo: TranscodeVideoDependency;
1942
1958
  };
@@ -1990,6 +2006,7 @@ export declare class RecordingManager {
1990
2006
  cleanup(): void;
1991
2007
  private resetRecordingState;
1992
2008
  private resolveFinalRecordingBlob;
2009
+ private resolveDeferredCaptureFrameRate;
1993
2010
  private resetPauseState;
1994
2011
  private getOrCreateStreamProcessor;
1995
2012
  private resolveRouteDecision;
@@ -3162,6 +3179,28 @@ export declare function getFileExtensionForOutputFormat(format: OutputFormat): s
3162
3179
  export declare function transcodeVideo(input: TranscodeInput, config?: Partial<TranscodeConfig>, onProgress?: (progress: number) => void): Promise<TranscodeResult>;
3163
3180
  export declare function transcodeVideoForNativeCamera(file: File, config?: Partial<TranscodeConfig>, onProgress?: (progress: number) => void): Promise<TranscodeResult>;
3164
3181
 
3182
+ export type VideoPacketSummary = {
3183
+ averagePacketRate: number;
3184
+ duration: number;
3185
+ maximumPacketGap: number;
3186
+ packetCount: number;
3187
+ };
3188
+ export type DeferredVideoStatsValidation = {
3189
+ expectedDurationSeconds: number;
3190
+ output: VideoPacketSummary | null;
3191
+ source: VideoPacketSummary | null;
3192
+ stage?: "source" | "output";
3193
+ targetFrameRate: number;
3194
+ };
3195
+ export type DeferredTranscodeContext = {
3196
+ expectedDurationSeconds: number;
3197
+ };
3198
+ export type DeferredTranscodeResult = TranscodeResult & {
3199
+ diagnostics: Record<string, string | number | boolean>;
3200
+ };
3201
+ export declare function validateDeferredVideoStats(validation: DeferredVideoStatsValidation): void;
3202
+ export declare function transcodeDeferredVideo(input: Blob, config: TranscodeConfig, onProgress: ((progress: number) => void) | undefined, context: DeferredTranscodeContext): Promise<DeferredTranscodeResult>;
3203
+
3165
3204
  export type VidtreoRecorderConfig = {
3166
3205
  apiKey: string;
3167
3206
  apiUrl?: string;
package/dist/index.js CHANGED
@@ -1890,6 +1890,9 @@ function createConversionOptions(config, optimizeForSpeed = false) {
1890
1890
  if (config.codec !== undefined) {
1891
1891
  video.codec = config.codec;
1892
1892
  }
1893
+ if (config.latencyMode !== undefined) {
1894
+ video.latencyMode = config.latencyMode;
1895
+ }
1893
1896
  if (optimizeForSpeed) {
1894
1897
  video.bitrateMode = "variable";
1895
1898
  video.latencyMode = "realtime";
@@ -5282,6 +5285,8 @@ var ANDROID_SAFE_CAPTURE_CONSTRAINTS = {
5282
5285
  height: { ideal: 720, max: 720 },
5283
5286
  frameRate: { ideal: 24, max: 24 }
5284
5287
  };
5288
+ var LOW_RESOURCE_SAFE_CAPTURE_FRAME_RATE = 20;
5289
+ var ANDROID_SAFE_CAPTURE_FRAME_RATE = 24;
5285
5290
  var SAFE_CAPTURE_TIMESLICE_MS = 1000;
5286
5291
  var MEDIA_RECORDER_ERROR_FINALIZE_TIMEOUT_MS = 5000;
5287
5292
  var ANDROID_POST_RECORDING_TRANSCODE_REASON = "android-post-recording-transcode";
@@ -5319,6 +5324,12 @@ class StreamRecordingState {
5319
5324
  mediaRecorderStopResolve = null;
5320
5325
  mediaRecorderStopReject = null;
5321
5326
  mediaRecorderError = null;
5327
+ safeCaptureFrameRate = null;
5328
+ safeCaptureStartTime = null;
5329
+ safeCaptureExpectedDurationSeconds = null;
5330
+ mediaRecorderRequestedMimeType = null;
5331
+ mediaRecorderGeneration = 0;
5332
+ isSafeCaptureSettled = false;
5322
5333
  mediaRecorderErrorTimeoutId = null;
5323
5334
  streamManager;
5324
5335
  dependencies;
@@ -5461,7 +5472,8 @@ class StreamRecordingState {
5461
5472
  this.startRecordingTimer();
5462
5473
  }
5463
5474
  async stopRecording() {
5464
- const recordingElapsedSeconds = (this.dependencies.getCurrentTimestamp() - this.recordingStartTime - this.totalPausedTime) / MILLISECONDS_PER_SECOND2;
5475
+ const stopTimestamp = this.dependencies.getCurrentTimestamp();
5476
+ const recordingElapsedSeconds = this.resolveActiveRecordingDurationSeconds(stopTimestamp);
5465
5477
  logger.debug("[StreamRecordingState] stopRecording called", {
5466
5478
  hasStreamProcessor: !!this.streamProcessor,
5467
5479
  isRecording: this.isRecording(),
@@ -5492,10 +5504,12 @@ class StreamRecordingState {
5492
5504
  }
5493
5505
  let result;
5494
5506
  if (this.activeRouteDecision.route === "safe-capture-post-recording-transcode") {
5495
- const safeCaptureResult = await this.stopSafeCaptureRecording();
5507
+ const safeCaptureResult = await this.stopSafeCaptureRecording(recordingElapsedSeconds);
5496
5508
  logger.debug("[StreamRecordingState] Finalizing MediaRecorder capture");
5497
5509
  result = {
5498
5510
  blob: safeCaptureResult.blob,
5511
+ captureFrameRate: safeCaptureResult.captureFrameRate,
5512
+ expectedDurationSeconds: safeCaptureResult.expectedDurationSeconds,
5499
5513
  mediaRecorderDiagnostics: safeCaptureResult.diagnostics
5500
5514
  };
5501
5515
  } else {
@@ -5513,6 +5527,8 @@ class StreamRecordingState {
5513
5527
  });
5514
5528
  return {
5515
5529
  blob: result.blob,
5530
+ captureFrameRate: result.captureFrameRate,
5531
+ expectedDurationSeconds: result.expectedDurationSeconds,
5516
5532
  tabVisibilityIntervals,
5517
5533
  recordingStats: result.recordingStats,
5518
5534
  encoderAcceleration: result.encoderAcceleration,
@@ -5537,7 +5553,7 @@ class StreamRecordingState {
5537
5553
  resumeRecording() {
5538
5554
  if (this.pauseStartTime !== null) {
5539
5555
  const pausedDuration = this.dependencies.getCurrentTimestamp() - this.pauseStartTime;
5540
- this.totalPausedTime += pausedDuration;
5556
+ this.totalPausedTime += Math.max(0, pausedDuration);
5541
5557
  this.pauseStartTime = null;
5542
5558
  }
5543
5559
  if (this.tabVisibilityTracker) {
@@ -5632,6 +5648,18 @@ class StreamRecordingState {
5632
5648
  this.totalPausedTime = 0;
5633
5649
  this.pauseStartTime = null;
5634
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
+ }
5635
5663
  async startSafeCaptureRecording(mediaStream, config) {
5636
5664
  if (typeof MediaRecorder === "undefined") {
5637
5665
  throw createUnsupportedRouteError({
@@ -5644,11 +5672,16 @@ class StreamRecordingState {
5644
5672
  });
5645
5673
  }
5646
5674
  await this.applySafeCaptureTrackConstraints(mediaStream);
5675
+ this.safeCaptureFrameRate = this.resolveEffectiveSafeCaptureFrameRate(mediaStream, config);
5647
5676
  this.mediaRecorderChunks = [];
5648
5677
  this.mediaRecorderError = null;
5649
- const preferredMimeType = this.resolvePreferredSafeCaptureMimeType(config);
5650
- const recorderOptions = preferredMimeType === undefined ? undefined : { mimeType: preferredMimeType };
5651
- 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;
5652
5685
  this.mediaRecorder = recorder;
5653
5686
  this.mediaRecorderStopPromise = new Promise((resolve, reject) => {
5654
5687
  this.mediaRecorderStopResolve = resolve;
@@ -5658,31 +5691,97 @@ class StreamRecordingState {
5658
5691
  return;
5659
5692
  });
5660
5693
  recorder.ondataavailable = (event) => {
5694
+ if (!this.isActiveSafeCaptureGeneration(generation)) {
5695
+ return;
5696
+ }
5661
5697
  if (event.data.size > 0) {
5662
5698
  this.mediaRecorderChunks.push(event.data);
5663
5699
  }
5664
5700
  };
5665
5701
  recorder.onstop = () => {
5702
+ if (!this.isActiveSafeCaptureGeneration(generation)) {
5703
+ return;
5704
+ }
5705
+ this.isSafeCaptureSettled = true;
5666
5706
  this.clearMediaRecorderErrorTimeout();
5667
5707
  if (this.mediaRecorderError !== null) {
5668
5708
  this.updateMediaRecorderErrorDiagnostics(recorder);
5669
5709
  this.mediaRecorderStopReject?.(this.mediaRecorderError);
5670
5710
  return;
5671
5711
  }
5672
- const mimeType = recorder.mimeType || this.resolveSafeCaptureMimeType();
5712
+ const mimeType = this.resolveRecordedMimeType(recorder);
5673
5713
  const blob = new Blob(this.mediaRecorderChunks, { type: mimeType });
5674
5714
  const diagnostics = this.buildMediaRecorderDiagnostics(recorder);
5715
+ const captureFrameRate = this.safeCaptureFrameRate;
5716
+ let expectedDurationSeconds = this.safeCaptureExpectedDurationSeconds;
5717
+ if (expectedDurationSeconds === null) {
5718
+ expectedDurationSeconds = this.resolveActiveRecordingDurationSeconds(this.dependencies.getCurrentTimestamp());
5719
+ }
5720
+ if (captureFrameRate === null) {
5721
+ this.mediaRecorderStopReject?.(new Error("Safe capture frame rate is unavailable"));
5722
+ return;
5723
+ }
5724
+ diagnostics.mediaRecorderCaptureFrameRate = captureFrameRate;
5725
+ diagnostics.mediaRecorderExpectedDurationSeconds = expectedDurationSeconds;
5675
5726
  if (blob.size > 0) {
5676
- this.mediaRecorderStopResolve?.({ blob, diagnostics });
5727
+ this.mediaRecorderStopResolve?.({
5728
+ blob,
5729
+ captureFrameRate,
5730
+ diagnostics,
5731
+ expectedDurationSeconds
5732
+ });
5677
5733
  return;
5678
5734
  }
5679
5735
  this.mediaRecorderStopReject?.(this.createEmptyMediaRecorderError(recorder));
5680
5736
  };
5681
5737
  recorder.onerror = (event) => {
5738
+ if (!this.isActiveSafeCaptureGeneration(generation)) {
5739
+ return;
5740
+ }
5682
5741
  this.mediaRecorderError = this.createMediaRecorderError(event, recorder);
5683
5742
  };
5743
+ this.safeCaptureStartTime = this.dependencies.getCurrentTimestamp();
5684
5744
  recorder.start(SAFE_CAPTURE_TIMESLICE_MS);
5685
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);
5764
+ }
5765
+ resolveEffectiveSafeCaptureFrameRate(mediaStream, config) {
5766
+ const videoTrack = mediaStream.getVideoTracks()[0];
5767
+ if (videoTrack && typeof videoTrack.getSettings === "function") {
5768
+ const frameRate = videoTrack.getSettings().frameRate;
5769
+ if (typeof frameRate === "number" && Number.isFinite(frameRate) && frameRate > 0) {
5770
+ return frameRate;
5771
+ }
5772
+ }
5773
+ const reasonCodes = this.activeRouteDecision.reasonCodes;
5774
+ if (reasonCodes.includes(ANDROID_POST_RECORDING_TRANSCODE_REASON)) {
5775
+ return ANDROID_SAFE_CAPTURE_FRAME_RATE;
5776
+ }
5777
+ 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")) {
5778
+ return LOW_RESOURCE_SAFE_CAPTURE_FRAME_RATE;
5779
+ }
5780
+ if (typeof config.fps === "number" && Number.isFinite(config.fps) && config.fps > 0) {
5781
+ return config.fps;
5782
+ }
5783
+ return LOW_RESOURCE_SAFE_CAPTURE_FRAME_RATE;
5784
+ }
5686
5785
  async applySafeCaptureTrackConstraints(mediaStream) {
5687
5786
  const constraints = this.resolveSafeCaptureVideoConstraints();
5688
5787
  if (constraints === null) {
@@ -5690,6 +5789,9 @@ class StreamRecordingState {
5690
5789
  }
5691
5790
  const videoTracks = mediaStream.getVideoTracks();
5692
5791
  await Promise.all(videoTracks.map(async (track) => {
5792
+ if (typeof track.applyConstraints !== "function") {
5793
+ return;
5794
+ }
5693
5795
  try {
5694
5796
  await track.applyConstraints(constraints);
5695
5797
  } catch (error) {
@@ -5707,35 +5809,62 @@ class StreamRecordingState {
5707
5809
  }
5708
5810
  return null;
5709
5811
  }
5710
- async stopSafeCaptureRecording() {
5812
+ async stopSafeCaptureRecording(expectedDurationSeconds) {
5711
5813
  const recorder = this.mediaRecorder;
5712
5814
  const stopPromise = this.mediaRecorderStopPromise;
5713
5815
  if (!(recorder && stopPromise)) {
5714
5816
  throw new Error("Safe capture recorder was not started");
5715
5817
  }
5716
- if (recorder.state !== "inactive") {
5717
- recorder.stop();
5718
- }
5719
- this.scheduleMediaRecorderErrorFinalizeTimeout(recorder);
5720
- try {
5721
- return await stopPromise;
5722
- } finally {
5723
- this.mediaRecorder = null;
5724
- this.mediaRecorderChunks = [];
5725
- this.mediaRecorderStopPromise = null;
5726
- this.mediaRecorderStopResolve = null;
5727
- this.mediaRecorderStopReject = null;
5728
- this.mediaRecorderError = null;
5729
- this.clearMediaRecorderErrorTimeout();
5730
- this.activeRouteDecision = DEFAULT_ROUTE_DECISION;
5731
- }
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;
5732
5848
  }
5733
5849
  resolveSafeCaptureMimeType() {
5734
5850
  const mediaStream = this.streamManager.getStream();
5735
5851
  const hasVideo = (mediaStream?.getVideoTracks().length ?? 0) > 0;
5736
5852
  return hasVideo ? "video/webm" : "audio/webm";
5737
5853
  }
5738
- 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() {
5739
5868
  const canUseMimeType = (mimeType) => {
5740
5869
  if (typeof MediaRecorder.isTypeSupported !== "function") {
5741
5870
  return false;
@@ -5752,22 +5881,7 @@ class StreamRecordingState {
5752
5881
  "video/mp4;codecs=avc1.42001f,mp4a.40.2",
5753
5882
  "video/mp4"
5754
5883
  ];
5755
- let candidateMimeTypes = mp4CandidateMimeTypes;
5756
- if (this.shouldPreferWebmSafeCapture(config)) {
5757
- candidateMimeTypes = [
5758
- ...webmCandidateMimeTypes,
5759
- ...mp4CandidateMimeTypes
5760
- ];
5761
- } else if (config.format !== "mp4") {
5762
- candidateMimeTypes = webmCandidateMimeTypes;
5763
- }
5764
- return candidateMimeTypes.find(canUseMimeType);
5765
- }
5766
- shouldPreferWebmSafeCapture(config) {
5767
- if (config.format !== "mp4") {
5768
- return false;
5769
- }
5770
- return this.activeRouteDecision.reasonCodes.includes(ANDROID_POST_RECORDING_TRANSCODE_REASON);
5884
+ return [...webmCandidateMimeTypes, ...mp4CandidateMimeTypes].filter(canUseMimeType);
5771
5885
  }
5772
5886
  scheduleMediaRecorderErrorFinalizeTimeout(recorder) {
5773
5887
  if (this.mediaRecorderErrorTimeoutId !== null) {
@@ -5794,9 +5908,10 @@ class StreamRecordingState {
5794
5908
  const error = new Error("Browser recording failed before upload could start");
5795
5909
  error.name = "MediaRecorderRecordingError";
5796
5910
  error.code = MEDIA_RECORDER_ERROR_CODE;
5797
- error.mediaRecorderMimeType = recorder.mimeType;
5911
+ error.mediaRecorderMimeType = this.resolveRecordedMimeType(recorder);
5798
5912
  error.mediaRecorderChunkCount = this.mediaRecorderChunks.length;
5799
5913
  error.mediaRecorderChunkSize = this.getMediaRecorderChunkSize();
5914
+ this.assignSafeCaptureErrorDiagnostics(error);
5800
5915
  if (browserError?.name) {
5801
5916
  error.mediaRecorderErrorName = browserError.name;
5802
5917
  }
@@ -5809,9 +5924,10 @@ class StreamRecordingState {
5809
5924
  const error = new Error("Browser recording produced no data before upload could start");
5810
5925
  error.name = "MediaRecorderEmptyDataError";
5811
5926
  error.code = MEDIA_RECORDER_EMPTY_ERROR_CODE;
5812
- error.mediaRecorderMimeType = recorder.mimeType;
5927
+ error.mediaRecorderMimeType = this.resolveRecordedMimeType(recorder);
5813
5928
  error.mediaRecorderChunkCount = this.mediaRecorderChunks.length;
5814
5929
  error.mediaRecorderChunkSize = this.getMediaRecorderChunkSize();
5930
+ this.assignSafeCaptureErrorDiagnostics(error);
5815
5931
  if (this.mediaRecorderError?.mediaRecorderErrorName) {
5816
5932
  error.mediaRecorderErrorName = this.mediaRecorderError.mediaRecorderErrorName;
5817
5933
  }
@@ -5824,17 +5940,21 @@ class StreamRecordingState {
5824
5940
  if (this.mediaRecorderError === null) {
5825
5941
  return;
5826
5942
  }
5827
- this.mediaRecorderError.mediaRecorderMimeType = recorder.mimeType;
5943
+ this.mediaRecorderError.mediaRecorderMimeType = this.resolveRecordedMimeType(recorder);
5828
5944
  this.mediaRecorderError.mediaRecorderChunkCount = this.mediaRecorderChunks.length;
5829
5945
  this.mediaRecorderError.mediaRecorderChunkSize = this.getMediaRecorderChunkSize();
5946
+ this.assignSafeCaptureErrorDiagnostics(this.mediaRecorderError);
5830
5947
  }
5831
5948
  buildMediaRecorderDiagnostics(recorder) {
5832
5949
  const diagnostics = {
5833
- mediaRecorderMimeType: recorder.mimeType,
5950
+ mediaRecorderMimeType: this.resolveRecordedMimeType(recorder),
5834
5951
  mediaRecorderChunkCount: this.mediaRecorderChunks.length,
5835
5952
  mediaRecorderChunkSize: this.getMediaRecorderChunkSize(),
5836
5953
  mediaRecorderHadError: this.mediaRecorderError !== null
5837
5954
  };
5955
+ if (this.mediaRecorderRequestedMimeType !== null) {
5956
+ diagnostics.mediaRecorderRequestedMimeType = this.mediaRecorderRequestedMimeType;
5957
+ }
5838
5958
  if (this.mediaRecorderError?.mediaRecorderErrorName) {
5839
5959
  diagnostics.mediaRecorderErrorName = this.mediaRecorderError.mediaRecorderErrorName;
5840
5960
  }
@@ -5843,6 +5963,15 @@ class StreamRecordingState {
5843
5963
  }
5844
5964
  return diagnostics;
5845
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
+ }
5846
5975
  getMediaRecorderChunkSize() {
5847
5976
  return this.mediaRecorderChunks.reduce((total, chunk) => total + chunk.size, 0);
5848
5977
  }
@@ -5926,6 +6055,12 @@ class StreamRecordingState {
5926
6055
  this.mediaRecorderStopResolve = null;
5927
6056
  this.mediaRecorderStopReject = null;
5928
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;
5929
6064
  this.clearMediaRecorderErrorTimeout();
5930
6065
  this.activeRouteDecision = DEFAULT_ROUTE_DECISION;
5931
6066
  if (this.streamProcessor) {
@@ -6073,6 +6208,7 @@ var RECORDING_CODEC_UNSUPPORTED_CODES = new Set([
6073
6208
  "recording.video-codec-unavailable"
6074
6209
  ]);
6075
6210
  var RECORDING_SERVER_TRANSCODE_FALLBACK_CODES = new Set([
6211
+ "recording.deferred-transcode-integrity",
6076
6212
  "recording.post-recording-transcode-fallback",
6077
6213
  "transcode.post-recording-fallback",
6078
6214
  "route.post-recording-transcode-fallback"
@@ -6315,7 +6451,7 @@ function resolveDeviceError(input) {
6315
6451
  // package.json
6316
6452
  var package_default = {
6317
6453
  name: "@vidtreo/recorder",
6318
- version: "1.7.9",
6454
+ version: "1.7.11",
6319
6455
  type: "module",
6320
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.",
6321
6457
  main: "./dist/index.js",
@@ -6400,12 +6536,23 @@ var MAX_RETRY_ATTEMPTS = 3;
6400
6536
  var MAX_PENDING_EVENTS = 100;
6401
6537
  var BRACKET_ERROR_CODE_PATTERN = /\[([a-z]+(?:[.-][a-z0-9]+)+)\]/i;
6402
6538
  var INLINE_ERROR_CODE_PATTERN = /\b([a-z]+(?:[.-][a-z0-9]+)+)\b/i;
6403
- var MEDIA_RECORDER_ERROR_PROPERTY_KEYS = [
6539
+ var RECORDING_INTEGRITY_ERROR_PROPERTY_KEYS = [
6404
6540
  "mediaRecorderErrorName",
6405
6541
  "mediaRecorderErrorMessage",
6406
6542
  "mediaRecorderMimeType",
6407
6543
  "mediaRecorderChunkCount",
6408
- "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"
6409
6556
  ];
6410
6557
  function resolveInstallationId(dependencies) {
6411
6558
  const storageProvider = dependencies.storageProvider;
@@ -6761,7 +6908,7 @@ class TelemetryClient {
6761
6908
  }
6762
6909
  const candidate = error;
6763
6910
  let properties = {};
6764
- for (const key of MEDIA_RECORDER_ERROR_PROPERTY_KEYS) {
6911
+ for (const key of RECORDING_INTEGRITY_ERROR_PROPERTY_KEYS) {
6765
6912
  const value = candidate[key];
6766
6913
  if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
6767
6914
  properties = { ...properties, [key]: value };
@@ -22751,6 +22898,243 @@ class StreamProcessor {
22751
22898
  }
22752
22899
  }
22753
22900
 
22901
+ // src/core/transcode/deferred-video-transcoder.ts
22902
+ import { ALL_FORMATS as ALL_FORMATS2, BlobSource as BlobSource3, EncodedPacketSink, Input as Input3 } from "mediabunny";
22903
+ var MINIMUM_FRAME_DENSITY_RATIO = 0.9;
22904
+ var MAXIMUM_DURATION_DIFFERENCE_RATIO = 0.05;
22905
+ var MINIMUM_DURATION_TOLERANCE_SECONDS = 0.25;
22906
+ var MAXIMUM_FRAME_GAP_MULTIPLIER = 5;
22907
+ var MINIMUM_FRAME_GAP_TOLERANCE_SECONDS = 0.25;
22908
+ var LONG_RECORDING_THRESHOLD_SECONDS = 3;
22909
+ var LONG_RECORDING_DURATION_TOLERANCE_SECONDS = 1.5;
22910
+ var LONG_RECORDING_DURATION_TOLERANCE_RATIO = 0.15;
22911
+ var SHORT_RECORDING_DURATION_TOLERANCE_SECONDS = 0.75;
22912
+ var SHORT_RECORDING_DURATION_TOLERANCE_RATIO = 0.5;
22913
+ var INTEGRITY_ERROR_CODE = "recording.deferred-transcode-integrity";
22914
+ function createIntegrityError(message, diagnostics = {}) {
22915
+ const error = new Error(message);
22916
+ error.name = "DeferredTranscodeIntegrityError";
22917
+ error.code = INTEGRITY_ERROR_CODE;
22918
+ error.isRecoverable = true;
22919
+ Object.assign(error, diagnostics);
22920
+ return error;
22921
+ }
22922
+ function validatePacketSummary(summary, label, diagnostics) {
22923
+ if (!Number.isFinite(summary.averagePacketRate)) {
22924
+ throw createIntegrityError(`${label} video packet rate is invalid`, diagnostics);
22925
+ }
22926
+ if (!Number.isFinite(summary.duration)) {
22927
+ throw createIntegrityError(`${label} video duration is invalid`, diagnostics);
22928
+ }
22929
+ if (!Number.isFinite(summary.maximumPacketGap)) {
22930
+ throw createIntegrityError(`${label} video maximum packet gap is invalid`, diagnostics);
22931
+ }
22932
+ if (summary.averagePacketRate <= 0 || summary.duration <= 0) {
22933
+ throw createIntegrityError(`${label} video packet statistics are empty`, diagnostics);
22934
+ }
22935
+ if (summary.maximumPacketGap < 0) {
22936
+ throw createIntegrityError(`${label} video maximum packet gap is invalid`, diagnostics);
22937
+ }
22938
+ if (!Number.isInteger(summary.packetCount) || summary.packetCount <= 0) {
22939
+ throw createIntegrityError(`${label} video packet count is invalid`, diagnostics);
22940
+ }
22941
+ }
22942
+ function validateMaximumFrameGap(summary, label, targetFrameRate, diagnostics) {
22943
+ const maximumPacketGap = Math.max(MINIMUM_FRAME_GAP_TOLERANCE_SECONDS, MAXIMUM_FRAME_GAP_MULTIPLIER / targetFrameRate);
22944
+ if (summary.maximumPacketGap > maximumPacketGap) {
22945
+ throw createIntegrityError(`${label} video contains a material frame gap`, diagnostics);
22946
+ }
22947
+ }
22948
+ function validateOutputFrameDensity(summary, targetFrameRate, diagnostics) {
22949
+ const minimumPacketRate = targetFrameRate * MINIMUM_FRAME_DENSITY_RATIO;
22950
+ if (summary.averagePacketRate < minimumPacketRate) {
22951
+ throw createIntegrityError("Output video frame density is below the capture target", diagnostics);
22952
+ }
22953
+ }
22954
+ function buildIntegrityDiagnostics(validation) {
22955
+ let stage = "output";
22956
+ if (validation.stage !== undefined) {
22957
+ stage = validation.stage;
22958
+ }
22959
+ const diagnostics = {
22960
+ deferredIntegrityStage: stage
22961
+ };
22962
+ assignFiniteDiagnostic(diagnostics, "mediaRecorderExpectedDurationSeconds", validation.expectedDurationSeconds);
22963
+ if (validation.source !== null) {
22964
+ assignFiniteDiagnostic(diagnostics, "deferredSourceDurationSeconds", validation.source.duration);
22965
+ assignFiniteDiagnostic(diagnostics, "deferredSourcePacketCount", validation.source.packetCount);
22966
+ assignFiniteDiagnostic(diagnostics, "deferredSourcePacketRate", validation.source.averagePacketRate);
22967
+ assignFiniteDiagnostic(diagnostics, "deferredSourceMaximumPacketGap", validation.source.maximumPacketGap);
22968
+ }
22969
+ if (validation.output !== null) {
22970
+ assignFiniteDiagnostic(diagnostics, "deferredOutputDurationSeconds", validation.output.duration);
22971
+ assignFiniteDiagnostic(diagnostics, "deferredOutputPacketCount", validation.output.packetCount);
22972
+ assignFiniteDiagnostic(diagnostics, "deferredOutputPacketRate", validation.output.averagePacketRate);
22973
+ assignFiniteDiagnostic(diagnostics, "deferredOutputMaximumPacketGap", validation.output.maximumPacketGap);
22974
+ }
22975
+ return diagnostics;
22976
+ }
22977
+ function assignFiniteDiagnostic(diagnostics, key, value) {
22978
+ if (Number.isFinite(value)) {
22979
+ diagnostics[key] = value;
22980
+ }
22981
+ }
22982
+ function resolveLifecycleDurationTolerance(expectedDurationSeconds) {
22983
+ if (expectedDurationSeconds >= LONG_RECORDING_THRESHOLD_SECONDS) {
22984
+ return Math.max(LONG_RECORDING_DURATION_TOLERANCE_SECONDS, expectedDurationSeconds * LONG_RECORDING_DURATION_TOLERANCE_RATIO);
22985
+ }
22986
+ return Math.max(SHORT_RECORDING_DURATION_TOLERANCE_SECONDS, expectedDurationSeconds * SHORT_RECORDING_DURATION_TOLERANCE_RATIO);
22987
+ }
22988
+ function validateLifecycleCoverage(summary, label, expectedDurationSeconds, diagnostics) {
22989
+ const durationDifference = Math.abs(summary.duration - expectedDurationSeconds);
22990
+ const tolerance = resolveLifecycleDurationTolerance(expectedDurationSeconds);
22991
+ if (durationDifference > tolerance) {
22992
+ throw createIntegrityError(`${label} video duration does not cover the active recording`, diagnostics);
22993
+ }
22994
+ }
22995
+ function validateDeferredVideoStats(validation) {
22996
+ const diagnostics = buildIntegrityDiagnostics(validation);
22997
+ if (!Number.isFinite(validation.expectedDurationSeconds) || validation.expectedDurationSeconds < 0) {
22998
+ throw createIntegrityError("Deferred transcode requires a valid active recording duration", diagnostics);
22999
+ }
23000
+ if (!Number.isFinite(validation.targetFrameRate) || validation.targetFrameRate <= 0) {
23001
+ throw createIntegrityError("Deferred transcode requires a valid capture frame rate", diagnostics);
23002
+ }
23003
+ if (validation.source === null) {
23004
+ throw createIntegrityError("Deferred transcode source has no primary video track", diagnostics);
23005
+ }
23006
+ validatePacketSummary(validation.source, "Source", diagnostics);
23007
+ validateMaximumFrameGap(validation.source, "Source", validation.targetFrameRate, diagnostics);
23008
+ validateLifecycleCoverage(validation.source, "Source", validation.expectedDurationSeconds, diagnostics);
23009
+ if (validation.stage === "source") {
23010
+ return;
23011
+ }
23012
+ if (validation.output === null) {
23013
+ throw createIntegrityError("Deferred transcode output has no primary video track", diagnostics);
23014
+ }
23015
+ validatePacketSummary(validation.output, "Output", diagnostics);
23016
+ validateOutputFrameDensity(validation.output, validation.targetFrameRate, diagnostics);
23017
+ validateMaximumFrameGap(validation.output, "Output", validation.targetFrameRate, diagnostics);
23018
+ validateLifecycleCoverage(validation.output, "Output", validation.expectedDurationSeconds, diagnostics);
23019
+ const durationDifference = Math.abs(validation.source.duration - validation.output.duration);
23020
+ const durationTolerance = Math.max(MINIMUM_DURATION_TOLERANCE_SECONDS, validation.source.duration * MAXIMUM_DURATION_DIFFERENCE_RATIO);
23021
+ if (durationDifference > durationTolerance) {
23022
+ throw createIntegrityError("Deferred transcode output duration differs materially from source", diagnostics);
23023
+ }
23024
+ const minimumPacketRate = validation.targetFrameRate * MINIMUM_FRAME_DENSITY_RATIO;
23025
+ const minimumPacketCount = Math.floor(validation.source.duration * minimumPacketRate);
23026
+ if (validation.output.averagePacketRate < minimumPacketRate || validation.output.packetCount < minimumPacketCount) {
23027
+ throw createIntegrityError("Deferred transcode output lost material video frame density", diagnostics);
23028
+ }
23029
+ }
23030
+ async function inspectVideoPackets(blob) {
23031
+ const input = new Input3({
23032
+ formats: ALL_FORMATS2,
23033
+ source: new BlobSource3(blob)
23034
+ });
23035
+ const videoTrack = await input.getPrimaryVideoTrack();
23036
+ if (videoTrack === null) {
23037
+ input.dispose();
23038
+ return null;
23039
+ }
23040
+ try {
23041
+ const packetSink = new EncodedPacketSink(videoTrack);
23042
+ const timestamps = [];
23043
+ let firstTimestamp = Number.POSITIVE_INFINITY;
23044
+ let lastEndTimestamp = Number.NEGATIVE_INFINITY;
23045
+ for await (const packet of packetSink.packets(undefined, undefined, {
23046
+ metadataOnly: true
23047
+ })) {
23048
+ timestamps.push(packet.timestamp);
23049
+ firstTimestamp = Math.min(firstTimestamp, packet.timestamp);
23050
+ lastEndTimestamp = Math.max(lastEndTimestamp, packet.timestamp + packet.duration);
23051
+ }
23052
+ timestamps.sort((left, right) => left - right);
23053
+ let maximumPacketGap = 0;
23054
+ for (let index = 1;index < timestamps.length; index += 1) {
23055
+ maximumPacketGap = Math.max(maximumPacketGap, timestamps[index] - timestamps[index - 1]);
23056
+ }
23057
+ const duration = lastEndTimestamp - firstTimestamp;
23058
+ const packetCount = timestamps.length;
23059
+ const averagePacketRate = packetCount > 0 ? packetCount / duration : 0;
23060
+ return {
23061
+ averagePacketRate,
23062
+ duration,
23063
+ maximumPacketGap,
23064
+ packetCount
23065
+ };
23066
+ } finally {
23067
+ input.dispose();
23068
+ }
23069
+ }
23070
+ async function inspectDeferredVideo(blob, label, diagnostics) {
23071
+ return await inspectVideoPackets(blob).catch((error) => {
23072
+ const message = extractErrorMessage(error);
23073
+ throw createIntegrityError(`Could not inspect ${label} deferred video: ${message}`, diagnostics);
23074
+ });
23075
+ }
23076
+ async function transcodeDeferredVideo(input, config, onProgress, context) {
23077
+ const targetFrameRate = config.fps;
23078
+ if (typeof targetFrameRate !== "number" || !Number.isFinite(targetFrameRate) || targetFrameRate <= 0) {
23079
+ throw createIntegrityError("Deferred transcode requires a valid capture frame rate", {
23080
+ deferredIntegrityStage: "source",
23081
+ mediaRecorderExpectedDurationSeconds: context.expectedDurationSeconds
23082
+ });
23083
+ }
23084
+ const source = await inspectDeferredVideo(input, "source", {
23085
+ deferredIntegrityStage: "source",
23086
+ mediaRecorderExpectedDurationSeconds: context.expectedDurationSeconds
23087
+ });
23088
+ validateDeferredVideoStats({
23089
+ expectedDurationSeconds: context.expectedDurationSeconds,
23090
+ output: null,
23091
+ source,
23092
+ stage: "source",
23093
+ targetFrameRate
23094
+ });
23095
+ const transcodeDiagnostics = buildIntegrityDiagnostics({
23096
+ expectedDurationSeconds: context.expectedDurationSeconds,
23097
+ output: null,
23098
+ source,
23099
+ stage: "source",
23100
+ targetFrameRate
23101
+ });
23102
+ transcodeDiagnostics.deferredIntegrityStage = "transcode";
23103
+ const result = await transcodeVideo(input, config, onProgress).catch((error) => {
23104
+ let transcodeError;
23105
+ if (error instanceof Error) {
23106
+ transcodeError = error;
23107
+ } else {
23108
+ transcodeError = new Error(extractErrorMessage(error));
23109
+ }
23110
+ Object.assign(transcodeError, transcodeDiagnostics);
23111
+ throw transcodeError;
23112
+ });
23113
+ const outputDiagnostics = {
23114
+ deferredIntegrityStage: "output",
23115
+ mediaRecorderExpectedDurationSeconds: context.expectedDurationSeconds
23116
+ };
23117
+ if (source !== null) {
23118
+ outputDiagnostics.deferredSourceDurationSeconds = source.duration;
23119
+ outputDiagnostics.deferredSourcePacketCount = source.packetCount;
23120
+ outputDiagnostics.deferredSourcePacketRate = source.averagePacketRate;
23121
+ outputDiagnostics.deferredSourceMaximumPacketGap = source.maximumPacketGap;
23122
+ }
23123
+ const output = await inspectDeferredVideo(result.blob, "output", outputDiagnostics);
23124
+ const validation = {
23125
+ expectedDurationSeconds: context.expectedDurationSeconds,
23126
+ output,
23127
+ source,
23128
+ stage: "output",
23129
+ targetFrameRate
23130
+ };
23131
+ validateDeferredVideoStats(validation);
23132
+ return {
23133
+ ...result,
23134
+ diagnostics: buildIntegrityDiagnostics(validation)
23135
+ };
23136
+ }
23137
+
22754
23138
  // src/core/recording/recording-manager.ts
22755
23139
  var DEFAULT_COUNTDOWN_DURATION = 5000;
22756
23140
  var MILLISECONDS_PER_SECOND4 = 1000;
@@ -22759,12 +23143,15 @@ var RECORDING_TIMER_INTERVAL = 1000;
22759
23143
  var RECORDING_STATE_RECORDING = "recording";
22760
23144
  var RECORDING_STATE_IDLE = "idle";
22761
23145
  var RECORDING_STATE_COUNTDOWN = "countdown";
23146
+ var LOW_RESOURCE_SAFE_CAPTURE_FRAME_RATE2 = 20;
23147
+ var ANDROID_SAFE_CAPTURE_FRAME_RATE2 = 24;
23148
+ var ANDROID_POST_RECORDING_TRANSCODE_REASON2 = "android-post-recording-transcode";
22762
23149
  var DEFAULT_ROUTE_DECISION2 = {
22763
23150
  route: "local-webcodecs",
22764
23151
  reasonCodes: ["webcodecs-supported", "target-codec-supported"]
22765
23152
  };
22766
23153
  var DEFAULT_RECORDING_MANAGER_DEPENDENCIES = {
22767
- transcodeVideo
23154
+ transcodeVideo: transcodeDeferredVideo
22768
23155
  };
22769
23156
 
22770
23157
  class RecordingManager {
@@ -22999,7 +23386,8 @@ class RecordingManager {
22999
23386
  this.callbacks.onStopAudioTracking();
23000
23387
  logger.debug("[RecordingManager] Stopping recording in stream manager");
23001
23388
  const stopResult = await this.streamManager.stopRecording();
23002
- const finalBlob = await this.resolveFinalRecordingBlob(stopResult.blob);
23389
+ const finalResult = await this.resolveFinalRecordingBlob(stopResult.blob, stopResult.captureFrameRate, stopResult.expectedDurationSeconds);
23390
+ const finalBlob = finalResult.blob;
23003
23391
  logger.info("[RecordingManager] Recording stopped, blob size:", finalBlob.size);
23004
23392
  this.recordingState = RECORDING_STATE_IDLE;
23005
23393
  this.callbacks.onStateChange(this.recordingState);
@@ -23021,6 +23409,9 @@ class RecordingManager {
23021
23409
  if (stopResult.mediaRecorderDiagnostics !== undefined) {
23022
23410
  Object.assign(telemetryProperties, stopResult.mediaRecorderDiagnostics);
23023
23411
  }
23412
+ if (finalResult.diagnostics !== undefined) {
23413
+ Object.assign(telemetryProperties, finalResult.diagnostics);
23414
+ }
23024
23415
  return {
23025
23416
  blob: finalBlob,
23026
23417
  telemetryProperties
@@ -23101,23 +23492,39 @@ class RecordingManager {
23101
23492
  this.recordingStartTime = null;
23102
23493
  this.maxTimeRemaining = null;
23103
23494
  }
23104
- async resolveFinalRecordingBlob(blob) {
23495
+ async resolveFinalRecordingBlob(blob, captureFrameRate, expectedDurationSeconds) {
23105
23496
  if (this.activeRouteDecision.route !== "safe-capture-post-recording-transcode") {
23106
- return blob;
23497
+ return { blob };
23107
23498
  }
23108
23499
  if (!this.activeRecordingConfig) {
23109
23500
  throw new Error("Recording config unavailable for final video processing");
23110
23501
  }
23502
+ if (typeof expectedDurationSeconds !== "number" || !Number.isFinite(expectedDurationSeconds) || expectedDurationSeconds < 0) {
23503
+ throw new Error("Recording active duration unavailable for final video processing");
23504
+ }
23111
23505
  const transcodeConfig = {
23112
23506
  ...this.activeRecordingConfig,
23113
- fps: undefined
23507
+ fps: this.resolveDeferredCaptureFrameRate(captureFrameRate),
23508
+ latencyMode: "quality"
23114
23509
  };
23115
23510
  this.callbacks.onTranscodingProgress?.(0);
23116
23511
  const result = await this.dependencies.transcodeVideo(blob, transcodeConfig, (progress) => {
23117
23512
  this.callbacks.onTranscodingProgress?.(progress);
23118
- });
23513
+ }, { expectedDurationSeconds });
23119
23514
  this.callbacks.onTranscodingProgress?.(1);
23120
- return result.blob;
23515
+ return {
23516
+ blob: result.blob,
23517
+ diagnostics: result.diagnostics
23518
+ };
23519
+ }
23520
+ resolveDeferredCaptureFrameRate(captureFrameRate) {
23521
+ if (typeof captureFrameRate === "number" && Number.isFinite(captureFrameRate) && captureFrameRate > 0) {
23522
+ return captureFrameRate;
23523
+ }
23524
+ if (this.activeRouteDecision.reasonCodes.includes(ANDROID_POST_RECORDING_TRANSCODE_REASON2)) {
23525
+ return ANDROID_SAFE_CAPTURE_FRAME_RATE2;
23526
+ }
23527
+ return LOW_RESOURCE_SAFE_CAPTURE_FRAME_RATE2;
23121
23528
  }
23122
23529
  resetPauseState() {
23123
23530
  this.isPaused = false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vidtreo/recorder",
3
- "version": "1.7.9",
3
+ "version": "1.7.11",
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",