@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 +33 -2
- package/dist/index.js +194 -8
- package/package.json +1 -1
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
|
|
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
|
|
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?.({
|
|
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(
|
|
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.
|
|
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,
|
|
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.
|
|
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",
|