@vidtreo/recorder 1.7.9 → 1.7.10

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
@@ -200,6 +200,8 @@ export {};
200
200
 
201
201
  export {};
202
202
 
203
+ export {};
204
+
203
205
  declare const ANSI_COLORS: {
204
206
  readonly reset: "\u001B[0m";
205
207
  readonly bright: "\u001B[1m";
@@ -1557,6 +1559,7 @@ 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;
1560
1563
  tabVisibilityIntervals: VisibilityInterval[];
1561
1564
  recordingStats?: StreamRecordingStats;
1562
1565
  encoderAcceleration?: VideoHardwareAcceleration;
@@ -1587,6 +1590,7 @@ export declare class StreamRecordingState {
1587
1590
  private mediaRecorderStopResolve;
1588
1591
  private mediaRecorderStopReject;
1589
1592
  private mediaRecorderError;
1593
+ private safeCaptureFrameRate;
1590
1594
  private mediaRecorderErrorTimeoutId;
1591
1595
  private readonly streamManager;
1592
1596
  private readonly dependencies;
@@ -1611,6 +1615,7 @@ export declare class StreamRecordingState {
1611
1615
  private clearBufferSizeInterval;
1612
1616
  private resetRecordingState;
1613
1617
  private startSafeCaptureRecording;
1618
+ private resolveEffectiveSafeCaptureFrameRate;
1614
1619
  private applySafeCaptureTrackConstraints;
1615
1620
  private resolveSafeCaptureVideoConstraints;
1616
1621
  private stopSafeCaptureRecording;
@@ -1930,13 +1935,13 @@ export declare class RecorderController {
1930
1935
 
1931
1936
  import type { CameraStreamManager } from "../stream/stream";
1932
1937
  import { StreamProcessor } from "../stream/stream-processor";
1938
+ import { transcodeDeferredVideo } from "../transcode/deferred-video-transcoder";
1933
1939
  import type { TranscodeConfig } from "../transcode/transcode-types";
1934
- import { transcodeVideo } from "../transcode/video-transcoder";
1935
1940
  export type RecordingStopResult = {
1936
1941
  blob: Blob;
1937
1942
  telemetryProperties: Record<string, unknown>;
1938
1943
  };
1939
- type TranscodeVideoDependency = typeof transcodeVideo;
1944
+ type TranscodeVideoDependency = typeof transcodeDeferredVideo;
1940
1945
  type RecordingManagerDependencies = {
1941
1946
  transcodeVideo: TranscodeVideoDependency;
1942
1947
  };
@@ -1990,6 +1995,7 @@ export declare class RecordingManager {
1990
1995
  cleanup(): void;
1991
1996
  private resetRecordingState;
1992
1997
  private resolveFinalRecordingBlob;
1998
+ private resolveDeferredCaptureFrameRate;
1993
1999
  private resetPauseState;
1994
2000
  private getOrCreateStreamProcessor;
1995
2001
  private resolveRouteDecision;
@@ -3162,6 +3168,20 @@ export declare function getFileExtensionForOutputFormat(format: OutputFormat): s
3162
3168
  export declare function transcodeVideo(input: TranscodeInput, config?: Partial<TranscodeConfig>, onProgress?: (progress: number) => void): Promise<TranscodeResult>;
3163
3169
  export declare function transcodeVideoForNativeCamera(file: File, config?: Partial<TranscodeConfig>, onProgress?: (progress: number) => void): Promise<TranscodeResult>;
3164
3170
 
3171
+ export type VideoPacketSummary = {
3172
+ averagePacketRate: number;
3173
+ duration: number;
3174
+ maximumPacketGap: number;
3175
+ packetCount: number;
3176
+ };
3177
+ export type DeferredVideoStatsValidation = {
3178
+ output: VideoPacketSummary | null;
3179
+ source: VideoPacketSummary | null;
3180
+ targetFrameRate: number;
3181
+ };
3182
+ export declare function validateDeferredVideoStats(validation: DeferredVideoStatsValidation): void;
3183
+ export declare function transcodeDeferredVideo(input: Blob, config: TranscodeConfig, onProgress?: (progress: number) => void): Promise<TranscodeResult>;
3184
+
3165
3185
  export type VidtreoRecorderConfig = {
3166
3186
  apiKey: string;
3167
3187
  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,7 +5285,8 @@ var ANDROID_SAFE_CAPTURE_CONSTRAINTS = {
5282
5285
  height: { ideal: 720, max: 720 },
5283
5286
  frameRate: { ideal: 24, max: 24 }
5284
5287
  };
5285
- var SAFE_CAPTURE_TIMESLICE_MS = 1000;
5288
+ var LOW_RESOURCE_SAFE_CAPTURE_FRAME_RATE = 20;
5289
+ var ANDROID_SAFE_CAPTURE_FRAME_RATE = 24;
5286
5290
  var MEDIA_RECORDER_ERROR_FINALIZE_TIMEOUT_MS = 5000;
5287
5291
  var ANDROID_POST_RECORDING_TRANSCODE_REASON = "android-post-recording-transcode";
5288
5292
  var MEDIA_RECORDER_ERROR_CODE = "recording.mediarecorder-error";
@@ -5319,6 +5323,7 @@ class StreamRecordingState {
5319
5323
  mediaRecorderStopResolve = null;
5320
5324
  mediaRecorderStopReject = null;
5321
5325
  mediaRecorderError = null;
5326
+ safeCaptureFrameRate = null;
5322
5327
  mediaRecorderErrorTimeoutId = null;
5323
5328
  streamManager;
5324
5329
  dependencies;
@@ -5496,6 +5501,7 @@ class StreamRecordingState {
5496
5501
  logger.debug("[StreamRecordingState] Finalizing MediaRecorder capture");
5497
5502
  result = {
5498
5503
  blob: safeCaptureResult.blob,
5504
+ captureFrameRate: safeCaptureResult.captureFrameRate,
5499
5505
  mediaRecorderDiagnostics: safeCaptureResult.diagnostics
5500
5506
  };
5501
5507
  } else {
@@ -5513,6 +5519,7 @@ class StreamRecordingState {
5513
5519
  });
5514
5520
  return {
5515
5521
  blob: result.blob,
5522
+ captureFrameRate: result.captureFrameRate,
5516
5523
  tabVisibilityIntervals,
5517
5524
  recordingStats: result.recordingStats,
5518
5525
  encoderAcceleration: result.encoderAcceleration,
@@ -5644,6 +5651,7 @@ class StreamRecordingState {
5644
5651
  });
5645
5652
  }
5646
5653
  await this.applySafeCaptureTrackConstraints(mediaStream);
5654
+ this.safeCaptureFrameRate = this.resolveEffectiveSafeCaptureFrameRate(mediaStream, config);
5647
5655
  this.mediaRecorderChunks = [];
5648
5656
  this.mediaRecorderError = null;
5649
5657
  const preferredMimeType = this.resolvePreferredSafeCaptureMimeType(config);
@@ -5672,8 +5680,18 @@ class StreamRecordingState {
5672
5680
  const mimeType = recorder.mimeType || this.resolveSafeCaptureMimeType();
5673
5681
  const blob = new Blob(this.mediaRecorderChunks, { type: mimeType });
5674
5682
  const diagnostics = this.buildMediaRecorderDiagnostics(recorder);
5683
+ const captureFrameRate = this.safeCaptureFrameRate;
5684
+ if (captureFrameRate === null) {
5685
+ this.mediaRecorderStopReject?.(new Error("Safe capture frame rate is unavailable"));
5686
+ return;
5687
+ }
5688
+ diagnostics.mediaRecorderCaptureFrameRate = captureFrameRate;
5675
5689
  if (blob.size > 0) {
5676
- this.mediaRecorderStopResolve?.({ blob, diagnostics });
5690
+ this.mediaRecorderStopResolve?.({
5691
+ blob,
5692
+ captureFrameRate,
5693
+ diagnostics
5694
+ });
5677
5695
  return;
5678
5696
  }
5679
5697
  this.mediaRecorderStopReject?.(this.createEmptyMediaRecorderError(recorder));
@@ -5681,7 +5699,27 @@ class StreamRecordingState {
5681
5699
  recorder.onerror = (event) => {
5682
5700
  this.mediaRecorderError = this.createMediaRecorderError(event, recorder);
5683
5701
  };
5684
- recorder.start(SAFE_CAPTURE_TIMESLICE_MS);
5702
+ recorder.start();
5703
+ }
5704
+ resolveEffectiveSafeCaptureFrameRate(mediaStream, config) {
5705
+ const videoTrack = mediaStream.getVideoTracks()[0];
5706
+ if (videoTrack && typeof videoTrack.getSettings === "function") {
5707
+ const frameRate = videoTrack.getSettings().frameRate;
5708
+ if (typeof frameRate === "number" && Number.isFinite(frameRate) && frameRate > 0) {
5709
+ return frameRate;
5710
+ }
5711
+ }
5712
+ const reasonCodes = this.activeRouteDecision.reasonCodes;
5713
+ if (reasonCodes.includes(ANDROID_POST_RECORDING_TRANSCODE_REASON)) {
5714
+ return ANDROID_SAFE_CAPTURE_FRAME_RATE;
5715
+ }
5716
+ 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")) {
5717
+ return LOW_RESOURCE_SAFE_CAPTURE_FRAME_RATE;
5718
+ }
5719
+ if (typeof config.fps === "number" && Number.isFinite(config.fps) && config.fps > 0) {
5720
+ return config.fps;
5721
+ }
5722
+ return LOW_RESOURCE_SAFE_CAPTURE_FRAME_RATE;
5685
5723
  }
5686
5724
  async applySafeCaptureTrackConstraints(mediaStream) {
5687
5725
  const constraints = this.resolveSafeCaptureVideoConstraints();
@@ -5726,6 +5764,7 @@ class StreamRecordingState {
5726
5764
  this.mediaRecorderStopResolve = null;
5727
5765
  this.mediaRecorderStopReject = null;
5728
5766
  this.mediaRecorderError = null;
5767
+ this.safeCaptureFrameRate = null;
5729
5768
  this.clearMediaRecorderErrorTimeout();
5730
5769
  this.activeRouteDecision = DEFAULT_ROUTE_DECISION;
5731
5770
  }
@@ -6315,7 +6354,7 @@ function resolveDeviceError(input) {
6315
6354
  // package.json
6316
6355
  var package_default = {
6317
6356
  name: "@vidtreo/recorder",
6318
- version: "1.7.9",
6357
+ version: "1.7.10",
6319
6358
  type: "module",
6320
6359
  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
6360
  main: "./dist/index.js",
@@ -22751,6 +22790,136 @@ class StreamProcessor {
22751
22790
  }
22752
22791
  }
22753
22792
 
22793
+ // src/core/transcode/deferred-video-transcoder.ts
22794
+ import { ALL_FORMATS as ALL_FORMATS2, BlobSource as BlobSource3, EncodedPacketSink, Input as Input3 } from "mediabunny";
22795
+ var MINIMUM_FRAME_DENSITY_RATIO = 0.9;
22796
+ var MAXIMUM_DURATION_DIFFERENCE_RATIO = 0.05;
22797
+ var MINIMUM_DURATION_TOLERANCE_SECONDS = 0.25;
22798
+ var MAXIMUM_FRAME_GAP_MULTIPLIER = 5;
22799
+ var MINIMUM_FRAME_GAP_TOLERANCE_SECONDS = 0.25;
22800
+ var INTEGRITY_ERROR_CODE = "recording.deferred-transcode-integrity";
22801
+ function createIntegrityError(message) {
22802
+ const error = new Error(message);
22803
+ error.name = "DeferredTranscodeIntegrityError";
22804
+ error.code = INTEGRITY_ERROR_CODE;
22805
+ error.isRecoverable = true;
22806
+ return error;
22807
+ }
22808
+ function validatePacketSummary(summary, label) {
22809
+ if (!Number.isFinite(summary.averagePacketRate)) {
22810
+ throw createIntegrityError(`${label} video packet rate is invalid`);
22811
+ }
22812
+ if (!Number.isFinite(summary.duration)) {
22813
+ throw createIntegrityError(`${label} video duration is invalid`);
22814
+ }
22815
+ if (!Number.isFinite(summary.maximumPacketGap)) {
22816
+ throw createIntegrityError(`${label} video maximum packet gap is invalid`);
22817
+ }
22818
+ if (summary.averagePacketRate <= 0 || summary.duration <= 0) {
22819
+ throw createIntegrityError(`${label} video packet statistics are empty`);
22820
+ }
22821
+ if (summary.maximumPacketGap < 0) {
22822
+ throw createIntegrityError(`${label} video maximum packet gap is invalid`);
22823
+ }
22824
+ if (!Number.isInteger(summary.packetCount) || summary.packetCount <= 0) {
22825
+ throw createIntegrityError(`${label} video packet count is invalid`);
22826
+ }
22827
+ }
22828
+ function validateFrameCadence(summary, label, targetFrameRate) {
22829
+ const minimumPacketRate = targetFrameRate * MINIMUM_FRAME_DENSITY_RATIO;
22830
+ if (summary.averagePacketRate < minimumPacketRate) {
22831
+ throw createIntegrityError(`${label} video frame density is below the capture target`);
22832
+ }
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`);
22836
+ }
22837
+ }
22838
+ function validateDeferredVideoStats(validation) {
22839
+ if (!Number.isFinite(validation.targetFrameRate) || validation.targetFrameRate <= 0) {
22840
+ throw createIntegrityError("Deferred transcode requires a valid capture frame rate");
22841
+ }
22842
+ if (!(validation.source && validation.output)) {
22843
+ throw createIntegrityError("Deferred transcode source or output has no primary video track");
22844
+ }
22845
+ validatePacketSummary(validation.source, "Source");
22846
+ validatePacketSummary(validation.output, "Output");
22847
+ validateFrameCadence(validation.source, "Source", validation.targetFrameRate);
22848
+ validateFrameCadence(validation.output, "Output", validation.targetFrameRate);
22849
+ const durationDifference = Math.abs(validation.source.duration - validation.output.duration);
22850
+ const durationTolerance = Math.max(MINIMUM_DURATION_TOLERANCE_SECONDS, validation.source.duration * MAXIMUM_DURATION_DIFFERENCE_RATIO);
22851
+ if (durationDifference > durationTolerance) {
22852
+ throw createIntegrityError("Deferred transcode output duration differs materially from source");
22853
+ }
22854
+ const minimumPacketRate = validation.targetFrameRate * MINIMUM_FRAME_DENSITY_RATIO;
22855
+ const minimumPacketCount = Math.floor(validation.source.duration * minimumPacketRate);
22856
+ if (validation.output.averagePacketRate < minimumPacketRate || validation.output.packetCount < minimumPacketCount) {
22857
+ throw createIntegrityError("Deferred transcode output lost material video frame density");
22858
+ }
22859
+ }
22860
+ async function inspectVideoPackets(blob) {
22861
+ const input = new Input3({
22862
+ formats: ALL_FORMATS2,
22863
+ source: new BlobSource3(blob)
22864
+ });
22865
+ const videoTrack = await input.getPrimaryVideoTrack();
22866
+ if (videoTrack === null) {
22867
+ input.dispose();
22868
+ return null;
22869
+ }
22870
+ try {
22871
+ const packetSink = new EncodedPacketSink(videoTrack);
22872
+ const timestamps = [];
22873
+ let firstTimestamp = Number.POSITIVE_INFINITY;
22874
+ let lastEndTimestamp = Number.NEGATIVE_INFINITY;
22875
+ for await (const packet of packetSink.packets(undefined, undefined, {
22876
+ metadataOnly: true
22877
+ })) {
22878
+ timestamps.push(packet.timestamp);
22879
+ firstTimestamp = Math.min(firstTimestamp, packet.timestamp);
22880
+ lastEndTimestamp = Math.max(lastEndTimestamp, packet.timestamp + packet.duration);
22881
+ }
22882
+ timestamps.sort((left, right) => left - right);
22883
+ let maximumPacketGap = 0;
22884
+ for (let index = 1;index < timestamps.length; index += 1) {
22885
+ maximumPacketGap = Math.max(maximumPacketGap, timestamps[index] - timestamps[index - 1]);
22886
+ }
22887
+ const duration = lastEndTimestamp - firstTimestamp;
22888
+ const packetCount = timestamps.length;
22889
+ const averagePacketRate = packetCount > 0 ? packetCount / duration : 0;
22890
+ return {
22891
+ averagePacketRate,
22892
+ duration,
22893
+ maximumPacketGap,
22894
+ packetCount
22895
+ };
22896
+ } finally {
22897
+ input.dispose();
22898
+ }
22899
+ }
22900
+ async function inspectDeferredVideo(blob, label) {
22901
+ return await inspectVideoPackets(blob).catch((error) => {
22902
+ const message = extractErrorMessage(error);
22903
+ throw createIntegrityError(`Could not inspect ${label} deferred video: ${message}`);
22904
+ });
22905
+ }
22906
+ async function transcodeDeferredVideo(input, config, onProgress) {
22907
+ const targetFrameRate = config.fps;
22908
+ if (typeof targetFrameRate !== "number" || !Number.isFinite(targetFrameRate) || targetFrameRate <= 0) {
22909
+ throw createIntegrityError("Deferred transcode requires a valid capture frame rate");
22910
+ }
22911
+ const source = await inspectDeferredVideo(input, "source");
22912
+ validateDeferredVideoStats({
22913
+ output: source,
22914
+ source,
22915
+ targetFrameRate
22916
+ });
22917
+ const result = await transcodeVideo(input, config, onProgress);
22918
+ const output = await inspectDeferredVideo(result.blob, "output");
22919
+ validateDeferredVideoStats({ output, source, targetFrameRate });
22920
+ return result;
22921
+ }
22922
+
22754
22923
  // src/core/recording/recording-manager.ts
22755
22924
  var DEFAULT_COUNTDOWN_DURATION = 5000;
22756
22925
  var MILLISECONDS_PER_SECOND4 = 1000;
@@ -22759,12 +22928,15 @@ var RECORDING_TIMER_INTERVAL = 1000;
22759
22928
  var RECORDING_STATE_RECORDING = "recording";
22760
22929
  var RECORDING_STATE_IDLE = "idle";
22761
22930
  var RECORDING_STATE_COUNTDOWN = "countdown";
22931
+ var LOW_RESOURCE_SAFE_CAPTURE_FRAME_RATE2 = 20;
22932
+ var ANDROID_SAFE_CAPTURE_FRAME_RATE2 = 24;
22933
+ var ANDROID_POST_RECORDING_TRANSCODE_REASON2 = "android-post-recording-transcode";
22762
22934
  var DEFAULT_ROUTE_DECISION2 = {
22763
22935
  route: "local-webcodecs",
22764
22936
  reasonCodes: ["webcodecs-supported", "target-codec-supported"]
22765
22937
  };
22766
22938
  var DEFAULT_RECORDING_MANAGER_DEPENDENCIES = {
22767
- transcodeVideo
22939
+ transcodeVideo: transcodeDeferredVideo
22768
22940
  };
22769
22941
 
22770
22942
  class RecordingManager {
@@ -22999,7 +23171,7 @@ class RecordingManager {
22999
23171
  this.callbacks.onStopAudioTracking();
23000
23172
  logger.debug("[RecordingManager] Stopping recording in stream manager");
23001
23173
  const stopResult = await this.streamManager.stopRecording();
23002
- const finalBlob = await this.resolveFinalRecordingBlob(stopResult.blob);
23174
+ const finalBlob = await this.resolveFinalRecordingBlob(stopResult.blob, stopResult.captureFrameRate);
23003
23175
  logger.info("[RecordingManager] Recording stopped, blob size:", finalBlob.size);
23004
23176
  this.recordingState = RECORDING_STATE_IDLE;
23005
23177
  this.callbacks.onStateChange(this.recordingState);
@@ -23101,7 +23273,7 @@ class RecordingManager {
23101
23273
  this.recordingStartTime = null;
23102
23274
  this.maxTimeRemaining = null;
23103
23275
  }
23104
- async resolveFinalRecordingBlob(blob) {
23276
+ async resolveFinalRecordingBlob(blob, captureFrameRate) {
23105
23277
  if (this.activeRouteDecision.route !== "safe-capture-post-recording-transcode") {
23106
23278
  return blob;
23107
23279
  }
@@ -23110,7 +23282,8 @@ class RecordingManager {
23110
23282
  }
23111
23283
  const transcodeConfig = {
23112
23284
  ...this.activeRecordingConfig,
23113
- fps: undefined
23285
+ fps: this.resolveDeferredCaptureFrameRate(captureFrameRate),
23286
+ latencyMode: "quality"
23114
23287
  };
23115
23288
  this.callbacks.onTranscodingProgress?.(0);
23116
23289
  const result = await this.dependencies.transcodeVideo(blob, transcodeConfig, (progress) => {
@@ -23119,6 +23292,15 @@ class RecordingManager {
23119
23292
  this.callbacks.onTranscodingProgress?.(1);
23120
23293
  return result.blob;
23121
23294
  }
23295
+ resolveDeferredCaptureFrameRate(captureFrameRate) {
23296
+ if (typeof captureFrameRate === "number" && Number.isFinite(captureFrameRate) && captureFrameRate > 0) {
23297
+ return captureFrameRate;
23298
+ }
23299
+ if (this.activeRouteDecision.reasonCodes.includes(ANDROID_POST_RECORDING_TRANSCODE_REASON2)) {
23300
+ return ANDROID_SAFE_CAPTURE_FRAME_RATE2;
23301
+ }
23302
+ return LOW_RESOURCE_SAFE_CAPTURE_FRAME_RATE2;
23303
+ }
23122
23304
  resetPauseState() {
23123
23305
  this.isPaused = false;
23124
23306
  this.pauseStartTime = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vidtreo/recorder",
3
- "version": "1.7.9",
3
+ "version": "1.7.10",
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",