@vidtreo/recorder 1.6.3 → 1.7.1
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 +1963 -1946
- package/dist/index.js +237 -23
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1476,6 +1476,7 @@ async function buildDeviceCapabilityProfile(options = {}) {
|
|
|
1476
1476
|
// src/core/routing/recording-route-decision.ts
|
|
1477
1477
|
var DEFAULT_MIN_DEVICE_MEMORY_GB = 4;
|
|
1478
1478
|
var DEFAULT_MIN_HARDWARE_CONCURRENCY = 5;
|
|
1479
|
+
var PLATFORM_KEYWORD_ANDROID2 = "android";
|
|
1479
1480
|
var UNSUPPORTED_GUIDANCE_MESSAGE = "Recording is not supported in this browser context. Use a secure connection, go online, and try a current Chrome or Edge browser.";
|
|
1480
1481
|
function hasSufficientDeviceMemory(profile, minDeviceMemory) {
|
|
1481
1482
|
const deviceMemory = profile.resources.deviceMemory;
|
|
@@ -1530,6 +1531,17 @@ function buildFallbackRouteBlockers(profile) {
|
|
|
1530
1531
|
}
|
|
1531
1532
|
return reasonCodes;
|
|
1532
1533
|
}
|
|
1534
|
+
function isAndroidProfile(profile) {
|
|
1535
|
+
return profile.os.name.toLowerCase().includes(PLATFORM_KEYWORD_ANDROID2);
|
|
1536
|
+
}
|
|
1537
|
+
function buildPostRecordingTranscodeReasonCodes(profile, localRouteBlockers) {
|
|
1538
|
+
const reasonCodes = [...localRouteBlockers];
|
|
1539
|
+
if (isAndroidProfile(profile)) {
|
|
1540
|
+
reasonCodes.push("android-post-recording-transcode");
|
|
1541
|
+
}
|
|
1542
|
+
reasonCodes.push("mediarecorder-supported");
|
|
1543
|
+
return reasonCodes;
|
|
1544
|
+
}
|
|
1533
1545
|
function hasSupportedEncodingCapability(selectedCodec) {
|
|
1534
1546
|
const encodingCapability = selectedCodec?.encodingCapability;
|
|
1535
1547
|
return encodingCapability === undefined || encodingCapability.errorCode !== undefined || encodingCapability.supported === true;
|
|
@@ -1554,6 +1566,13 @@ function decideRecordingRoute(options) {
|
|
|
1554
1566
|
const hasSmoothEncoding = hasSmoothEncodingCapability(selectedCodec);
|
|
1555
1567
|
const hasPowerEfficientEncoding = hasPowerEfficientEncodingCapability(selectedCodec);
|
|
1556
1568
|
const localRouteBlockers = buildLocalRouteBlockers(options.profile, hasSupportedTargetCodec, hasEnoughDeviceMemory, hasEnoughHardwareConcurrency, hasSupportedEncoding, hasSmoothEncoding, hasPowerEfficientEncoding);
|
|
1569
|
+
if (isAndroidProfile(options.profile) && canUsePostRecordingTranscodeRoute(options.profile, hasSupportedTargetCodec)) {
|
|
1570
|
+
return {
|
|
1571
|
+
route: "safe-capture-post-recording-transcode",
|
|
1572
|
+
reasonCodes: buildPostRecordingTranscodeReasonCodes(options.profile, localRouteBlockers),
|
|
1573
|
+
selectedCodecId
|
|
1574
|
+
};
|
|
1575
|
+
}
|
|
1557
1576
|
if (options.profile.features.isSecureContext && options.profile.features.hasVideoEncoder && hasSupportedTargetCodec && options.profile.network.online !== false && hasEnoughDeviceMemory && hasEnoughHardwareConcurrency && hasSupportedEncoding && hasSmoothEncoding && hasPowerEfficientEncoding) {
|
|
1558
1577
|
return {
|
|
1559
1578
|
route: "local-webcodecs",
|
|
@@ -1564,7 +1583,7 @@ function decideRecordingRoute(options) {
|
|
|
1564
1583
|
if (canUsePostRecordingTranscodeRoute(options.profile, hasSupportedTargetCodec)) {
|
|
1565
1584
|
return {
|
|
1566
1585
|
route: "safe-capture-post-recording-transcode",
|
|
1567
|
-
reasonCodes:
|
|
1586
|
+
reasonCodes: buildPostRecordingTranscodeReasonCodes(options.profile, localRouteBlockers),
|
|
1568
1587
|
selectedCodecId
|
|
1569
1588
|
};
|
|
1570
1589
|
}
|
|
@@ -1758,6 +1777,12 @@ async function resolveAudioCodecFromPolicy(options) {
|
|
|
1758
1777
|
if (options.bitrate !== undefined) {
|
|
1759
1778
|
audioCodecOptions.bitrate = options.bitrate;
|
|
1760
1779
|
}
|
|
1780
|
+
if (options.numberOfChannels !== undefined) {
|
|
1781
|
+
audioCodecOptions.numberOfChannels = options.numberOfChannels;
|
|
1782
|
+
}
|
|
1783
|
+
if (options.sampleRate !== undefined) {
|
|
1784
|
+
audioCodecOptions.sampleRate = options.sampleRate;
|
|
1785
|
+
}
|
|
1761
1786
|
const probeResults = await Promise.allSettled([
|
|
1762
1787
|
getFirstEncodableAudioCodec(candidates, audioCodecOptions)
|
|
1763
1788
|
]);
|
|
@@ -5246,6 +5271,16 @@ var LOW_RESOURCE_SAFE_CAPTURE_CONSTRAINTS = {
|
|
|
5246
5271
|
height: { ideal: 480, max: 480 },
|
|
5247
5272
|
frameRate: { ideal: 20, max: 20 }
|
|
5248
5273
|
};
|
|
5274
|
+
var ANDROID_SAFE_CAPTURE_CONSTRAINTS = {
|
|
5275
|
+
width: { ideal: 1280, max: 1280 },
|
|
5276
|
+
height: { ideal: 720, max: 720 },
|
|
5277
|
+
frameRate: { ideal: 24, max: 24 }
|
|
5278
|
+
};
|
|
5279
|
+
var SAFE_CAPTURE_TIMESLICE_MS = 1000;
|
|
5280
|
+
var MEDIA_RECORDER_ERROR_FINALIZE_TIMEOUT_MS = 5000;
|
|
5281
|
+
var ANDROID_POST_RECORDING_TRANSCODE_REASON = "android-post-recording-transcode";
|
|
5282
|
+
var MEDIA_RECORDER_ERROR_CODE = "recording.mediarecorder-error";
|
|
5283
|
+
var MEDIA_RECORDER_EMPTY_ERROR_CODE = "recording.mediarecorder-empty";
|
|
5249
5284
|
function createUnsupportedRouteError(routeDecision) {
|
|
5250
5285
|
const guidance = routeDecision.guidance?.message ?? "This browser or device cannot safely record video. Try a supported browser or upload an existing video file.";
|
|
5251
5286
|
const error = new Error(guidance);
|
|
@@ -5277,6 +5312,8 @@ class StreamRecordingState {
|
|
|
5277
5312
|
mediaRecorderStopPromise = null;
|
|
5278
5313
|
mediaRecorderStopResolve = null;
|
|
5279
5314
|
mediaRecorderStopReject = null;
|
|
5315
|
+
mediaRecorderError = null;
|
|
5316
|
+
mediaRecorderErrorTimeoutId = null;
|
|
5280
5317
|
streamManager;
|
|
5281
5318
|
dependencies;
|
|
5282
5319
|
constructor(streamManager, dependencies) {
|
|
@@ -5449,9 +5486,11 @@ class StreamRecordingState {
|
|
|
5449
5486
|
}
|
|
5450
5487
|
let result;
|
|
5451
5488
|
if (this.activeRouteDecision.route === "safe-capture-post-recording-transcode") {
|
|
5489
|
+
const safeCaptureResult = await this.stopSafeCaptureRecording();
|
|
5452
5490
|
logger.debug("[StreamRecordingState] Finalizing MediaRecorder capture");
|
|
5453
5491
|
result = {
|
|
5454
|
-
blob:
|
|
5492
|
+
blob: safeCaptureResult.blob,
|
|
5493
|
+
mediaRecorderDiagnostics: safeCaptureResult.diagnostics
|
|
5455
5494
|
};
|
|
5456
5495
|
} else {
|
|
5457
5496
|
logger.debug("[StreamRecordingState] Finalizing stream processor");
|
|
@@ -5470,7 +5509,8 @@ class StreamRecordingState {
|
|
|
5470
5509
|
blob: result.blob,
|
|
5471
5510
|
tabVisibilityIntervals,
|
|
5472
5511
|
recordingStats: result.recordingStats,
|
|
5473
|
-
encoderAcceleration: result.encoderAcceleration
|
|
5512
|
+
encoderAcceleration: result.encoderAcceleration,
|
|
5513
|
+
mediaRecorderDiagnostics: result.mediaRecorderDiagnostics
|
|
5474
5514
|
};
|
|
5475
5515
|
}
|
|
5476
5516
|
pauseRecording() {
|
|
@@ -5481,6 +5521,9 @@ class StreamRecordingState {
|
|
|
5481
5521
|
if (this.tabVisibilityTracker) {
|
|
5482
5522
|
this.tabVisibilityTracker.pause();
|
|
5483
5523
|
}
|
|
5524
|
+
if (this.mediaRecorder?.state === "recording") {
|
|
5525
|
+
this.mediaRecorder.pause?.();
|
|
5526
|
+
}
|
|
5484
5527
|
if (this.streamProcessor && this.isRecording()) {
|
|
5485
5528
|
this.streamProcessor.pause();
|
|
5486
5529
|
}
|
|
@@ -5494,6 +5537,9 @@ class StreamRecordingState {
|
|
|
5494
5537
|
if (this.tabVisibilityTracker) {
|
|
5495
5538
|
this.tabVisibilityTracker.resume();
|
|
5496
5539
|
}
|
|
5540
|
+
if (this.mediaRecorder?.state === "paused") {
|
|
5541
|
+
this.mediaRecorder.resume?.();
|
|
5542
|
+
}
|
|
5497
5543
|
this.startRecordingTimer();
|
|
5498
5544
|
if (this.streamProcessor && this.isRecording()) {
|
|
5499
5545
|
this.streamProcessor.resume();
|
|
@@ -5593,6 +5639,7 @@ class StreamRecordingState {
|
|
|
5593
5639
|
}
|
|
5594
5640
|
await this.applySafeCaptureTrackConstraints(mediaStream);
|
|
5595
5641
|
this.mediaRecorderChunks = [];
|
|
5642
|
+
this.mediaRecorderError = null;
|
|
5596
5643
|
const preferredMimeType = this.resolvePreferredSafeCaptureMimeType(config);
|
|
5597
5644
|
const recorderOptions = preferredMimeType === undefined ? undefined : { mimeType: preferredMimeType };
|
|
5598
5645
|
const recorder = new MediaRecorder(mediaStream, recorderOptions);
|
|
@@ -5601,20 +5648,34 @@ class StreamRecordingState {
|
|
|
5601
5648
|
this.mediaRecorderStopResolve = resolve;
|
|
5602
5649
|
this.mediaRecorderStopReject = reject;
|
|
5603
5650
|
});
|
|
5651
|
+
this.mediaRecorderStopPromise.catch(() => {
|
|
5652
|
+
return;
|
|
5653
|
+
});
|
|
5604
5654
|
recorder.ondataavailable = (event) => {
|
|
5605
5655
|
if (event.data.size > 0) {
|
|
5606
5656
|
this.mediaRecorderChunks.push(event.data);
|
|
5607
5657
|
}
|
|
5608
5658
|
};
|
|
5609
5659
|
recorder.onstop = () => {
|
|
5660
|
+
this.clearMediaRecorderErrorTimeout();
|
|
5661
|
+
if (this.mediaRecorderError !== null) {
|
|
5662
|
+
this.updateMediaRecorderErrorDiagnostics(recorder);
|
|
5663
|
+
this.mediaRecorderStopReject?.(this.mediaRecorderError);
|
|
5664
|
+
return;
|
|
5665
|
+
}
|
|
5610
5666
|
const mimeType = recorder.mimeType || this.resolveSafeCaptureMimeType();
|
|
5611
5667
|
const blob = new Blob(this.mediaRecorderChunks, { type: mimeType });
|
|
5612
|
-
this.
|
|
5668
|
+
const diagnostics = this.buildMediaRecorderDiagnostics(recorder);
|
|
5669
|
+
if (blob.size > 0) {
|
|
5670
|
+
this.mediaRecorderStopResolve?.({ blob, diagnostics });
|
|
5671
|
+
return;
|
|
5672
|
+
}
|
|
5673
|
+
this.mediaRecorderStopReject?.(this.createEmptyMediaRecorderError(recorder));
|
|
5613
5674
|
};
|
|
5614
|
-
recorder.onerror = () => {
|
|
5615
|
-
this.
|
|
5675
|
+
recorder.onerror = (event) => {
|
|
5676
|
+
this.mediaRecorderError = this.createMediaRecorderError(event, recorder);
|
|
5616
5677
|
};
|
|
5617
|
-
recorder.start();
|
|
5678
|
+
recorder.start(SAFE_CAPTURE_TIMESLICE_MS);
|
|
5618
5679
|
}
|
|
5619
5680
|
async applySafeCaptureTrackConstraints(mediaStream) {
|
|
5620
5681
|
const constraints = this.resolveSafeCaptureVideoConstraints();
|
|
@@ -5635,6 +5696,9 @@ class StreamRecordingState {
|
|
|
5635
5696
|
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")) {
|
|
5636
5697
|
return LOW_RESOURCE_SAFE_CAPTURE_CONSTRAINTS;
|
|
5637
5698
|
}
|
|
5699
|
+
if (reasonCodes.includes(ANDROID_POST_RECORDING_TRANSCODE_REASON)) {
|
|
5700
|
+
return ANDROID_SAFE_CAPTURE_CONSTRAINTS;
|
|
5701
|
+
}
|
|
5638
5702
|
return null;
|
|
5639
5703
|
}
|
|
5640
5704
|
async stopSafeCaptureRecording() {
|
|
@@ -5646,6 +5710,7 @@ class StreamRecordingState {
|
|
|
5646
5710
|
if (recorder.state !== "inactive") {
|
|
5647
5711
|
recorder.stop();
|
|
5648
5712
|
}
|
|
5713
|
+
this.scheduleMediaRecorderErrorFinalizeTimeout(recorder);
|
|
5649
5714
|
try {
|
|
5650
5715
|
return await stopPromise;
|
|
5651
5716
|
} finally {
|
|
@@ -5654,6 +5719,8 @@ class StreamRecordingState {
|
|
|
5654
5719
|
this.mediaRecorderStopPromise = null;
|
|
5655
5720
|
this.mediaRecorderStopResolve = null;
|
|
5656
5721
|
this.mediaRecorderStopReject = null;
|
|
5722
|
+
this.mediaRecorderError = null;
|
|
5723
|
+
this.clearMediaRecorderErrorTimeout();
|
|
5657
5724
|
this.activeRouteDecision = DEFAULT_ROUTE_DECISION;
|
|
5658
5725
|
}
|
|
5659
5726
|
}
|
|
@@ -5669,17 +5736,110 @@ class StreamRecordingState {
|
|
|
5669
5736
|
}
|
|
5670
5737
|
return MediaRecorder.isTypeSupported(mimeType);
|
|
5671
5738
|
};
|
|
5672
|
-
const
|
|
5673
|
-
"video/mp4;codecs=avc1.42E01E,mp4a.40.2",
|
|
5674
|
-
"video/mp4;codecs=avc1.42001f,mp4a.40.2",
|
|
5675
|
-
"video/mp4"
|
|
5676
|
-
] : [
|
|
5739
|
+
const webmCandidateMimeTypes = [
|
|
5677
5740
|
"video/webm;codecs=vp8,opus",
|
|
5678
5741
|
"video/webm;codecs=vp9,opus",
|
|
5679
5742
|
"video/webm"
|
|
5680
5743
|
];
|
|
5744
|
+
const mp4CandidateMimeTypes = [
|
|
5745
|
+
"video/mp4;codecs=avc1.42E01E,mp4a.40.2",
|
|
5746
|
+
"video/mp4;codecs=avc1.42001f,mp4a.40.2",
|
|
5747
|
+
"video/mp4"
|
|
5748
|
+
];
|
|
5749
|
+
let candidateMimeTypes = mp4CandidateMimeTypes;
|
|
5750
|
+
if (this.shouldPreferWebmSafeCapture(config)) {
|
|
5751
|
+
candidateMimeTypes = [
|
|
5752
|
+
...webmCandidateMimeTypes,
|
|
5753
|
+
...mp4CandidateMimeTypes
|
|
5754
|
+
];
|
|
5755
|
+
} else if (config.format !== "mp4") {
|
|
5756
|
+
candidateMimeTypes = webmCandidateMimeTypes;
|
|
5757
|
+
}
|
|
5681
5758
|
return candidateMimeTypes.find(canUseMimeType);
|
|
5682
5759
|
}
|
|
5760
|
+
shouldPreferWebmSafeCapture(config) {
|
|
5761
|
+
if (config.format !== "mp4") {
|
|
5762
|
+
return false;
|
|
5763
|
+
}
|
|
5764
|
+
return this.activeRouteDecision.reasonCodes.includes(ANDROID_POST_RECORDING_TRANSCODE_REASON);
|
|
5765
|
+
}
|
|
5766
|
+
scheduleMediaRecorderErrorFinalizeTimeout(recorder) {
|
|
5767
|
+
if (this.mediaRecorderErrorTimeoutId !== null) {
|
|
5768
|
+
return;
|
|
5769
|
+
}
|
|
5770
|
+
this.mediaRecorderErrorTimeoutId = window.setTimeout(() => {
|
|
5771
|
+
if (this.mediaRecorderError !== null) {
|
|
5772
|
+
this.mediaRecorderStopReject?.(this.mediaRecorderError);
|
|
5773
|
+
return;
|
|
5774
|
+
}
|
|
5775
|
+
this.mediaRecorderStopReject?.(this.createEmptyMediaRecorderError(recorder));
|
|
5776
|
+
}, MEDIA_RECORDER_ERROR_FINALIZE_TIMEOUT_MS);
|
|
5777
|
+
}
|
|
5778
|
+
clearMediaRecorderErrorTimeout() {
|
|
5779
|
+
if (this.mediaRecorderErrorTimeoutId === null) {
|
|
5780
|
+
return;
|
|
5781
|
+
}
|
|
5782
|
+
window.clearTimeout(this.mediaRecorderErrorTimeoutId);
|
|
5783
|
+
this.mediaRecorderErrorTimeoutId = null;
|
|
5784
|
+
}
|
|
5785
|
+
createMediaRecorderError(event, recorder) {
|
|
5786
|
+
const mediaRecorderEvent = event;
|
|
5787
|
+
const browserError = mediaRecorderEvent.error;
|
|
5788
|
+
const error = new Error("Browser recording failed before upload could start");
|
|
5789
|
+
error.name = "MediaRecorderRecordingError";
|
|
5790
|
+
error.code = MEDIA_RECORDER_ERROR_CODE;
|
|
5791
|
+
error.mediaRecorderMimeType = recorder.mimeType;
|
|
5792
|
+
error.mediaRecorderChunkCount = this.mediaRecorderChunks.length;
|
|
5793
|
+
error.mediaRecorderChunkSize = this.getMediaRecorderChunkSize();
|
|
5794
|
+
if (browserError?.name) {
|
|
5795
|
+
error.mediaRecorderErrorName = browserError.name;
|
|
5796
|
+
}
|
|
5797
|
+
if (browserError?.message) {
|
|
5798
|
+
error.mediaRecorderErrorMessage = browserError.message;
|
|
5799
|
+
}
|
|
5800
|
+
return error;
|
|
5801
|
+
}
|
|
5802
|
+
createEmptyMediaRecorderError(recorder) {
|
|
5803
|
+
const error = new Error("Browser recording produced no data before upload could start");
|
|
5804
|
+
error.name = "MediaRecorderEmptyDataError";
|
|
5805
|
+
error.code = MEDIA_RECORDER_EMPTY_ERROR_CODE;
|
|
5806
|
+
error.mediaRecorderMimeType = recorder.mimeType;
|
|
5807
|
+
error.mediaRecorderChunkCount = this.mediaRecorderChunks.length;
|
|
5808
|
+
error.mediaRecorderChunkSize = this.getMediaRecorderChunkSize();
|
|
5809
|
+
if (this.mediaRecorderError?.mediaRecorderErrorName) {
|
|
5810
|
+
error.mediaRecorderErrorName = this.mediaRecorderError.mediaRecorderErrorName;
|
|
5811
|
+
}
|
|
5812
|
+
if (this.mediaRecorderError?.mediaRecorderErrorMessage) {
|
|
5813
|
+
error.mediaRecorderErrorMessage = this.mediaRecorderError.mediaRecorderErrorMessage;
|
|
5814
|
+
}
|
|
5815
|
+
return error;
|
|
5816
|
+
}
|
|
5817
|
+
updateMediaRecorderErrorDiagnostics(recorder) {
|
|
5818
|
+
if (this.mediaRecorderError === null) {
|
|
5819
|
+
return;
|
|
5820
|
+
}
|
|
5821
|
+
this.mediaRecorderError.mediaRecorderMimeType = recorder.mimeType;
|
|
5822
|
+
this.mediaRecorderError.mediaRecorderChunkCount = this.mediaRecorderChunks.length;
|
|
5823
|
+
this.mediaRecorderError.mediaRecorderChunkSize = this.getMediaRecorderChunkSize();
|
|
5824
|
+
}
|
|
5825
|
+
buildMediaRecorderDiagnostics(recorder) {
|
|
5826
|
+
const diagnostics = {
|
|
5827
|
+
mediaRecorderMimeType: recorder.mimeType,
|
|
5828
|
+
mediaRecorderChunkCount: this.mediaRecorderChunks.length,
|
|
5829
|
+
mediaRecorderChunkSize: this.getMediaRecorderChunkSize(),
|
|
5830
|
+
mediaRecorderHadError: this.mediaRecorderError !== null
|
|
5831
|
+
};
|
|
5832
|
+
if (this.mediaRecorderError?.mediaRecorderErrorName) {
|
|
5833
|
+
diagnostics.mediaRecorderErrorName = this.mediaRecorderError.mediaRecorderErrorName;
|
|
5834
|
+
}
|
|
5835
|
+
if (this.mediaRecorderError?.mediaRecorderErrorMessage) {
|
|
5836
|
+
diagnostics.mediaRecorderErrorMessage = this.mediaRecorderError.mediaRecorderErrorMessage;
|
|
5837
|
+
}
|
|
5838
|
+
return diagnostics;
|
|
5839
|
+
}
|
|
5840
|
+
getMediaRecorderChunkSize() {
|
|
5841
|
+
return this.mediaRecorderChunks.reduce((total, chunk) => total + chunk.size, 0);
|
|
5842
|
+
}
|
|
5683
5843
|
resetPauseState() {
|
|
5684
5844
|
this.totalPausedTime = 0;
|
|
5685
5845
|
this.pauseStartTime = null;
|
|
@@ -5759,6 +5919,8 @@ class StreamRecordingState {
|
|
|
5759
5919
|
this.mediaRecorderStopPromise = null;
|
|
5760
5920
|
this.mediaRecorderStopResolve = null;
|
|
5761
5921
|
this.mediaRecorderStopReject = null;
|
|
5922
|
+
this.mediaRecorderError = null;
|
|
5923
|
+
this.clearMediaRecorderErrorTimeout();
|
|
5762
5924
|
this.activeRouteDecision = DEFAULT_ROUTE_DECISION;
|
|
5763
5925
|
if (this.streamProcessor) {
|
|
5764
5926
|
this.streamProcessor.destroy();
|
|
@@ -5906,6 +6068,8 @@ var RECORDING_CODEC_UNSUPPORTED_CODES = new Set([
|
|
|
5906
6068
|
]);
|
|
5907
6069
|
var RECORDING_SERVER_TRANSCODE_FALLBACK_CODES = new Set([
|
|
5908
6070
|
"recording.post-recording-transcode-fallback",
|
|
6071
|
+
"recording.mediarecorder-empty",
|
|
6072
|
+
"recording.mediarecorder-error",
|
|
5909
6073
|
"transcode.post-recording-fallback",
|
|
5910
6074
|
"route.post-recording-transcode-fallback"
|
|
5911
6075
|
]);
|
|
@@ -6141,7 +6305,7 @@ function resolveDeviceError(input) {
|
|
|
6141
6305
|
// package.json
|
|
6142
6306
|
var package_default = {
|
|
6143
6307
|
name: "@vidtreo/recorder",
|
|
6144
|
-
version: "1.
|
|
6308
|
+
version: "1.7.1",
|
|
6145
6309
|
type: "module",
|
|
6146
6310
|
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.",
|
|
6147
6311
|
main: "./dist/index.js",
|
|
@@ -6226,6 +6390,13 @@ var MAX_RETRY_ATTEMPTS = 3;
|
|
|
6226
6390
|
var MAX_PENDING_EVENTS = 100;
|
|
6227
6391
|
var BRACKET_ERROR_CODE_PATTERN = /\[([a-z]+(?:[.-][a-z0-9]+)+)\]/i;
|
|
6228
6392
|
var INLINE_ERROR_CODE_PATTERN = /\b([a-z]+(?:[.-][a-z0-9]+)+)\b/i;
|
|
6393
|
+
var MEDIA_RECORDER_ERROR_PROPERTY_KEYS = [
|
|
6394
|
+
"mediaRecorderErrorName",
|
|
6395
|
+
"mediaRecorderErrorMessage",
|
|
6396
|
+
"mediaRecorderMimeType",
|
|
6397
|
+
"mediaRecorderChunkCount",
|
|
6398
|
+
"mediaRecorderChunkSize"
|
|
6399
|
+
];
|
|
6229
6400
|
function resolveInstallationId(dependencies) {
|
|
6230
6401
|
const storageProvider = dependencies.storageProvider;
|
|
6231
6402
|
const stored = storageProvider?.getItem(TELEMETRY_STORAGE_KEY);
|
|
@@ -6567,6 +6738,23 @@ class TelemetryClient {
|
|
|
6567
6738
|
recordingPipelineErrorCode: recordingPipelineCode
|
|
6568
6739
|
};
|
|
6569
6740
|
}
|
|
6741
|
+
return {
|
|
6742
|
+
...properties,
|
|
6743
|
+
...this.buildMediaRecorderErrorProperties(error)
|
|
6744
|
+
};
|
|
6745
|
+
}
|
|
6746
|
+
buildMediaRecorderErrorProperties(error) {
|
|
6747
|
+
if (!(error && typeof error === "object")) {
|
|
6748
|
+
return {};
|
|
6749
|
+
}
|
|
6750
|
+
const candidate = error;
|
|
6751
|
+
let properties = {};
|
|
6752
|
+
for (const key of MEDIA_RECORDER_ERROR_PROPERTY_KEYS) {
|
|
6753
|
+
const value = candidate[key];
|
|
6754
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
6755
|
+
properties = { ...properties, [key]: value };
|
|
6756
|
+
}
|
|
6757
|
+
}
|
|
6570
6758
|
return properties;
|
|
6571
6759
|
}
|
|
6572
6760
|
buildFingerprint() {
|
|
@@ -20980,6 +21168,8 @@ var CODEC_CACHE_AUDIO_OVERRIDE = "audioOverride";
|
|
|
20980
21168
|
var CODEC_CACHE_VIDEO_OVERRIDE = "videoOverride";
|
|
20981
21169
|
var CODEC_CACHE_AUDIO_BITRATE = "audioBitrate";
|
|
20982
21170
|
var CODEC_CACHE_VIDEO_BITRATE = "videoBitrate";
|
|
21171
|
+
var CODEC_CACHE_AUDIO_CHANNELS = "audioChannels";
|
|
21172
|
+
var CODEC_CACHE_AUDIO_SAMPLE_RATE = "audioSampleRate";
|
|
20983
21173
|
var CODEC_CACHE_WIDTH = "width";
|
|
20984
21174
|
var CODEC_CACHE_HEIGHT = "height";
|
|
20985
21175
|
var CODEC_CACHE_POLICY_PREFERRED_AUDIO = "policyPreferredAudio";
|
|
@@ -21241,7 +21431,6 @@ class WorkerProcessor {
|
|
|
21241
21431
|
isLinuxPlatform: this.isLinuxPlatformFn()
|
|
21242
21432
|
});
|
|
21243
21433
|
const audioBitrate = this.resolveAudioBitrate(config, format);
|
|
21244
|
-
const audioCodec = await this.resolveAudioCodecWithCache(config, format, policy, audioBitrate);
|
|
21245
21434
|
const codec = await this.resolveVideoCodecWithCache(config, format, policy);
|
|
21246
21435
|
const isScreenCapture = isScreenCaptureStream(stream);
|
|
21247
21436
|
logger.debug("[WorkerProcessor] Starting processing", {
|
|
@@ -21250,7 +21439,6 @@ class WorkerProcessor {
|
|
|
21250
21439
|
codec,
|
|
21251
21440
|
bitrate: config.bitrate
|
|
21252
21441
|
});
|
|
21253
|
-
const workerConfig = this.buildWorkerTranscodeConfig(config, audioCodec, audioBitrate, codec, format);
|
|
21254
21442
|
if (typeof config.fps === "number" && config.fps > 0) {
|
|
21255
21443
|
this.lastConfigFps = config.fps;
|
|
21256
21444
|
}
|
|
@@ -21286,6 +21474,8 @@ class WorkerProcessor {
|
|
|
21286
21474
|
};
|
|
21287
21475
|
}
|
|
21288
21476
|
const { audioConfig, audioStream, shouldStartAudioWorklet } = await this.prepareAudioPipeline(audioTrack, workerProbeResult);
|
|
21477
|
+
const audioCodec = await this.resolveAudioCodecWithCache(config, format, policy, audioBitrate, audioConfig);
|
|
21478
|
+
const workerConfig = this.buildWorkerTranscodeConfig(config, audioCodec, audioBitrate, codec, format);
|
|
21289
21479
|
const overlayConfigToSend = this.buildOverlayConfigToSend();
|
|
21290
21480
|
const message = createStartMessage({
|
|
21291
21481
|
videoTrack: videoInput.videoTrack,
|
|
@@ -21545,12 +21735,14 @@ class WorkerProcessor {
|
|
|
21545
21735
|
}
|
|
21546
21736
|
return getPresetAudioBitrateForFormat(format);
|
|
21547
21737
|
}
|
|
21548
|
-
async resolveAudioCodec(config, format, policy, audioBitrate) {
|
|
21738
|
+
async resolveAudioCodec(config, format, policy, audioBitrate, audioConfig) {
|
|
21549
21739
|
return await resolveAudioCodecFromPolicy({
|
|
21550
21740
|
format,
|
|
21551
21741
|
overrideCodec: config.audioCodec,
|
|
21552
21742
|
policy,
|
|
21553
|
-
bitrate: audioBitrate
|
|
21743
|
+
bitrate: audioBitrate,
|
|
21744
|
+
numberOfChannels: audioConfig?.numberOfChannels,
|
|
21745
|
+
sampleRate: audioConfig?.sampleRate
|
|
21554
21746
|
});
|
|
21555
21747
|
}
|
|
21556
21748
|
async resolveVideoCodec(config, format, policy) {
|
|
@@ -21563,13 +21755,13 @@ class WorkerProcessor {
|
|
|
21563
21755
|
bitrate: config.bitrate
|
|
21564
21756
|
});
|
|
21565
21757
|
}
|
|
21566
|
-
async resolveAudioCodecWithCache(config, format, policy, audioBitrate) {
|
|
21567
|
-
const audioCodecCacheKey = this.buildAudioCodecCacheKey(config, format, policy, audioBitrate);
|
|
21758
|
+
async resolveAudioCodecWithCache(config, format, policy, audioBitrate, audioConfig) {
|
|
21759
|
+
const audioCodecCacheKey = this.buildAudioCodecCacheKey(config, format, policy, audioBitrate, audioConfig);
|
|
21568
21760
|
const cachedAudioCodec = resolvedAudioCodecCache.get(audioCodecCacheKey);
|
|
21569
21761
|
if (cachedAudioCodec) {
|
|
21570
21762
|
return cachedAudioCodec;
|
|
21571
21763
|
}
|
|
21572
|
-
const resolvedAudioCodec = await this.resolveAudioCodec(config, format, policy, audioBitrate);
|
|
21764
|
+
const resolvedAudioCodec = await this.resolveAudioCodec(config, format, policy, audioBitrate, audioConfig);
|
|
21573
21765
|
setCodecCacheValue(resolvedAudioCodecCache, audioCodecCacheKey, resolvedAudioCodec);
|
|
21574
21766
|
return resolvedAudioCodec;
|
|
21575
21767
|
}
|
|
@@ -21583,15 +21775,19 @@ class WorkerProcessor {
|
|
|
21583
21775
|
setCodecCacheValue(resolvedVideoCodecCache, videoCodecCacheKey, resolvedVideoCodec);
|
|
21584
21776
|
return resolvedVideoCodec;
|
|
21585
21777
|
}
|
|
21586
|
-
buildAudioCodecCacheKey(config, format, policy, audioBitrate) {
|
|
21778
|
+
buildAudioCodecCacheKey(config, format, policy, audioBitrate, audioConfig) {
|
|
21587
21779
|
const audioOverride = formatCacheValue(config.audioCodec);
|
|
21588
21780
|
const preferredAudioCodec = formatCacheValue(policy.preferredAudioCodec);
|
|
21589
21781
|
const audioFallbackOrder = formatCacheArray(policy.audioCodecFallbackOrder);
|
|
21590
21782
|
const audioBitrateValue = formatCacheValue(audioBitrate);
|
|
21783
|
+
const audioChannelsValue = formatCacheValue(audioConfig?.numberOfChannels);
|
|
21784
|
+
const audioSampleRateValue = formatCacheValue(audioConfig?.sampleRate);
|
|
21591
21785
|
return [
|
|
21592
21786
|
buildCacheEntry(CODEC_CACHE_FORMAT, formatCacheValue(format)),
|
|
21593
21787
|
buildCacheEntry(CODEC_CACHE_AUDIO_OVERRIDE, audioOverride),
|
|
21594
21788
|
buildCacheEntry(CODEC_CACHE_AUDIO_BITRATE, audioBitrateValue),
|
|
21789
|
+
buildCacheEntry(CODEC_CACHE_AUDIO_CHANNELS, audioChannelsValue),
|
|
21790
|
+
buildCacheEntry(CODEC_CACHE_AUDIO_SAMPLE_RATE, audioSampleRateValue),
|
|
21595
21791
|
buildCacheEntry(CODEC_CACHE_POLICY_PREFERRED_AUDIO, preferredAudioCodec),
|
|
21596
21792
|
buildCacheEntry(CODEC_CACHE_POLICY_AUDIO_FALLBACK, audioFallbackOrder)
|
|
21597
21793
|
].join(CODEC_CACHE_KEY_SEPARATOR);
|
|
@@ -22020,7 +22216,7 @@ class WorkerProcessor {
|
|
|
22020
22216
|
isLinuxPlatform: this.isLinuxPlatformFn()
|
|
22021
22217
|
});
|
|
22022
22218
|
const cacheAudioBitrate = this.resolveAudioBitrate(config, format);
|
|
22023
|
-
this.resolveAudioCodecWithCache(config, format, cachePolicy, cacheAudioBitrate).catch(this.handleWarmupCacheError);
|
|
22219
|
+
this.resolveAudioCodecWithCache(config, format, cachePolicy, cacheAudioBitrate, null).catch(this.handleWarmupCacheError);
|
|
22024
22220
|
this.resolveVideoCodecWithCache(config, format, cachePolicy).catch(this.handleWarmupCacheError);
|
|
22025
22221
|
const workerConfig = this.buildWorkerTranscodeConfig(config, policy.preferredAudioCodec, audioBitrate, codec, format);
|
|
22026
22222
|
const message = {
|
|
@@ -22637,6 +22833,9 @@ class RecordingManager {
|
|
|
22637
22833
|
if (stopResult.encoderAcceleration !== undefined) {
|
|
22638
22834
|
telemetryProperties.encoderAcceleration = stopResult.encoderAcceleration;
|
|
22639
22835
|
}
|
|
22836
|
+
if (stopResult.mediaRecorderDiagnostics !== undefined) {
|
|
22837
|
+
Object.assign(telemetryProperties, stopResult.mediaRecorderDiagnostics);
|
|
22838
|
+
}
|
|
22640
22839
|
return {
|
|
22641
22840
|
blob: finalBlob,
|
|
22642
22841
|
telemetryProperties
|
|
@@ -22646,6 +22845,10 @@ class RecordingManager {
|
|
|
22646
22845
|
this.handleError(normalizedError);
|
|
22647
22846
|
this.recordingState = RECORDING_STATE_IDLE;
|
|
22648
22847
|
this.callbacks.onStateChange(this.recordingState);
|
|
22848
|
+
if (this.streamProcessor) {
|
|
22849
|
+
this.streamProcessor.destroy();
|
|
22850
|
+
this.streamProcessor = null;
|
|
22851
|
+
}
|
|
22649
22852
|
throw normalizedError;
|
|
22650
22853
|
} finally {
|
|
22651
22854
|
this.activeRecordingConfig = null;
|
|
@@ -23105,6 +23308,7 @@ class RecorderController {
|
|
|
23105
23308
|
}
|
|
23106
23309
|
async stopRecording() {
|
|
23107
23310
|
const sourceType = this.getCurrentSourceType();
|
|
23311
|
+
let shouldStopStream = true;
|
|
23108
23312
|
try {
|
|
23109
23313
|
const stopResult = await this.telemetryManager.executeActionWithResult({
|
|
23110
23314
|
requestedEvent: "recording.stop.requested",
|
|
@@ -23115,7 +23319,6 @@ class RecorderController {
|
|
|
23115
23319
|
await this.sourceSwitchManager.handleRecordingStop().catch(() => {
|
|
23116
23320
|
throw new Error("Source switch cleanup failed");
|
|
23117
23321
|
});
|
|
23118
|
-
this.streamManager.stopStream();
|
|
23119
23322
|
return recordingStopResult;
|
|
23120
23323
|
},
|
|
23121
23324
|
properties: {
|
|
@@ -23127,11 +23330,22 @@ class RecorderController {
|
|
|
23127
23330
|
});
|
|
23128
23331
|
return stopResult.blob;
|
|
23129
23332
|
} catch (error) {
|
|
23333
|
+
shouldStopStream = this.shouldStopStreamAfterStopFailure(error);
|
|
23130
23334
|
if (isAudioMissingError(error)) {
|
|
23131
23335
|
this.sendAudioMissingTelemetry(error);
|
|
23132
23336
|
}
|
|
23133
23337
|
throw error;
|
|
23338
|
+
} finally {
|
|
23339
|
+
if (shouldStopStream) {
|
|
23340
|
+
this.streamManager.stopStream();
|
|
23341
|
+
}
|
|
23342
|
+
}
|
|
23343
|
+
}
|
|
23344
|
+
shouldStopStreamAfterStopFailure(error) {
|
|
23345
|
+
if (!(error && typeof error === "object" && ("code" in error))) {
|
|
23346
|
+
return true;
|
|
23134
23347
|
}
|
|
23348
|
+
return error.code !== ERROR_RECORDING_STOP_NOT_READY;
|
|
23135
23349
|
}
|
|
23136
23350
|
sendAudioMissingTelemetry(error) {
|
|
23137
23351
|
const properties = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vidtreo/recorder",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.1",
|
|
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",
|