@vidtreo/recorder 1.7.8 → 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
@@ -185,6 +185,19 @@ export {};
185
185
 
186
186
  export {};
187
187
 
188
+ export type MediaStats = {
189
+ audioChannels: number;
190
+ audioDuration: number;
191
+ audioSampleRate: number;
192
+ videoFrameCount: number;
193
+ videoFrameRate: number;
194
+ videoDuration: number;
195
+ };
196
+ export declare function inspectMedia(blob: Blob): Promise<MediaStats>;
197
+ export declare function transcodePreservingCadence(blob: Blob): Promise<Blob>;
198
+
199
+ export {};
200
+
188
201
  export {};
189
202
 
190
203
  export {};
@@ -1546,6 +1559,7 @@ import type { TranscodeConfig } from "../transcode/transcode-types";
1546
1559
  import { type VisibilityInterval } from "../utils/tab-visibility-tracker";
1547
1560
  export type StopRecordingResult = {
1548
1561
  blob: Blob;
1562
+ captureFrameRate?: number;
1549
1563
  tabVisibilityIntervals: VisibilityInterval[];
1550
1564
  recordingStats?: StreamRecordingStats;
1551
1565
  encoderAcceleration?: VideoHardwareAcceleration;
@@ -1576,6 +1590,7 @@ export declare class StreamRecordingState {
1576
1590
  private mediaRecorderStopResolve;
1577
1591
  private mediaRecorderStopReject;
1578
1592
  private mediaRecorderError;
1593
+ private safeCaptureFrameRate;
1579
1594
  private mediaRecorderErrorTimeoutId;
1580
1595
  private readonly streamManager;
1581
1596
  private readonly dependencies;
@@ -1600,6 +1615,7 @@ export declare class StreamRecordingState {
1600
1615
  private clearBufferSizeInterval;
1601
1616
  private resetRecordingState;
1602
1617
  private startSafeCaptureRecording;
1618
+ private resolveEffectiveSafeCaptureFrameRate;
1603
1619
  private applySafeCaptureTrackConstraints;
1604
1620
  private resolveSafeCaptureVideoConstraints;
1605
1621
  private stopSafeCaptureRecording;
@@ -1919,13 +1935,13 @@ export declare class RecorderController {
1919
1935
 
1920
1936
  import type { CameraStreamManager } from "../stream/stream";
1921
1937
  import { StreamProcessor } from "../stream/stream-processor";
1938
+ import { transcodeDeferredVideo } from "../transcode/deferred-video-transcoder";
1922
1939
  import type { TranscodeConfig } from "../transcode/transcode-types";
1923
- import { transcodeVideo } from "../transcode/video-transcoder";
1924
1940
  export type RecordingStopResult = {
1925
1941
  blob: Blob;
1926
1942
  telemetryProperties: Record<string, unknown>;
1927
1943
  };
1928
- type TranscodeVideoDependency = typeof transcodeVideo;
1944
+ type TranscodeVideoDependency = typeof transcodeDeferredVideo;
1929
1945
  type RecordingManagerDependencies = {
1930
1946
  transcodeVideo: TranscodeVideoDependency;
1931
1947
  };
@@ -1979,6 +1995,7 @@ export declare class RecordingManager {
1979
1995
  cleanup(): void;
1980
1996
  private resetRecordingState;
1981
1997
  private resolveFinalRecordingBlob;
1998
+ private resolveDeferredCaptureFrameRate;
1982
1999
  private resetPauseState;
1983
2000
  private getOrCreateStreamProcessor;
1984
2001
  private resolveRouteDecision;
@@ -3151,6 +3168,20 @@ export declare function getFileExtensionForOutputFormat(format: OutputFormat): s
3151
3168
  export declare function transcodeVideo(input: TranscodeInput, config?: Partial<TranscodeConfig>, onProgress?: (progress: number) => void): Promise<TranscodeResult>;
3152
3169
  export declare function transcodeVideoForNativeCamera(file: File, config?: Partial<TranscodeConfig>, onProgress?: (progress: number) => void): Promise<TranscodeResult>;
3153
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
+
3154
3185
  export type VidtreoRecorderConfig = {
3155
3186
  apiKey: string;
3156
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.8",
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,20 +23273,34 @@ 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
  }
23108
23280
  if (!this.activeRecordingConfig) {
23109
23281
  throw new Error("Recording config unavailable for final video processing");
23110
23282
  }
23283
+ const transcodeConfig = {
23284
+ ...this.activeRecordingConfig,
23285
+ fps: this.resolveDeferredCaptureFrameRate(captureFrameRate),
23286
+ latencyMode: "quality"
23287
+ };
23111
23288
  this.callbacks.onTranscodingProgress?.(0);
23112
- const result = await this.dependencies.transcodeVideo(blob, this.activeRecordingConfig, (progress) => {
23289
+ const result = await this.dependencies.transcodeVideo(blob, transcodeConfig, (progress) => {
23113
23290
  this.callbacks.onTranscodingProgress?.(progress);
23114
23291
  });
23115
23292
  this.callbacks.onTranscodingProgress?.(1);
23116
23293
  return result.blob;
23117
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
+ }
23118
23304
  resetPauseState() {
23119
23305
  this.isPaused = false;
23120
23306
  this.pauseStartTime = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vidtreo/recorder",
3
- "version": "1.7.8",
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",