@vidtreo/recorder 1.7.10 → 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 +23 -4
- package/dist/index.js +314 -89
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -194,7 +194,7 @@ 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
198
|
|
|
199
199
|
export {};
|
|
200
200
|
|
|
@@ -1560,6 +1560,7 @@ import { type VisibilityInterval } from "../utils/tab-visibility-tracker";
|
|
|
1560
1560
|
export type StopRecordingResult = {
|
|
1561
1561
|
blob: Blob;
|
|
1562
1562
|
captureFrameRate?: number;
|
|
1563
|
+
expectedDurationSeconds?: number;
|
|
1563
1564
|
tabVisibilityIntervals: VisibilityInterval[];
|
|
1564
1565
|
recordingStats?: StreamRecordingStats;
|
|
1565
1566
|
encoderAcceleration?: VideoHardwareAcceleration;
|
|
@@ -1591,6 +1592,11 @@ export declare class StreamRecordingState {
|
|
|
1591
1592
|
private mediaRecorderStopReject;
|
|
1592
1593
|
private mediaRecorderError;
|
|
1593
1594
|
private safeCaptureFrameRate;
|
|
1595
|
+
private safeCaptureStartTime;
|
|
1596
|
+
private safeCaptureExpectedDurationSeconds;
|
|
1597
|
+
private mediaRecorderRequestedMimeType;
|
|
1598
|
+
private mediaRecorderGeneration;
|
|
1599
|
+
private isSafeCaptureSettled;
|
|
1594
1600
|
private mediaRecorderErrorTimeoutId;
|
|
1595
1601
|
private readonly streamManager;
|
|
1596
1602
|
private readonly dependencies;
|
|
@@ -1614,20 +1620,25 @@ export declare class StreamRecordingState {
|
|
|
1614
1620
|
private clearRecordingTimer;
|
|
1615
1621
|
private clearBufferSizeInterval;
|
|
1616
1622
|
private resetRecordingState;
|
|
1623
|
+
private resolveActiveRecordingDurationSeconds;
|
|
1617
1624
|
private startSafeCaptureRecording;
|
|
1625
|
+
private isActiveSafeCaptureGeneration;
|
|
1626
|
+
private createSafeCaptureRecorder;
|
|
1618
1627
|
private resolveEffectiveSafeCaptureFrameRate;
|
|
1619
1628
|
private applySafeCaptureTrackConstraints;
|
|
1620
1629
|
private resolveSafeCaptureVideoConstraints;
|
|
1621
1630
|
private stopSafeCaptureRecording;
|
|
1631
|
+
private cleanupSafeCaptureRecording;
|
|
1622
1632
|
private resolveSafeCaptureMimeType;
|
|
1623
|
-
private
|
|
1624
|
-
private
|
|
1633
|
+
private resolveRecordedMimeType;
|
|
1634
|
+
private resolveSafeCaptureMimeTypeCandidates;
|
|
1625
1635
|
private scheduleMediaRecorderErrorFinalizeTimeout;
|
|
1626
1636
|
private clearMediaRecorderErrorTimeout;
|
|
1627
1637
|
private createMediaRecorderError;
|
|
1628
1638
|
private createEmptyMediaRecorderError;
|
|
1629
1639
|
private updateMediaRecorderErrorDiagnostics;
|
|
1630
1640
|
private buildMediaRecorderDiagnostics;
|
|
1641
|
+
private assignSafeCaptureErrorDiagnostics;
|
|
1631
1642
|
private getMediaRecorderChunkSize;
|
|
1632
1643
|
private resetPauseState;
|
|
1633
1644
|
private resolveTabVisibilityOverlayText;
|
|
@@ -3175,12 +3186,20 @@ export type VideoPacketSummary = {
|
|
|
3175
3186
|
packetCount: number;
|
|
3176
3187
|
};
|
|
3177
3188
|
export type DeferredVideoStatsValidation = {
|
|
3189
|
+
expectedDurationSeconds: number;
|
|
3178
3190
|
output: VideoPacketSummary | null;
|
|
3179
3191
|
source: VideoPacketSummary | null;
|
|
3192
|
+
stage?: "source" | "output";
|
|
3180
3193
|
targetFrameRate: number;
|
|
3181
3194
|
};
|
|
3195
|
+
export type DeferredTranscodeContext = {
|
|
3196
|
+
expectedDurationSeconds: number;
|
|
3197
|
+
};
|
|
3198
|
+
export type DeferredTranscodeResult = TranscodeResult & {
|
|
3199
|
+
diagnostics: Record<string, string | number | boolean>;
|
|
3200
|
+
};
|
|
3182
3201
|
export declare function validateDeferredVideoStats(validation: DeferredVideoStatsValidation): void;
|
|
3183
|
-
export declare function transcodeDeferredVideo(input: Blob, config: TranscodeConfig, onProgress
|
|
3202
|
+
export declare function transcodeDeferredVideo(input: Blob, config: TranscodeConfig, onProgress: ((progress: number) => void) | undefined, context: DeferredTranscodeContext): Promise<DeferredTranscodeResult>;
|
|
3184
3203
|
|
|
3185
3204
|
export type VidtreoRecorderConfig = {
|
|
3186
3205
|
apiKey: string;
|
package/dist/index.js
CHANGED
|
@@ -5287,6 +5287,7 @@ var ANDROID_SAFE_CAPTURE_CONSTRAINTS = {
|
|
|
5287
5287
|
};
|
|
5288
5288
|
var LOW_RESOURCE_SAFE_CAPTURE_FRAME_RATE = 20;
|
|
5289
5289
|
var ANDROID_SAFE_CAPTURE_FRAME_RATE = 24;
|
|
5290
|
+
var SAFE_CAPTURE_TIMESLICE_MS = 1000;
|
|
5290
5291
|
var MEDIA_RECORDER_ERROR_FINALIZE_TIMEOUT_MS = 5000;
|
|
5291
5292
|
var ANDROID_POST_RECORDING_TRANSCODE_REASON = "android-post-recording-transcode";
|
|
5292
5293
|
var MEDIA_RECORDER_ERROR_CODE = "recording.mediarecorder-error";
|
|
@@ -5324,6 +5325,11 @@ class StreamRecordingState {
|
|
|
5324
5325
|
mediaRecorderStopReject = null;
|
|
5325
5326
|
mediaRecorderError = null;
|
|
5326
5327
|
safeCaptureFrameRate = null;
|
|
5328
|
+
safeCaptureStartTime = null;
|
|
5329
|
+
safeCaptureExpectedDurationSeconds = null;
|
|
5330
|
+
mediaRecorderRequestedMimeType = null;
|
|
5331
|
+
mediaRecorderGeneration = 0;
|
|
5332
|
+
isSafeCaptureSettled = false;
|
|
5327
5333
|
mediaRecorderErrorTimeoutId = null;
|
|
5328
5334
|
streamManager;
|
|
5329
5335
|
dependencies;
|
|
@@ -5466,7 +5472,8 @@ class StreamRecordingState {
|
|
|
5466
5472
|
this.startRecordingTimer();
|
|
5467
5473
|
}
|
|
5468
5474
|
async stopRecording() {
|
|
5469
|
-
const
|
|
5475
|
+
const stopTimestamp = this.dependencies.getCurrentTimestamp();
|
|
5476
|
+
const recordingElapsedSeconds = this.resolveActiveRecordingDurationSeconds(stopTimestamp);
|
|
5470
5477
|
logger.debug("[StreamRecordingState] stopRecording called", {
|
|
5471
5478
|
hasStreamProcessor: !!this.streamProcessor,
|
|
5472
5479
|
isRecording: this.isRecording(),
|
|
@@ -5497,11 +5504,12 @@ class StreamRecordingState {
|
|
|
5497
5504
|
}
|
|
5498
5505
|
let result;
|
|
5499
5506
|
if (this.activeRouteDecision.route === "safe-capture-post-recording-transcode") {
|
|
5500
|
-
const safeCaptureResult = await this.stopSafeCaptureRecording();
|
|
5507
|
+
const safeCaptureResult = await this.stopSafeCaptureRecording(recordingElapsedSeconds);
|
|
5501
5508
|
logger.debug("[StreamRecordingState] Finalizing MediaRecorder capture");
|
|
5502
5509
|
result = {
|
|
5503
5510
|
blob: safeCaptureResult.blob,
|
|
5504
5511
|
captureFrameRate: safeCaptureResult.captureFrameRate,
|
|
5512
|
+
expectedDurationSeconds: safeCaptureResult.expectedDurationSeconds,
|
|
5505
5513
|
mediaRecorderDiagnostics: safeCaptureResult.diagnostics
|
|
5506
5514
|
};
|
|
5507
5515
|
} else {
|
|
@@ -5520,6 +5528,7 @@ class StreamRecordingState {
|
|
|
5520
5528
|
return {
|
|
5521
5529
|
blob: result.blob,
|
|
5522
5530
|
captureFrameRate: result.captureFrameRate,
|
|
5531
|
+
expectedDurationSeconds: result.expectedDurationSeconds,
|
|
5523
5532
|
tabVisibilityIntervals,
|
|
5524
5533
|
recordingStats: result.recordingStats,
|
|
5525
5534
|
encoderAcceleration: result.encoderAcceleration,
|
|
@@ -5544,7 +5553,7 @@ class StreamRecordingState {
|
|
|
5544
5553
|
resumeRecording() {
|
|
5545
5554
|
if (this.pauseStartTime !== null) {
|
|
5546
5555
|
const pausedDuration = this.dependencies.getCurrentTimestamp() - this.pauseStartTime;
|
|
5547
|
-
this.totalPausedTime += pausedDuration;
|
|
5556
|
+
this.totalPausedTime += Math.max(0, pausedDuration);
|
|
5548
5557
|
this.pauseStartTime = null;
|
|
5549
5558
|
}
|
|
5550
5559
|
if (this.tabVisibilityTracker) {
|
|
@@ -5639,6 +5648,18 @@ class StreamRecordingState {
|
|
|
5639
5648
|
this.totalPausedTime = 0;
|
|
5640
5649
|
this.pauseStartTime = null;
|
|
5641
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
|
+
}
|
|
5642
5663
|
async startSafeCaptureRecording(mediaStream, config) {
|
|
5643
5664
|
if (typeof MediaRecorder === "undefined") {
|
|
5644
5665
|
throw createUnsupportedRouteError({
|
|
@@ -5654,9 +5675,13 @@ class StreamRecordingState {
|
|
|
5654
5675
|
this.safeCaptureFrameRate = this.resolveEffectiveSafeCaptureFrameRate(mediaStream, config);
|
|
5655
5676
|
this.mediaRecorderChunks = [];
|
|
5656
5677
|
this.mediaRecorderError = null;
|
|
5657
|
-
|
|
5658
|
-
|
|
5659
|
-
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;
|
|
5660
5685
|
this.mediaRecorder = recorder;
|
|
5661
5686
|
this.mediaRecorderStopPromise = new Promise((resolve, reject) => {
|
|
5662
5687
|
this.mediaRecorderStopResolve = resolve;
|
|
@@ -5666,40 +5691,76 @@ class StreamRecordingState {
|
|
|
5666
5691
|
return;
|
|
5667
5692
|
});
|
|
5668
5693
|
recorder.ondataavailable = (event) => {
|
|
5694
|
+
if (!this.isActiveSafeCaptureGeneration(generation)) {
|
|
5695
|
+
return;
|
|
5696
|
+
}
|
|
5669
5697
|
if (event.data.size > 0) {
|
|
5670
5698
|
this.mediaRecorderChunks.push(event.data);
|
|
5671
5699
|
}
|
|
5672
5700
|
};
|
|
5673
5701
|
recorder.onstop = () => {
|
|
5702
|
+
if (!this.isActiveSafeCaptureGeneration(generation)) {
|
|
5703
|
+
return;
|
|
5704
|
+
}
|
|
5705
|
+
this.isSafeCaptureSettled = true;
|
|
5674
5706
|
this.clearMediaRecorderErrorTimeout();
|
|
5675
5707
|
if (this.mediaRecorderError !== null) {
|
|
5676
5708
|
this.updateMediaRecorderErrorDiagnostics(recorder);
|
|
5677
5709
|
this.mediaRecorderStopReject?.(this.mediaRecorderError);
|
|
5678
5710
|
return;
|
|
5679
5711
|
}
|
|
5680
|
-
const mimeType =
|
|
5712
|
+
const mimeType = this.resolveRecordedMimeType(recorder);
|
|
5681
5713
|
const blob = new Blob(this.mediaRecorderChunks, { type: mimeType });
|
|
5682
5714
|
const diagnostics = this.buildMediaRecorderDiagnostics(recorder);
|
|
5683
5715
|
const captureFrameRate = this.safeCaptureFrameRate;
|
|
5716
|
+
let expectedDurationSeconds = this.safeCaptureExpectedDurationSeconds;
|
|
5717
|
+
if (expectedDurationSeconds === null) {
|
|
5718
|
+
expectedDurationSeconds = this.resolveActiveRecordingDurationSeconds(this.dependencies.getCurrentTimestamp());
|
|
5719
|
+
}
|
|
5684
5720
|
if (captureFrameRate === null) {
|
|
5685
5721
|
this.mediaRecorderStopReject?.(new Error("Safe capture frame rate is unavailable"));
|
|
5686
5722
|
return;
|
|
5687
5723
|
}
|
|
5688
5724
|
diagnostics.mediaRecorderCaptureFrameRate = captureFrameRate;
|
|
5725
|
+
diagnostics.mediaRecorderExpectedDurationSeconds = expectedDurationSeconds;
|
|
5689
5726
|
if (blob.size > 0) {
|
|
5690
5727
|
this.mediaRecorderStopResolve?.({
|
|
5691
5728
|
blob,
|
|
5692
5729
|
captureFrameRate,
|
|
5693
|
-
diagnostics
|
|
5730
|
+
diagnostics,
|
|
5731
|
+
expectedDurationSeconds
|
|
5694
5732
|
});
|
|
5695
5733
|
return;
|
|
5696
5734
|
}
|
|
5697
5735
|
this.mediaRecorderStopReject?.(this.createEmptyMediaRecorderError(recorder));
|
|
5698
5736
|
};
|
|
5699
5737
|
recorder.onerror = (event) => {
|
|
5738
|
+
if (!this.isActiveSafeCaptureGeneration(generation)) {
|
|
5739
|
+
return;
|
|
5740
|
+
}
|
|
5700
5741
|
this.mediaRecorderError = this.createMediaRecorderError(event, recorder);
|
|
5701
5742
|
};
|
|
5702
|
-
|
|
5743
|
+
this.safeCaptureStartTime = this.dependencies.getCurrentTimestamp();
|
|
5744
|
+
recorder.start(SAFE_CAPTURE_TIMESLICE_MS);
|
|
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);
|
|
5703
5764
|
}
|
|
5704
5765
|
resolveEffectiveSafeCaptureFrameRate(mediaStream, config) {
|
|
5705
5766
|
const videoTrack = mediaStream.getVideoTracks()[0];
|
|
@@ -5728,6 +5789,9 @@ class StreamRecordingState {
|
|
|
5728
5789
|
}
|
|
5729
5790
|
const videoTracks = mediaStream.getVideoTracks();
|
|
5730
5791
|
await Promise.all(videoTracks.map(async (track) => {
|
|
5792
|
+
if (typeof track.applyConstraints !== "function") {
|
|
5793
|
+
return;
|
|
5794
|
+
}
|
|
5731
5795
|
try {
|
|
5732
5796
|
await track.applyConstraints(constraints);
|
|
5733
5797
|
} catch (error) {
|
|
@@ -5745,36 +5809,62 @@ class StreamRecordingState {
|
|
|
5745
5809
|
}
|
|
5746
5810
|
return null;
|
|
5747
5811
|
}
|
|
5748
|
-
async stopSafeCaptureRecording() {
|
|
5812
|
+
async stopSafeCaptureRecording(expectedDurationSeconds) {
|
|
5749
5813
|
const recorder = this.mediaRecorder;
|
|
5750
5814
|
const stopPromise = this.mediaRecorderStopPromise;
|
|
5751
5815
|
if (!(recorder && stopPromise)) {
|
|
5752
5816
|
throw new Error("Safe capture recorder was not started");
|
|
5753
5817
|
}
|
|
5754
|
-
|
|
5755
|
-
|
|
5756
|
-
|
|
5757
|
-
|
|
5758
|
-
|
|
5759
|
-
|
|
5760
|
-
|
|
5761
|
-
|
|
5762
|
-
|
|
5763
|
-
|
|
5764
|
-
|
|
5765
|
-
|
|
5766
|
-
this.
|
|
5767
|
-
|
|
5768
|
-
|
|
5769
|
-
|
|
5770
|
-
|
|
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;
|
|
5771
5848
|
}
|
|
5772
5849
|
resolveSafeCaptureMimeType() {
|
|
5773
5850
|
const mediaStream = this.streamManager.getStream();
|
|
5774
5851
|
const hasVideo = (mediaStream?.getVideoTracks().length ?? 0) > 0;
|
|
5775
5852
|
return hasVideo ? "video/webm" : "audio/webm";
|
|
5776
5853
|
}
|
|
5777
|
-
|
|
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() {
|
|
5778
5868
|
const canUseMimeType = (mimeType) => {
|
|
5779
5869
|
if (typeof MediaRecorder.isTypeSupported !== "function") {
|
|
5780
5870
|
return false;
|
|
@@ -5791,22 +5881,7 @@ class StreamRecordingState {
|
|
|
5791
5881
|
"video/mp4;codecs=avc1.42001f,mp4a.40.2",
|
|
5792
5882
|
"video/mp4"
|
|
5793
5883
|
];
|
|
5794
|
-
|
|
5795
|
-
if (this.shouldPreferWebmSafeCapture(config)) {
|
|
5796
|
-
candidateMimeTypes = [
|
|
5797
|
-
...webmCandidateMimeTypes,
|
|
5798
|
-
...mp4CandidateMimeTypes
|
|
5799
|
-
];
|
|
5800
|
-
} else if (config.format !== "mp4") {
|
|
5801
|
-
candidateMimeTypes = webmCandidateMimeTypes;
|
|
5802
|
-
}
|
|
5803
|
-
return candidateMimeTypes.find(canUseMimeType);
|
|
5804
|
-
}
|
|
5805
|
-
shouldPreferWebmSafeCapture(config) {
|
|
5806
|
-
if (config.format !== "mp4") {
|
|
5807
|
-
return false;
|
|
5808
|
-
}
|
|
5809
|
-
return this.activeRouteDecision.reasonCodes.includes(ANDROID_POST_RECORDING_TRANSCODE_REASON);
|
|
5884
|
+
return [...webmCandidateMimeTypes, ...mp4CandidateMimeTypes].filter(canUseMimeType);
|
|
5810
5885
|
}
|
|
5811
5886
|
scheduleMediaRecorderErrorFinalizeTimeout(recorder) {
|
|
5812
5887
|
if (this.mediaRecorderErrorTimeoutId !== null) {
|
|
@@ -5833,9 +5908,10 @@ class StreamRecordingState {
|
|
|
5833
5908
|
const error = new Error("Browser recording failed before upload could start");
|
|
5834
5909
|
error.name = "MediaRecorderRecordingError";
|
|
5835
5910
|
error.code = MEDIA_RECORDER_ERROR_CODE;
|
|
5836
|
-
error.mediaRecorderMimeType = recorder
|
|
5911
|
+
error.mediaRecorderMimeType = this.resolveRecordedMimeType(recorder);
|
|
5837
5912
|
error.mediaRecorderChunkCount = this.mediaRecorderChunks.length;
|
|
5838
5913
|
error.mediaRecorderChunkSize = this.getMediaRecorderChunkSize();
|
|
5914
|
+
this.assignSafeCaptureErrorDiagnostics(error);
|
|
5839
5915
|
if (browserError?.name) {
|
|
5840
5916
|
error.mediaRecorderErrorName = browserError.name;
|
|
5841
5917
|
}
|
|
@@ -5848,9 +5924,10 @@ class StreamRecordingState {
|
|
|
5848
5924
|
const error = new Error("Browser recording produced no data before upload could start");
|
|
5849
5925
|
error.name = "MediaRecorderEmptyDataError";
|
|
5850
5926
|
error.code = MEDIA_RECORDER_EMPTY_ERROR_CODE;
|
|
5851
|
-
error.mediaRecorderMimeType = recorder
|
|
5927
|
+
error.mediaRecorderMimeType = this.resolveRecordedMimeType(recorder);
|
|
5852
5928
|
error.mediaRecorderChunkCount = this.mediaRecorderChunks.length;
|
|
5853
5929
|
error.mediaRecorderChunkSize = this.getMediaRecorderChunkSize();
|
|
5930
|
+
this.assignSafeCaptureErrorDiagnostics(error);
|
|
5854
5931
|
if (this.mediaRecorderError?.mediaRecorderErrorName) {
|
|
5855
5932
|
error.mediaRecorderErrorName = this.mediaRecorderError.mediaRecorderErrorName;
|
|
5856
5933
|
}
|
|
@@ -5863,17 +5940,21 @@ class StreamRecordingState {
|
|
|
5863
5940
|
if (this.mediaRecorderError === null) {
|
|
5864
5941
|
return;
|
|
5865
5942
|
}
|
|
5866
|
-
this.mediaRecorderError.mediaRecorderMimeType = recorder
|
|
5943
|
+
this.mediaRecorderError.mediaRecorderMimeType = this.resolveRecordedMimeType(recorder);
|
|
5867
5944
|
this.mediaRecorderError.mediaRecorderChunkCount = this.mediaRecorderChunks.length;
|
|
5868
5945
|
this.mediaRecorderError.mediaRecorderChunkSize = this.getMediaRecorderChunkSize();
|
|
5946
|
+
this.assignSafeCaptureErrorDiagnostics(this.mediaRecorderError);
|
|
5869
5947
|
}
|
|
5870
5948
|
buildMediaRecorderDiagnostics(recorder) {
|
|
5871
5949
|
const diagnostics = {
|
|
5872
|
-
mediaRecorderMimeType: recorder
|
|
5950
|
+
mediaRecorderMimeType: this.resolveRecordedMimeType(recorder),
|
|
5873
5951
|
mediaRecorderChunkCount: this.mediaRecorderChunks.length,
|
|
5874
5952
|
mediaRecorderChunkSize: this.getMediaRecorderChunkSize(),
|
|
5875
5953
|
mediaRecorderHadError: this.mediaRecorderError !== null
|
|
5876
5954
|
};
|
|
5955
|
+
if (this.mediaRecorderRequestedMimeType !== null) {
|
|
5956
|
+
diagnostics.mediaRecorderRequestedMimeType = this.mediaRecorderRequestedMimeType;
|
|
5957
|
+
}
|
|
5877
5958
|
if (this.mediaRecorderError?.mediaRecorderErrorName) {
|
|
5878
5959
|
diagnostics.mediaRecorderErrorName = this.mediaRecorderError.mediaRecorderErrorName;
|
|
5879
5960
|
}
|
|
@@ -5882,6 +5963,15 @@ class StreamRecordingState {
|
|
|
5882
5963
|
}
|
|
5883
5964
|
return diagnostics;
|
|
5884
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
|
+
}
|
|
5885
5975
|
getMediaRecorderChunkSize() {
|
|
5886
5976
|
return this.mediaRecorderChunks.reduce((total, chunk) => total + chunk.size, 0);
|
|
5887
5977
|
}
|
|
@@ -5965,6 +6055,12 @@ class StreamRecordingState {
|
|
|
5965
6055
|
this.mediaRecorderStopResolve = null;
|
|
5966
6056
|
this.mediaRecorderStopReject = null;
|
|
5967
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;
|
|
5968
6064
|
this.clearMediaRecorderErrorTimeout();
|
|
5969
6065
|
this.activeRouteDecision = DEFAULT_ROUTE_DECISION;
|
|
5970
6066
|
if (this.streamProcessor) {
|
|
@@ -6112,6 +6208,7 @@ var RECORDING_CODEC_UNSUPPORTED_CODES = new Set([
|
|
|
6112
6208
|
"recording.video-codec-unavailable"
|
|
6113
6209
|
]);
|
|
6114
6210
|
var RECORDING_SERVER_TRANSCODE_FALLBACK_CODES = new Set([
|
|
6211
|
+
"recording.deferred-transcode-integrity",
|
|
6115
6212
|
"recording.post-recording-transcode-fallback",
|
|
6116
6213
|
"transcode.post-recording-fallback",
|
|
6117
6214
|
"route.post-recording-transcode-fallback"
|
|
@@ -6354,7 +6451,7 @@ function resolveDeviceError(input) {
|
|
|
6354
6451
|
// package.json
|
|
6355
6452
|
var package_default = {
|
|
6356
6453
|
name: "@vidtreo/recorder",
|
|
6357
|
-
version: "1.7.
|
|
6454
|
+
version: "1.7.11",
|
|
6358
6455
|
type: "module",
|
|
6359
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.",
|
|
6360
6457
|
main: "./dist/index.js",
|
|
@@ -6439,12 +6536,23 @@ var MAX_RETRY_ATTEMPTS = 3;
|
|
|
6439
6536
|
var MAX_PENDING_EVENTS = 100;
|
|
6440
6537
|
var BRACKET_ERROR_CODE_PATTERN = /\[([a-z]+(?:[.-][a-z0-9]+)+)\]/i;
|
|
6441
6538
|
var INLINE_ERROR_CODE_PATTERN = /\b([a-z]+(?:[.-][a-z0-9]+)+)\b/i;
|
|
6442
|
-
var
|
|
6539
|
+
var RECORDING_INTEGRITY_ERROR_PROPERTY_KEYS = [
|
|
6443
6540
|
"mediaRecorderErrorName",
|
|
6444
6541
|
"mediaRecorderErrorMessage",
|
|
6445
6542
|
"mediaRecorderMimeType",
|
|
6446
6543
|
"mediaRecorderChunkCount",
|
|
6447
|
-
"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"
|
|
6448
6556
|
];
|
|
6449
6557
|
function resolveInstallationId(dependencies) {
|
|
6450
6558
|
const storageProvider = dependencies.storageProvider;
|
|
@@ -6800,7 +6908,7 @@ class TelemetryClient {
|
|
|
6800
6908
|
}
|
|
6801
6909
|
const candidate = error;
|
|
6802
6910
|
let properties = {};
|
|
6803
|
-
for (const key of
|
|
6911
|
+
for (const key of RECORDING_INTEGRITY_ERROR_PROPERTY_KEYS) {
|
|
6804
6912
|
const value = candidate[key];
|
|
6805
6913
|
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
6806
6914
|
properties = { ...properties, [key]: value };
|
|
@@ -22797,64 +22905,126 @@ var MAXIMUM_DURATION_DIFFERENCE_RATIO = 0.05;
|
|
|
22797
22905
|
var MINIMUM_DURATION_TOLERANCE_SECONDS = 0.25;
|
|
22798
22906
|
var MAXIMUM_FRAME_GAP_MULTIPLIER = 5;
|
|
22799
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;
|
|
22800
22913
|
var INTEGRITY_ERROR_CODE = "recording.deferred-transcode-integrity";
|
|
22801
|
-
function createIntegrityError(message) {
|
|
22914
|
+
function createIntegrityError(message, diagnostics = {}) {
|
|
22802
22915
|
const error = new Error(message);
|
|
22803
22916
|
error.name = "DeferredTranscodeIntegrityError";
|
|
22804
22917
|
error.code = INTEGRITY_ERROR_CODE;
|
|
22805
22918
|
error.isRecoverable = true;
|
|
22919
|
+
Object.assign(error, diagnostics);
|
|
22806
22920
|
return error;
|
|
22807
22921
|
}
|
|
22808
|
-
function validatePacketSummary(summary, label) {
|
|
22922
|
+
function validatePacketSummary(summary, label, diagnostics) {
|
|
22809
22923
|
if (!Number.isFinite(summary.averagePacketRate)) {
|
|
22810
|
-
throw createIntegrityError(`${label} video packet rate is invalid
|
|
22924
|
+
throw createIntegrityError(`${label} video packet rate is invalid`, diagnostics);
|
|
22811
22925
|
}
|
|
22812
22926
|
if (!Number.isFinite(summary.duration)) {
|
|
22813
|
-
throw createIntegrityError(`${label} video duration is invalid
|
|
22927
|
+
throw createIntegrityError(`${label} video duration is invalid`, diagnostics);
|
|
22814
22928
|
}
|
|
22815
22929
|
if (!Number.isFinite(summary.maximumPacketGap)) {
|
|
22816
|
-
throw createIntegrityError(`${label} video maximum packet gap is invalid
|
|
22930
|
+
throw createIntegrityError(`${label} video maximum packet gap is invalid`, diagnostics);
|
|
22817
22931
|
}
|
|
22818
22932
|
if (summary.averagePacketRate <= 0 || summary.duration <= 0) {
|
|
22819
|
-
throw createIntegrityError(`${label} video packet statistics are empty
|
|
22933
|
+
throw createIntegrityError(`${label} video packet statistics are empty`, diagnostics);
|
|
22820
22934
|
}
|
|
22821
22935
|
if (summary.maximumPacketGap < 0) {
|
|
22822
|
-
throw createIntegrityError(`${label} video maximum packet gap is invalid
|
|
22936
|
+
throw createIntegrityError(`${label} video maximum packet gap is invalid`, diagnostics);
|
|
22823
22937
|
}
|
|
22824
22938
|
if (!Number.isInteger(summary.packetCount) || summary.packetCount <= 0) {
|
|
22825
|
-
throw createIntegrityError(`${label} video packet count is invalid
|
|
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);
|
|
22826
22946
|
}
|
|
22827
22947
|
}
|
|
22828
|
-
function
|
|
22948
|
+
function validateOutputFrameDensity(summary, targetFrameRate, diagnostics) {
|
|
22829
22949
|
const minimumPacketRate = targetFrameRate * MINIMUM_FRAME_DENSITY_RATIO;
|
|
22830
22950
|
if (summary.averagePacketRate < minimumPacketRate) {
|
|
22831
|
-
throw createIntegrityError(
|
|
22951
|
+
throw createIntegrityError("Output video frame density is below the capture target", diagnostics);
|
|
22832
22952
|
}
|
|
22833
|
-
|
|
22834
|
-
|
|
22835
|
-
|
|
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);
|
|
22836
22993
|
}
|
|
22837
22994
|
}
|
|
22838
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
|
+
}
|
|
22839
23000
|
if (!Number.isFinite(validation.targetFrameRate) || validation.targetFrameRate <= 0) {
|
|
22840
|
-
throw createIntegrityError("Deferred transcode requires a valid capture frame rate");
|
|
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);
|
|
22841
23005
|
}
|
|
22842
|
-
|
|
22843
|
-
|
|
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);
|
|
22844
23014
|
}
|
|
22845
|
-
validatePacketSummary(validation.
|
|
22846
|
-
|
|
22847
|
-
|
|
22848
|
-
|
|
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);
|
|
22849
23019
|
const durationDifference = Math.abs(validation.source.duration - validation.output.duration);
|
|
22850
23020
|
const durationTolerance = Math.max(MINIMUM_DURATION_TOLERANCE_SECONDS, validation.source.duration * MAXIMUM_DURATION_DIFFERENCE_RATIO);
|
|
22851
23021
|
if (durationDifference > durationTolerance) {
|
|
22852
|
-
throw createIntegrityError("Deferred transcode output duration differs materially from source");
|
|
23022
|
+
throw createIntegrityError("Deferred transcode output duration differs materially from source", diagnostics);
|
|
22853
23023
|
}
|
|
22854
23024
|
const minimumPacketRate = validation.targetFrameRate * MINIMUM_FRAME_DENSITY_RATIO;
|
|
22855
23025
|
const minimumPacketCount = Math.floor(validation.source.duration * minimumPacketRate);
|
|
22856
23026
|
if (validation.output.averagePacketRate < minimumPacketRate || validation.output.packetCount < minimumPacketCount) {
|
|
22857
|
-
throw createIntegrityError("Deferred transcode output lost material video frame density");
|
|
23027
|
+
throw createIntegrityError("Deferred transcode output lost material video frame density", diagnostics);
|
|
22858
23028
|
}
|
|
22859
23029
|
}
|
|
22860
23030
|
async function inspectVideoPackets(blob) {
|
|
@@ -22897,27 +23067,72 @@ async function inspectVideoPackets(blob) {
|
|
|
22897
23067
|
input.dispose();
|
|
22898
23068
|
}
|
|
22899
23069
|
}
|
|
22900
|
-
async function inspectDeferredVideo(blob, label) {
|
|
23070
|
+
async function inspectDeferredVideo(blob, label, diagnostics) {
|
|
22901
23071
|
return await inspectVideoPackets(blob).catch((error) => {
|
|
22902
23072
|
const message = extractErrorMessage(error);
|
|
22903
|
-
throw createIntegrityError(`Could not inspect ${label} deferred video: ${message}
|
|
23073
|
+
throw createIntegrityError(`Could not inspect ${label} deferred video: ${message}`, diagnostics);
|
|
22904
23074
|
});
|
|
22905
23075
|
}
|
|
22906
|
-
async function transcodeDeferredVideo(input, config, onProgress) {
|
|
23076
|
+
async function transcodeDeferredVideo(input, config, onProgress, context) {
|
|
22907
23077
|
const targetFrameRate = config.fps;
|
|
22908
23078
|
if (typeof targetFrameRate !== "number" || !Number.isFinite(targetFrameRate) || targetFrameRate <= 0) {
|
|
22909
|
-
throw createIntegrityError("Deferred transcode requires a valid capture frame rate"
|
|
23079
|
+
throw createIntegrityError("Deferred transcode requires a valid capture frame rate", {
|
|
23080
|
+
deferredIntegrityStage: "source",
|
|
23081
|
+
mediaRecorderExpectedDurationSeconds: context.expectedDurationSeconds
|
|
23082
|
+
});
|
|
22910
23083
|
}
|
|
22911
|
-
const source = await inspectDeferredVideo(input, "source"
|
|
23084
|
+
const source = await inspectDeferredVideo(input, "source", {
|
|
23085
|
+
deferredIntegrityStage: "source",
|
|
23086
|
+
mediaRecorderExpectedDurationSeconds: context.expectedDurationSeconds
|
|
23087
|
+
});
|
|
22912
23088
|
validateDeferredVideoStats({
|
|
22913
|
-
|
|
23089
|
+
expectedDurationSeconds: context.expectedDurationSeconds,
|
|
23090
|
+
output: null,
|
|
22914
23091
|
source,
|
|
23092
|
+
stage: "source",
|
|
22915
23093
|
targetFrameRate
|
|
22916
23094
|
});
|
|
22917
|
-
const
|
|
22918
|
-
|
|
22919
|
-
|
|
22920
|
-
|
|
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
|
+
};
|
|
22921
23136
|
}
|
|
22922
23137
|
|
|
22923
23138
|
// src/core/recording/recording-manager.ts
|
|
@@ -23171,7 +23386,8 @@ class RecordingManager {
|
|
|
23171
23386
|
this.callbacks.onStopAudioTracking();
|
|
23172
23387
|
logger.debug("[RecordingManager] Stopping recording in stream manager");
|
|
23173
23388
|
const stopResult = await this.streamManager.stopRecording();
|
|
23174
|
-
const
|
|
23389
|
+
const finalResult = await this.resolveFinalRecordingBlob(stopResult.blob, stopResult.captureFrameRate, stopResult.expectedDurationSeconds);
|
|
23390
|
+
const finalBlob = finalResult.blob;
|
|
23175
23391
|
logger.info("[RecordingManager] Recording stopped, blob size:", finalBlob.size);
|
|
23176
23392
|
this.recordingState = RECORDING_STATE_IDLE;
|
|
23177
23393
|
this.callbacks.onStateChange(this.recordingState);
|
|
@@ -23193,6 +23409,9 @@ class RecordingManager {
|
|
|
23193
23409
|
if (stopResult.mediaRecorderDiagnostics !== undefined) {
|
|
23194
23410
|
Object.assign(telemetryProperties, stopResult.mediaRecorderDiagnostics);
|
|
23195
23411
|
}
|
|
23412
|
+
if (finalResult.diagnostics !== undefined) {
|
|
23413
|
+
Object.assign(telemetryProperties, finalResult.diagnostics);
|
|
23414
|
+
}
|
|
23196
23415
|
return {
|
|
23197
23416
|
blob: finalBlob,
|
|
23198
23417
|
telemetryProperties
|
|
@@ -23273,13 +23492,16 @@ class RecordingManager {
|
|
|
23273
23492
|
this.recordingStartTime = null;
|
|
23274
23493
|
this.maxTimeRemaining = null;
|
|
23275
23494
|
}
|
|
23276
|
-
async resolveFinalRecordingBlob(blob, captureFrameRate) {
|
|
23495
|
+
async resolveFinalRecordingBlob(blob, captureFrameRate, expectedDurationSeconds) {
|
|
23277
23496
|
if (this.activeRouteDecision.route !== "safe-capture-post-recording-transcode") {
|
|
23278
|
-
return blob;
|
|
23497
|
+
return { blob };
|
|
23279
23498
|
}
|
|
23280
23499
|
if (!this.activeRecordingConfig) {
|
|
23281
23500
|
throw new Error("Recording config unavailable for final video processing");
|
|
23282
23501
|
}
|
|
23502
|
+
if (typeof expectedDurationSeconds !== "number" || !Number.isFinite(expectedDurationSeconds) || expectedDurationSeconds < 0) {
|
|
23503
|
+
throw new Error("Recording active duration unavailable for final video processing");
|
|
23504
|
+
}
|
|
23283
23505
|
const transcodeConfig = {
|
|
23284
23506
|
...this.activeRecordingConfig,
|
|
23285
23507
|
fps: this.resolveDeferredCaptureFrameRate(captureFrameRate),
|
|
@@ -23288,9 +23510,12 @@ class RecordingManager {
|
|
|
23288
23510
|
this.callbacks.onTranscodingProgress?.(0);
|
|
23289
23511
|
const result = await this.dependencies.transcodeVideo(blob, transcodeConfig, (progress) => {
|
|
23290
23512
|
this.callbacks.onTranscodingProgress?.(progress);
|
|
23291
|
-
});
|
|
23513
|
+
}, { expectedDurationSeconds });
|
|
23292
23514
|
this.callbacks.onTranscodingProgress?.(1);
|
|
23293
|
-
return
|
|
23515
|
+
return {
|
|
23516
|
+
blob: result.blob,
|
|
23517
|
+
diagnostics: result.diagnostics
|
|
23518
|
+
};
|
|
23294
23519
|
}
|
|
23295
23520
|
resolveDeferredCaptureFrameRate(captureFrameRate) {
|
|
23296
23521
|
if (typeof captureFrameRate === "number" && Number.isFinite(captureFrameRate) && captureFrameRate > 0) {
|
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",
|