@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 +44 -5
- package/dist/index.js +464 -57
- package/package.json +1 -1
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
|
|
1619
|
-
private
|
|
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
|
|
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
|
|
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
|
-
|
|
5650
|
-
|
|
5651
|
-
const
|
|
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 =
|
|
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?.({
|
|
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
|
-
|
|
5717
|
-
|
|
5718
|
-
|
|
5719
|
-
|
|
5720
|
-
|
|
5721
|
-
|
|
5722
|
-
|
|
5723
|
-
|
|
5724
|
-
|
|
5725
|
-
|
|
5726
|
-
|
|
5727
|
-
|
|
5728
|
-
this.
|
|
5729
|
-
|
|
5730
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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.
|
|
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",
|