@vidtreo/recorder 1.6.2 → 1.7.0
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 +29 -2
- package/dist/index.js +342 -39
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export type StopRecordingResult = {
|
|
|
7
7
|
tabVisibilityIntervals: VisibilityInterval[];
|
|
8
8
|
recordingStats?: StreamRecordingStats;
|
|
9
9
|
encoderAcceleration?: VideoHardwareAcceleration;
|
|
10
|
+
mediaRecorderDiagnostics?: Record<string, unknown>;
|
|
10
11
|
};
|
|
11
12
|
type CheckRecorderSupportDependency = (options: SupportCheckOptions) => Promise<SupportReport>;
|
|
12
13
|
type GetCurrentTimestampDependency = () => number;
|
|
@@ -32,6 +33,8 @@ export declare class StreamRecordingState {
|
|
|
32
33
|
private mediaRecorderStopPromise;
|
|
33
34
|
private mediaRecorderStopResolve;
|
|
34
35
|
private mediaRecorderStopReject;
|
|
36
|
+
private mediaRecorderError;
|
|
37
|
+
private mediaRecorderErrorTimeoutId;
|
|
35
38
|
private readonly streamManager;
|
|
36
39
|
private readonly dependencies;
|
|
37
40
|
constructor(streamManager: StreamManager, dependencies?: Partial<StreamRecordingStateDependencies>);
|
|
@@ -60,6 +63,14 @@ export declare class StreamRecordingState {
|
|
|
60
63
|
private stopSafeCaptureRecording;
|
|
61
64
|
private resolveSafeCaptureMimeType;
|
|
62
65
|
private resolvePreferredSafeCaptureMimeType;
|
|
66
|
+
private shouldPreferWebmSafeCapture;
|
|
67
|
+
private scheduleMediaRecorderErrorFinalizeTimeout;
|
|
68
|
+
private clearMediaRecorderErrorTimeout;
|
|
69
|
+
private createMediaRecorderError;
|
|
70
|
+
private createEmptyMediaRecorderError;
|
|
71
|
+
private updateMediaRecorderErrorDiagnostics;
|
|
72
|
+
private buildMediaRecorderDiagnostics;
|
|
73
|
+
private getMediaRecorderChunkSize;
|
|
63
74
|
private resetPauseState;
|
|
64
75
|
private resolveTabVisibilityOverlayText;
|
|
65
76
|
private setupVisibilityUpdates;
|
|
@@ -683,9 +694,17 @@ export type VideoEncoderProbeResult = {
|
|
|
683
694
|
id: string;
|
|
684
695
|
config: VideoEncoderConfig;
|
|
685
696
|
supported: boolean;
|
|
697
|
+
encodingCapability?: VideoEncodingCapabilityProbeResult;
|
|
686
698
|
errorCode?: "codec-probe-error";
|
|
687
699
|
errorMessage?: string;
|
|
688
700
|
};
|
|
701
|
+
export type VideoEncodingCapabilityProbeResult = {
|
|
702
|
+
supported: boolean;
|
|
703
|
+
smooth: boolean;
|
|
704
|
+
powerEfficient: boolean;
|
|
705
|
+
errorCode?: "encoding-capability-probe-error";
|
|
706
|
+
errorMessage?: string;
|
|
707
|
+
};
|
|
689
708
|
export type DeviceCapabilityProfile = {
|
|
690
709
|
browser: {
|
|
691
710
|
name: string;
|
|
@@ -712,12 +731,14 @@ export type DeviceCapabilityProfile = {
|
|
|
712
731
|
codecProbeResults: VideoEncoderProbeResult[];
|
|
713
732
|
};
|
|
714
733
|
export type VideoEncoderSupportProbe = (config: VideoEncoderConfig) => Promise<VideoEncoderSupport>;
|
|
734
|
+
export type VideoEncodingCapabilityProbe = (config: VideoEncoderConfig) => Promise<MediaCapabilitiesEncodingInfo | null>;
|
|
715
735
|
type DeviceCapabilityProfileDependencies = {
|
|
716
736
|
navigatorProvider: CapabilityNavigatorProvider | null;
|
|
717
737
|
secureContextProvider: () => boolean;
|
|
718
738
|
mediaRecorderAvailabilityProvider: () => boolean;
|
|
719
739
|
videoEncoderAvailabilityProvider: () => boolean;
|
|
720
740
|
codecSupportProbe: VideoEncoderSupportProbe;
|
|
741
|
+
encodingCapabilityProbe: VideoEncodingCapabilityProbe;
|
|
721
742
|
};
|
|
722
743
|
export type BuildDeviceCapabilityProfileOptions = {
|
|
723
744
|
codecCandidates?: VideoEncoderConfigCandidate[];
|
|
@@ -1326,6 +1347,7 @@ export declare class TelemetryClient {
|
|
|
1326
1347
|
private isSafeCapabilityValue;
|
|
1327
1348
|
private isRouteSourceType;
|
|
1328
1349
|
private buildErrorProperties;
|
|
1350
|
+
private buildMediaRecorderErrorProperties;
|
|
1329
1351
|
private buildFingerprint;
|
|
1330
1352
|
private buildContext;
|
|
1331
1353
|
private buildError;
|
|
@@ -2443,7 +2465,7 @@ export declare function deserializeBitrate(bitrate: number | string | undefined)
|
|
|
2443
2465
|
import type { DeviceCapabilityProfile } from "../capabilities/device-capability-profile";
|
|
2444
2466
|
export type RecordingCapabilityProfile = DeviceCapabilityProfile;
|
|
2445
2467
|
export type RecordingRoute = "local-webcodecs" | "safe-capture-post-recording-transcode" | "unsupported-with-guidance";
|
|
2446
|
-
export type RecordingRouteReasonCode = "webcodecs-supported" | "webcodecs-unavailable" | "
|
|
2468
|
+
export type RecordingRouteReasonCode = "webcodecs-supported" | "webcodecs-unavailable" | "target-codec-supported" | "target-codec-unsupported" | "encoding-capability-unsupported" | "encoding-not-smooth" | "encoding-not-power-efficient" | "insufficient-device-memory" | "insufficient-hardware-concurrency" | "insecure-context" | "mediarecorder-supported" | "mediarecorder-unavailable" | "android-post-recording-transcode" | "offline";
|
|
2447
2469
|
export type RecordingRouteDecision = {
|
|
2448
2470
|
route: RecordingRoute;
|
|
2449
2471
|
reasonCodes: RecordingRouteReasonCode[];
|
|
@@ -2466,7 +2488,8 @@ export declare function decideRecordingRoute(options: DecideRecordingRouteOption
|
|
|
2466
2488
|
export declare const ERROR_RECORDING_STOP_NOT_READY = "recording.stop-not-ready";
|
|
2467
2489
|
export declare const ERROR_RECORDING_FINALIZE_NOT_READY = "recording.finalize-not-ready";
|
|
2468
2490
|
export declare const ERROR_RECORDING_FINALIZE_TIMEOUT = "recording.finalize-timeout";
|
|
2469
|
-
export
|
|
2491
|
+
export declare const ERROR_RECORDING_UPLOAD_IN_PROGRESS = "recording.upload-in-progress";
|
|
2492
|
+
export type RecordingLifecycleErrorCode = typeof ERROR_RECORDING_STOP_NOT_READY | typeof ERROR_RECORDING_FINALIZE_NOT_READY | typeof ERROR_RECORDING_FINALIZE_TIMEOUT | typeof ERROR_RECORDING_UPLOAD_IN_PROGRESS;
|
|
2470
2493
|
export type RecordingLifecycleError = Error & {
|
|
2471
2494
|
code: RecordingLifecycleErrorCode;
|
|
2472
2495
|
isRecoverable: true;
|
|
@@ -2759,6 +2782,7 @@ export declare class RecorderController {
|
|
|
2759
2782
|
private isDestroyed;
|
|
2760
2783
|
private enableTabVisibilityOverlay;
|
|
2761
2784
|
private tabVisibilityOverlayText;
|
|
2785
|
+
private isUploadInProgress;
|
|
2762
2786
|
private recordingWarmupTimeoutId;
|
|
2763
2787
|
private audioTelemetryUnsub;
|
|
2764
2788
|
constructor(callbacks?: RecorderCallbacks);
|
|
@@ -2769,6 +2793,7 @@ export declare class RecorderController {
|
|
|
2769
2793
|
switchAudioDevice(deviceId: string | null): Promise<MediaStream>;
|
|
2770
2794
|
startRecording(): Promise<void>;
|
|
2771
2795
|
stopRecording(): Promise<Blob>;
|
|
2796
|
+
private shouldStopStreamAfterStopFailure;
|
|
2772
2797
|
private sendAudioMissingTelemetry;
|
|
2773
2798
|
getTabVisibilityOverlayConfig(): {
|
|
2774
2799
|
enabled: boolean;
|
|
@@ -2788,6 +2813,7 @@ export declare class RecorderController {
|
|
|
2788
2813
|
stopAudioLevelTracking(): void;
|
|
2789
2814
|
getAudioLevel(): number;
|
|
2790
2815
|
uploadVideo(blob: Blob, apiKey: string, backendUrl: string, metadata: Record<string, unknown>): Promise<void>;
|
|
2816
|
+
private uploadVideoAfterStatusClear;
|
|
2791
2817
|
private uploadDirectlyWithoutQueue;
|
|
2792
2818
|
getStream(): MediaStream | null;
|
|
2793
2819
|
isConfigReady(): boolean;
|
|
@@ -2802,6 +2828,7 @@ export declare class RecorderController {
|
|
|
2802
2828
|
getDeviceManager(): DeviceManager;
|
|
2803
2829
|
getConfig(): Promise<TranscodeConfig>;
|
|
2804
2830
|
getUploadService(): VideoUploadService | null;
|
|
2831
|
+
isUploading(): boolean;
|
|
2805
2832
|
isRecording(): boolean;
|
|
2806
2833
|
isActive(): boolean;
|
|
2807
2834
|
isAudioReady(): boolean;
|
package/dist/index.js
CHANGED
|
@@ -1321,6 +1321,48 @@ function isMediaRecorderAvailable() {
|
|
|
1321
1321
|
function isVideoEncoderAvailable() {
|
|
1322
1322
|
return typeof globalThis.VideoEncoder !== "undefined";
|
|
1323
1323
|
}
|
|
1324
|
+
function getGlobalMediaCapabilities() {
|
|
1325
|
+
if (typeof navigator === "undefined") {
|
|
1326
|
+
return null;
|
|
1327
|
+
}
|
|
1328
|
+
return navigator.mediaCapabilities;
|
|
1329
|
+
}
|
|
1330
|
+
function resolveEncodingContentType(codec) {
|
|
1331
|
+
const normalizedCodec = codec.toLowerCase();
|
|
1332
|
+
if (normalizedCodec.startsWith("avc1") || normalizedCodec.startsWith("av01") || normalizedCodec.startsWith("hvc1") || normalizedCodec.startsWith("hev1")) {
|
|
1333
|
+
return `video/mp4;codecs="${codec}"`;
|
|
1334
|
+
}
|
|
1335
|
+
if (normalizedCodec === "vp8" || normalizedCodec.startsWith("vp09") || normalizedCodec.startsWith("vp9")) {
|
|
1336
|
+
return `video/webm;codecs="${codec}"`;
|
|
1337
|
+
}
|
|
1338
|
+
return null;
|
|
1339
|
+
}
|
|
1340
|
+
function probeVideoEncodingCapability(config) {
|
|
1341
|
+
const mediaCapabilities = getGlobalMediaCapabilities();
|
|
1342
|
+
if (!mediaCapabilities) {
|
|
1343
|
+
return Promise.resolve(null);
|
|
1344
|
+
}
|
|
1345
|
+
const contentType = resolveEncodingContentType(config.codec);
|
|
1346
|
+
if (contentType === null) {
|
|
1347
|
+
return Promise.resolve(null);
|
|
1348
|
+
}
|
|
1349
|
+
if (!(typeof config.bitrate === "number")) {
|
|
1350
|
+
return Promise.resolve(null);
|
|
1351
|
+
}
|
|
1352
|
+
if (!(typeof config.framerate === "number")) {
|
|
1353
|
+
return Promise.resolve(null);
|
|
1354
|
+
}
|
|
1355
|
+
return mediaCapabilities.encodingInfo({
|
|
1356
|
+
type: "record",
|
|
1357
|
+
video: {
|
|
1358
|
+
contentType,
|
|
1359
|
+
width: config.width,
|
|
1360
|
+
height: config.height,
|
|
1361
|
+
bitrate: config.bitrate,
|
|
1362
|
+
framerate: config.framerate
|
|
1363
|
+
}
|
|
1364
|
+
});
|
|
1365
|
+
}
|
|
1324
1366
|
function probeVideoEncoderConfig(config) {
|
|
1325
1367
|
if (typeof globalThis.VideoEncoder === "undefined") {
|
|
1326
1368
|
return Promise.resolve({
|
|
@@ -1336,7 +1378,8 @@ function resolveDependencies(dependencies) {
|
|
|
1336
1378
|
secureContextProvider: dependencies?.secureContextProvider ?? isSecureContextAvailable,
|
|
1337
1379
|
mediaRecorderAvailabilityProvider: dependencies?.mediaRecorderAvailabilityProvider ?? isMediaRecorderAvailable,
|
|
1338
1380
|
videoEncoderAvailabilityProvider: dependencies?.videoEncoderAvailabilityProvider ?? isVideoEncoderAvailable,
|
|
1339
|
-
codecSupportProbe: dependencies?.codecSupportProbe ?? probeVideoEncoderConfig
|
|
1381
|
+
codecSupportProbe: dependencies?.codecSupportProbe ?? probeVideoEncoderConfig,
|
|
1382
|
+
encodingCapabilityProbe: dependencies?.encodingCapabilityProbe ?? probeVideoEncodingCapability
|
|
1340
1383
|
};
|
|
1341
1384
|
}
|
|
1342
1385
|
function parseUserAgent(userAgent) {
|
|
@@ -1362,14 +1405,22 @@ function normalizeProbeErrorMessage(error) {
|
|
|
1362
1405
|
}
|
|
1363
1406
|
return String(error);
|
|
1364
1407
|
}
|
|
1365
|
-
async function probeCodecCandidate(candidate, codecSupportProbe) {
|
|
1408
|
+
async function probeCodecCandidate(candidate, codecSupportProbe, encodingCapabilityProbe) {
|
|
1366
1409
|
try {
|
|
1367
1410
|
const result = await codecSupportProbe(candidate.config);
|
|
1368
|
-
|
|
1411
|
+
const encodingCapability = await probeEncodingCapability(candidate.config, encodingCapabilityProbe);
|
|
1412
|
+
const probeResult = {
|
|
1369
1413
|
id: candidate.id,
|
|
1370
1414
|
config: candidate.config,
|
|
1371
1415
|
supported: result.supported === true
|
|
1372
1416
|
};
|
|
1417
|
+
if (encodingCapability === undefined) {
|
|
1418
|
+
return probeResult;
|
|
1419
|
+
}
|
|
1420
|
+
return {
|
|
1421
|
+
...probeResult,
|
|
1422
|
+
encodingCapability
|
|
1423
|
+
};
|
|
1373
1424
|
} catch (error) {
|
|
1374
1425
|
return {
|
|
1375
1426
|
id: candidate.id,
|
|
@@ -1380,10 +1431,28 @@ async function probeCodecCandidate(candidate, codecSupportProbe) {
|
|
|
1380
1431
|
};
|
|
1381
1432
|
}
|
|
1382
1433
|
}
|
|
1434
|
+
function probeEncodingCapability(config, encodingCapabilityProbe) {
|
|
1435
|
+
return Promise.resolve().then(() => encodingCapabilityProbe(config)).then((result) => {
|
|
1436
|
+
if (result === null) {
|
|
1437
|
+
return;
|
|
1438
|
+
}
|
|
1439
|
+
return {
|
|
1440
|
+
supported: result.supported === true,
|
|
1441
|
+
smooth: result.smooth === true,
|
|
1442
|
+
powerEfficient: result.powerEfficient === true
|
|
1443
|
+
};
|
|
1444
|
+
}).catch((error) => ({
|
|
1445
|
+
supported: false,
|
|
1446
|
+
smooth: false,
|
|
1447
|
+
powerEfficient: false,
|
|
1448
|
+
errorCode: "encoding-capability-probe-error",
|
|
1449
|
+
errorMessage: normalizeProbeErrorMessage(error)
|
|
1450
|
+
}));
|
|
1451
|
+
}
|
|
1383
1452
|
async function buildDeviceCapabilityProfile(options = {}) {
|
|
1384
1453
|
const dependencies = resolveDependencies(options.dependencies);
|
|
1385
1454
|
const { browser, os } = parseUserAgent(dependencies.navigatorProvider?.userAgent);
|
|
1386
|
-
const codecProbeResults = await Promise.all((options.codecCandidates ?? []).map((candidate) => probeCodecCandidate(candidate, dependencies.codecSupportProbe)));
|
|
1455
|
+
const codecProbeResults = await Promise.all((options.codecCandidates ?? []).map((candidate) => probeCodecCandidate(candidate, dependencies.codecSupportProbe, dependencies.encodingCapabilityProbe)));
|
|
1387
1456
|
return {
|
|
1388
1457
|
browser,
|
|
1389
1458
|
os,
|
|
@@ -1406,7 +1475,8 @@ async function buildDeviceCapabilityProfile(options = {}) {
|
|
|
1406
1475
|
|
|
1407
1476
|
// src/core/routing/recording-route-decision.ts
|
|
1408
1477
|
var DEFAULT_MIN_DEVICE_MEMORY_GB = 4;
|
|
1409
|
-
var DEFAULT_MIN_HARDWARE_CONCURRENCY =
|
|
1478
|
+
var DEFAULT_MIN_HARDWARE_CONCURRENCY = 5;
|
|
1479
|
+
var PLATFORM_KEYWORD_ANDROID2 = "android";
|
|
1410
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.";
|
|
1411
1481
|
function hasSufficientDeviceMemory(profile, minDeviceMemory) {
|
|
1412
1482
|
const deviceMemory = profile.resources.deviceMemory;
|
|
@@ -1416,18 +1486,14 @@ function hasSufficientHardwareConcurrency(profile, minHardwareConcurrency) {
|
|
|
1416
1486
|
const hardwareConcurrency = profile.resources.hardwareConcurrency;
|
|
1417
1487
|
return hardwareConcurrency === undefined || hardwareConcurrency >= minHardwareConcurrency;
|
|
1418
1488
|
}
|
|
1419
|
-
function
|
|
1489
|
+
function findSupportedTargetCodec(profile, targetCodecIds) {
|
|
1420
1490
|
const targetCodecIdSet = new Set(targetCodecIds);
|
|
1421
|
-
|
|
1422
|
-
return supportedCodec?.id;
|
|
1491
|
+
return profile.codecProbeResults.find((result) => targetCodecIdSet.has(result.id) && result.supported);
|
|
1423
1492
|
}
|
|
1424
1493
|
function canUsePostRecordingTranscodeRoute(profile, hasSupportedTargetCodec) {
|
|
1425
1494
|
return profile.features.isSecureContext && profile.features.hasMediaRecorder && profile.features.hasVideoEncoder && hasSupportedTargetCodec && profile.network.online !== false;
|
|
1426
1495
|
}
|
|
1427
|
-
function
|
|
1428
|
-
return profile.os.name.toLowerCase() === "android";
|
|
1429
|
-
}
|
|
1430
|
-
function buildLocalRouteBlockers(profile, hasSupportedTargetCodec, hasEnoughDeviceMemory, hasEnoughHardwareConcurrency) {
|
|
1496
|
+
function buildLocalRouteBlockers(profile, hasSupportedTargetCodec, hasEnoughDeviceMemory, hasEnoughHardwareConcurrency, hasSupportedEncodingCapability, hasSmoothEncoding, hasPowerEfficientEncoding) {
|
|
1431
1497
|
const reasonCodes = [];
|
|
1432
1498
|
if (!profile.features.isSecureContext) {
|
|
1433
1499
|
reasonCodes.push("insecure-context");
|
|
@@ -1444,8 +1510,14 @@ function buildLocalRouteBlockers(profile, hasSupportedTargetCodec, hasEnoughDevi
|
|
|
1444
1510
|
if (!hasEnoughHardwareConcurrency) {
|
|
1445
1511
|
reasonCodes.push("insufficient-hardware-concurrency");
|
|
1446
1512
|
}
|
|
1447
|
-
if (
|
|
1448
|
-
reasonCodes.push("
|
|
1513
|
+
if (!hasSupportedEncodingCapability) {
|
|
1514
|
+
reasonCodes.push("encoding-capability-unsupported");
|
|
1515
|
+
}
|
|
1516
|
+
if (!hasSmoothEncoding) {
|
|
1517
|
+
reasonCodes.push("encoding-not-smooth");
|
|
1518
|
+
}
|
|
1519
|
+
if (!hasPowerEfficientEncoding) {
|
|
1520
|
+
reasonCodes.push("encoding-not-power-efficient");
|
|
1449
1521
|
}
|
|
1450
1522
|
return reasonCodes;
|
|
1451
1523
|
}
|
|
@@ -1459,15 +1531,49 @@ function buildFallbackRouteBlockers(profile) {
|
|
|
1459
1531
|
}
|
|
1460
1532
|
return reasonCodes;
|
|
1461
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
|
+
}
|
|
1545
|
+
function hasSupportedEncodingCapability(selectedCodec) {
|
|
1546
|
+
const encodingCapability = selectedCodec?.encodingCapability;
|
|
1547
|
+
return encodingCapability === undefined || encodingCapability.errorCode !== undefined || encodingCapability.supported === true;
|
|
1548
|
+
}
|
|
1549
|
+
function hasSmoothEncodingCapability(selectedCodec) {
|
|
1550
|
+
const encodingCapability = selectedCodec?.encodingCapability;
|
|
1551
|
+
return encodingCapability === undefined || encodingCapability.errorCode !== undefined || encodingCapability.supported !== true || encodingCapability.smooth === true;
|
|
1552
|
+
}
|
|
1553
|
+
function hasPowerEfficientEncodingCapability(selectedCodec) {
|
|
1554
|
+
const encodingCapability = selectedCodec?.encodingCapability;
|
|
1555
|
+
return encodingCapability === undefined || encodingCapability.errorCode !== undefined || encodingCapability.supported !== true || encodingCapability.powerEfficient === true;
|
|
1556
|
+
}
|
|
1462
1557
|
function decideRecordingRoute(options) {
|
|
1463
1558
|
const minDeviceMemory = options.resourceThresholds?.minDeviceMemory ?? DEFAULT_MIN_DEVICE_MEMORY_GB;
|
|
1464
1559
|
const minHardwareConcurrency = options.resourceThresholds?.minHardwareConcurrency ?? DEFAULT_MIN_HARDWARE_CONCURRENCY;
|
|
1465
|
-
const
|
|
1560
|
+
const selectedCodec = findSupportedTargetCodec(options.profile, options.targetCodecIds);
|
|
1561
|
+
const selectedCodecId = selectedCodec?.id;
|
|
1466
1562
|
const hasSupportedTargetCodec = selectedCodecId !== undefined;
|
|
1467
1563
|
const hasEnoughDeviceMemory = hasSufficientDeviceMemory(options.profile, minDeviceMemory);
|
|
1468
1564
|
const hasEnoughHardwareConcurrency = hasSufficientHardwareConcurrency(options.profile, minHardwareConcurrency);
|
|
1469
|
-
const
|
|
1470
|
-
|
|
1565
|
+
const hasSupportedEncoding = hasSupportedEncodingCapability(selectedCodec);
|
|
1566
|
+
const hasSmoothEncoding = hasSmoothEncodingCapability(selectedCodec);
|
|
1567
|
+
const hasPowerEfficientEncoding = hasPowerEfficientEncodingCapability(selectedCodec);
|
|
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
|
+
}
|
|
1576
|
+
if (options.profile.features.isSecureContext && options.profile.features.hasVideoEncoder && hasSupportedTargetCodec && options.profile.network.online !== false && hasEnoughDeviceMemory && hasEnoughHardwareConcurrency && hasSupportedEncoding && hasSmoothEncoding && hasPowerEfficientEncoding) {
|
|
1471
1577
|
return {
|
|
1472
1578
|
route: "local-webcodecs",
|
|
1473
1579
|
reasonCodes: ["webcodecs-supported", "target-codec-supported"],
|
|
@@ -1477,7 +1583,7 @@ function decideRecordingRoute(options) {
|
|
|
1477
1583
|
if (canUsePostRecordingTranscodeRoute(options.profile, hasSupportedTargetCodec)) {
|
|
1478
1584
|
return {
|
|
1479
1585
|
route: "safe-capture-post-recording-transcode",
|
|
1480
|
-
reasonCodes:
|
|
1586
|
+
reasonCodes: buildPostRecordingTranscodeReasonCodes(options.profile, localRouteBlockers),
|
|
1481
1587
|
selectedCodecId
|
|
1482
1588
|
};
|
|
1483
1589
|
}
|
|
@@ -4952,6 +5058,7 @@ class StreamManager {
|
|
|
4952
5058
|
var ERROR_RECORDING_STOP_NOT_READY = "recording.stop-not-ready";
|
|
4953
5059
|
var ERROR_RECORDING_FINALIZE_NOT_READY = "recording.finalize-not-ready";
|
|
4954
5060
|
var ERROR_RECORDING_FINALIZE_TIMEOUT = "recording.finalize-timeout";
|
|
5061
|
+
var ERROR_RECORDING_UPLOAD_IN_PROGRESS = "recording.upload-in-progress";
|
|
4955
5062
|
function createRecordingLifecycleError(code, message, details) {
|
|
4956
5063
|
const error = new Error(`${message} [${code}]`);
|
|
4957
5064
|
error.code = code;
|
|
@@ -4978,7 +5085,7 @@ function normalizeRecordingLifecycleError(error) {
|
|
|
4978
5085
|
}
|
|
4979
5086
|
function isRecordingLifecycleError(error) {
|
|
4980
5087
|
const errorWithCode = error;
|
|
4981
|
-
return errorWithCode.code === ERROR_RECORDING_STOP_NOT_READY || errorWithCode.code === ERROR_RECORDING_FINALIZE_NOT_READY || errorWithCode.code === ERROR_RECORDING_FINALIZE_TIMEOUT;
|
|
5088
|
+
return errorWithCode.code === ERROR_RECORDING_STOP_NOT_READY || errorWithCode.code === ERROR_RECORDING_FINALIZE_NOT_READY || errorWithCode.code === ERROR_RECORDING_FINALIZE_TIMEOUT || errorWithCode.code === ERROR_RECORDING_UPLOAD_IN_PROGRESS;
|
|
4982
5089
|
}
|
|
4983
5090
|
|
|
4984
5091
|
// src/core/utils/formatters.ts
|
|
@@ -5163,6 +5270,11 @@ var ANDROID_SAFE_CAPTURE_CONSTRAINTS = {
|
|
|
5163
5270
|
height: { ideal: 720, max: 720 },
|
|
5164
5271
|
frameRate: { ideal: 24, max: 24 }
|
|
5165
5272
|
};
|
|
5273
|
+
var SAFE_CAPTURE_TIMESLICE_MS = 1000;
|
|
5274
|
+
var MEDIA_RECORDER_ERROR_FINALIZE_TIMEOUT_MS = 5000;
|
|
5275
|
+
var ANDROID_POST_RECORDING_TRANSCODE_REASON = "android-post-recording-transcode";
|
|
5276
|
+
var MEDIA_RECORDER_ERROR_CODE = "recording.mediarecorder-error";
|
|
5277
|
+
var MEDIA_RECORDER_EMPTY_ERROR_CODE = "recording.mediarecorder-empty";
|
|
5166
5278
|
function createUnsupportedRouteError(routeDecision) {
|
|
5167
5279
|
const guidance = routeDecision.guidance?.message ?? "This browser or device cannot safely record video. Try a supported browser or upload an existing video file.";
|
|
5168
5280
|
const error = new Error(guidance);
|
|
@@ -5194,6 +5306,8 @@ class StreamRecordingState {
|
|
|
5194
5306
|
mediaRecorderStopPromise = null;
|
|
5195
5307
|
mediaRecorderStopResolve = null;
|
|
5196
5308
|
mediaRecorderStopReject = null;
|
|
5309
|
+
mediaRecorderError = null;
|
|
5310
|
+
mediaRecorderErrorTimeoutId = null;
|
|
5197
5311
|
streamManager;
|
|
5198
5312
|
dependencies;
|
|
5199
5313
|
constructor(streamManager, dependencies) {
|
|
@@ -5366,9 +5480,11 @@ class StreamRecordingState {
|
|
|
5366
5480
|
}
|
|
5367
5481
|
let result;
|
|
5368
5482
|
if (this.activeRouteDecision.route === "safe-capture-post-recording-transcode") {
|
|
5483
|
+
const safeCaptureResult = await this.stopSafeCaptureRecording();
|
|
5369
5484
|
logger.debug("[StreamRecordingState] Finalizing MediaRecorder capture");
|
|
5370
5485
|
result = {
|
|
5371
|
-
blob:
|
|
5486
|
+
blob: safeCaptureResult.blob,
|
|
5487
|
+
mediaRecorderDiagnostics: safeCaptureResult.diagnostics
|
|
5372
5488
|
};
|
|
5373
5489
|
} else {
|
|
5374
5490
|
logger.debug("[StreamRecordingState] Finalizing stream processor");
|
|
@@ -5387,7 +5503,8 @@ class StreamRecordingState {
|
|
|
5387
5503
|
blob: result.blob,
|
|
5388
5504
|
tabVisibilityIntervals,
|
|
5389
5505
|
recordingStats: result.recordingStats,
|
|
5390
|
-
encoderAcceleration: result.encoderAcceleration
|
|
5506
|
+
encoderAcceleration: result.encoderAcceleration,
|
|
5507
|
+
mediaRecorderDiagnostics: result.mediaRecorderDiagnostics
|
|
5391
5508
|
};
|
|
5392
5509
|
}
|
|
5393
5510
|
pauseRecording() {
|
|
@@ -5398,6 +5515,9 @@ class StreamRecordingState {
|
|
|
5398
5515
|
if (this.tabVisibilityTracker) {
|
|
5399
5516
|
this.tabVisibilityTracker.pause();
|
|
5400
5517
|
}
|
|
5518
|
+
if (this.mediaRecorder?.state === "recording") {
|
|
5519
|
+
this.mediaRecorder.pause?.();
|
|
5520
|
+
}
|
|
5401
5521
|
if (this.streamProcessor && this.isRecording()) {
|
|
5402
5522
|
this.streamProcessor.pause();
|
|
5403
5523
|
}
|
|
@@ -5411,6 +5531,9 @@ class StreamRecordingState {
|
|
|
5411
5531
|
if (this.tabVisibilityTracker) {
|
|
5412
5532
|
this.tabVisibilityTracker.resume();
|
|
5413
5533
|
}
|
|
5534
|
+
if (this.mediaRecorder?.state === "paused") {
|
|
5535
|
+
this.mediaRecorder.resume?.();
|
|
5536
|
+
}
|
|
5414
5537
|
this.startRecordingTimer();
|
|
5415
5538
|
if (this.streamProcessor && this.isRecording()) {
|
|
5416
5539
|
this.streamProcessor.resume();
|
|
@@ -5510,6 +5633,7 @@ class StreamRecordingState {
|
|
|
5510
5633
|
}
|
|
5511
5634
|
await this.applySafeCaptureTrackConstraints(mediaStream);
|
|
5512
5635
|
this.mediaRecorderChunks = [];
|
|
5636
|
+
this.mediaRecorderError = null;
|
|
5513
5637
|
const preferredMimeType = this.resolvePreferredSafeCaptureMimeType(config);
|
|
5514
5638
|
const recorderOptions = preferredMimeType === undefined ? undefined : { mimeType: preferredMimeType };
|
|
5515
5639
|
const recorder = new MediaRecorder(mediaStream, recorderOptions);
|
|
@@ -5518,20 +5642,34 @@ class StreamRecordingState {
|
|
|
5518
5642
|
this.mediaRecorderStopResolve = resolve;
|
|
5519
5643
|
this.mediaRecorderStopReject = reject;
|
|
5520
5644
|
});
|
|
5645
|
+
this.mediaRecorderStopPromise.catch(() => {
|
|
5646
|
+
return;
|
|
5647
|
+
});
|
|
5521
5648
|
recorder.ondataavailable = (event) => {
|
|
5522
5649
|
if (event.data.size > 0) {
|
|
5523
5650
|
this.mediaRecorderChunks.push(event.data);
|
|
5524
5651
|
}
|
|
5525
5652
|
};
|
|
5526
5653
|
recorder.onstop = () => {
|
|
5654
|
+
this.clearMediaRecorderErrorTimeout();
|
|
5655
|
+
if (this.mediaRecorderError !== null) {
|
|
5656
|
+
this.updateMediaRecorderErrorDiagnostics(recorder);
|
|
5657
|
+
this.mediaRecorderStopReject?.(this.mediaRecorderError);
|
|
5658
|
+
return;
|
|
5659
|
+
}
|
|
5527
5660
|
const mimeType = recorder.mimeType || this.resolveSafeCaptureMimeType();
|
|
5528
5661
|
const blob = new Blob(this.mediaRecorderChunks, { type: mimeType });
|
|
5529
|
-
this.
|
|
5662
|
+
const diagnostics = this.buildMediaRecorderDiagnostics(recorder);
|
|
5663
|
+
if (blob.size > 0) {
|
|
5664
|
+
this.mediaRecorderStopResolve?.({ blob, diagnostics });
|
|
5665
|
+
return;
|
|
5666
|
+
}
|
|
5667
|
+
this.mediaRecorderStopReject?.(this.createEmptyMediaRecorderError(recorder));
|
|
5530
5668
|
};
|
|
5531
|
-
recorder.onerror = () => {
|
|
5532
|
-
this.
|
|
5669
|
+
recorder.onerror = (event) => {
|
|
5670
|
+
this.mediaRecorderError = this.createMediaRecorderError(event, recorder);
|
|
5533
5671
|
};
|
|
5534
|
-
recorder.start();
|
|
5672
|
+
recorder.start(SAFE_CAPTURE_TIMESLICE_MS);
|
|
5535
5673
|
}
|
|
5536
5674
|
async applySafeCaptureTrackConstraints(mediaStream) {
|
|
5537
5675
|
const constraints = this.resolveSafeCaptureVideoConstraints();
|
|
@@ -5549,10 +5687,10 @@ class StreamRecordingState {
|
|
|
5549
5687
|
}
|
|
5550
5688
|
resolveSafeCaptureVideoConstraints() {
|
|
5551
5689
|
const reasonCodes = this.activeRouteDecision.reasonCodes;
|
|
5552
|
-
if (reasonCodes.includes("insufficient-device-memory") || reasonCodes.includes("insufficient-hardware-concurrency")) {
|
|
5690
|
+
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")) {
|
|
5553
5691
|
return LOW_RESOURCE_SAFE_CAPTURE_CONSTRAINTS;
|
|
5554
5692
|
}
|
|
5555
|
-
if (reasonCodes.includes(
|
|
5693
|
+
if (reasonCodes.includes(ANDROID_POST_RECORDING_TRANSCODE_REASON)) {
|
|
5556
5694
|
return ANDROID_SAFE_CAPTURE_CONSTRAINTS;
|
|
5557
5695
|
}
|
|
5558
5696
|
return null;
|
|
@@ -5566,6 +5704,7 @@ class StreamRecordingState {
|
|
|
5566
5704
|
if (recorder.state !== "inactive") {
|
|
5567
5705
|
recorder.stop();
|
|
5568
5706
|
}
|
|
5707
|
+
this.scheduleMediaRecorderErrorFinalizeTimeout(recorder);
|
|
5569
5708
|
try {
|
|
5570
5709
|
return await stopPromise;
|
|
5571
5710
|
} finally {
|
|
@@ -5574,6 +5713,8 @@ class StreamRecordingState {
|
|
|
5574
5713
|
this.mediaRecorderStopPromise = null;
|
|
5575
5714
|
this.mediaRecorderStopResolve = null;
|
|
5576
5715
|
this.mediaRecorderStopReject = null;
|
|
5716
|
+
this.mediaRecorderError = null;
|
|
5717
|
+
this.clearMediaRecorderErrorTimeout();
|
|
5577
5718
|
this.activeRouteDecision = DEFAULT_ROUTE_DECISION;
|
|
5578
5719
|
}
|
|
5579
5720
|
}
|
|
@@ -5589,17 +5730,110 @@ class StreamRecordingState {
|
|
|
5589
5730
|
}
|
|
5590
5731
|
return MediaRecorder.isTypeSupported(mimeType);
|
|
5591
5732
|
};
|
|
5592
|
-
const
|
|
5593
|
-
"video/mp4;codecs=avc1.42E01E,mp4a.40.2",
|
|
5594
|
-
"video/mp4;codecs=avc1.42001f,mp4a.40.2",
|
|
5595
|
-
"video/mp4"
|
|
5596
|
-
] : [
|
|
5733
|
+
const webmCandidateMimeTypes = [
|
|
5597
5734
|
"video/webm;codecs=vp8,opus",
|
|
5598
5735
|
"video/webm;codecs=vp9,opus",
|
|
5599
5736
|
"video/webm"
|
|
5600
5737
|
];
|
|
5738
|
+
const mp4CandidateMimeTypes = [
|
|
5739
|
+
"video/mp4;codecs=avc1.42E01E,mp4a.40.2",
|
|
5740
|
+
"video/mp4;codecs=avc1.42001f,mp4a.40.2",
|
|
5741
|
+
"video/mp4"
|
|
5742
|
+
];
|
|
5743
|
+
let candidateMimeTypes = mp4CandidateMimeTypes;
|
|
5744
|
+
if (this.shouldPreferWebmSafeCapture(config)) {
|
|
5745
|
+
candidateMimeTypes = [
|
|
5746
|
+
...webmCandidateMimeTypes,
|
|
5747
|
+
...mp4CandidateMimeTypes
|
|
5748
|
+
];
|
|
5749
|
+
} else if (config.format !== "mp4") {
|
|
5750
|
+
candidateMimeTypes = webmCandidateMimeTypes;
|
|
5751
|
+
}
|
|
5601
5752
|
return candidateMimeTypes.find(canUseMimeType);
|
|
5602
5753
|
}
|
|
5754
|
+
shouldPreferWebmSafeCapture(config) {
|
|
5755
|
+
if (config.format !== "mp4") {
|
|
5756
|
+
return false;
|
|
5757
|
+
}
|
|
5758
|
+
return this.activeRouteDecision.reasonCodes.includes(ANDROID_POST_RECORDING_TRANSCODE_REASON);
|
|
5759
|
+
}
|
|
5760
|
+
scheduleMediaRecorderErrorFinalizeTimeout(recorder) {
|
|
5761
|
+
if (this.mediaRecorderErrorTimeoutId !== null) {
|
|
5762
|
+
return;
|
|
5763
|
+
}
|
|
5764
|
+
this.mediaRecorderErrorTimeoutId = window.setTimeout(() => {
|
|
5765
|
+
if (this.mediaRecorderError !== null) {
|
|
5766
|
+
this.mediaRecorderStopReject?.(this.mediaRecorderError);
|
|
5767
|
+
return;
|
|
5768
|
+
}
|
|
5769
|
+
this.mediaRecorderStopReject?.(this.createEmptyMediaRecorderError(recorder));
|
|
5770
|
+
}, MEDIA_RECORDER_ERROR_FINALIZE_TIMEOUT_MS);
|
|
5771
|
+
}
|
|
5772
|
+
clearMediaRecorderErrorTimeout() {
|
|
5773
|
+
if (this.mediaRecorderErrorTimeoutId === null) {
|
|
5774
|
+
return;
|
|
5775
|
+
}
|
|
5776
|
+
window.clearTimeout(this.mediaRecorderErrorTimeoutId);
|
|
5777
|
+
this.mediaRecorderErrorTimeoutId = null;
|
|
5778
|
+
}
|
|
5779
|
+
createMediaRecorderError(event, recorder) {
|
|
5780
|
+
const mediaRecorderEvent = event;
|
|
5781
|
+
const browserError = mediaRecorderEvent.error;
|
|
5782
|
+
const error = new Error("Browser recording failed before upload could start");
|
|
5783
|
+
error.name = "MediaRecorderRecordingError";
|
|
5784
|
+
error.code = MEDIA_RECORDER_ERROR_CODE;
|
|
5785
|
+
error.mediaRecorderMimeType = recorder.mimeType;
|
|
5786
|
+
error.mediaRecorderChunkCount = this.mediaRecorderChunks.length;
|
|
5787
|
+
error.mediaRecorderChunkSize = this.getMediaRecorderChunkSize();
|
|
5788
|
+
if (browserError?.name) {
|
|
5789
|
+
error.mediaRecorderErrorName = browserError.name;
|
|
5790
|
+
}
|
|
5791
|
+
if (browserError?.message) {
|
|
5792
|
+
error.mediaRecorderErrorMessage = browserError.message;
|
|
5793
|
+
}
|
|
5794
|
+
return error;
|
|
5795
|
+
}
|
|
5796
|
+
createEmptyMediaRecorderError(recorder) {
|
|
5797
|
+
const error = new Error("Browser recording produced no data before upload could start");
|
|
5798
|
+
error.name = "MediaRecorderEmptyDataError";
|
|
5799
|
+
error.code = MEDIA_RECORDER_EMPTY_ERROR_CODE;
|
|
5800
|
+
error.mediaRecorderMimeType = recorder.mimeType;
|
|
5801
|
+
error.mediaRecorderChunkCount = this.mediaRecorderChunks.length;
|
|
5802
|
+
error.mediaRecorderChunkSize = this.getMediaRecorderChunkSize();
|
|
5803
|
+
if (this.mediaRecorderError?.mediaRecorderErrorName) {
|
|
5804
|
+
error.mediaRecorderErrorName = this.mediaRecorderError.mediaRecorderErrorName;
|
|
5805
|
+
}
|
|
5806
|
+
if (this.mediaRecorderError?.mediaRecorderErrorMessage) {
|
|
5807
|
+
error.mediaRecorderErrorMessage = this.mediaRecorderError.mediaRecorderErrorMessage;
|
|
5808
|
+
}
|
|
5809
|
+
return error;
|
|
5810
|
+
}
|
|
5811
|
+
updateMediaRecorderErrorDiagnostics(recorder) {
|
|
5812
|
+
if (this.mediaRecorderError === null) {
|
|
5813
|
+
return;
|
|
5814
|
+
}
|
|
5815
|
+
this.mediaRecorderError.mediaRecorderMimeType = recorder.mimeType;
|
|
5816
|
+
this.mediaRecorderError.mediaRecorderChunkCount = this.mediaRecorderChunks.length;
|
|
5817
|
+
this.mediaRecorderError.mediaRecorderChunkSize = this.getMediaRecorderChunkSize();
|
|
5818
|
+
}
|
|
5819
|
+
buildMediaRecorderDiagnostics(recorder) {
|
|
5820
|
+
const diagnostics = {
|
|
5821
|
+
mediaRecorderMimeType: recorder.mimeType,
|
|
5822
|
+
mediaRecorderChunkCount: this.mediaRecorderChunks.length,
|
|
5823
|
+
mediaRecorderChunkSize: this.getMediaRecorderChunkSize(),
|
|
5824
|
+
mediaRecorderHadError: this.mediaRecorderError !== null
|
|
5825
|
+
};
|
|
5826
|
+
if (this.mediaRecorderError?.mediaRecorderErrorName) {
|
|
5827
|
+
diagnostics.mediaRecorderErrorName = this.mediaRecorderError.mediaRecorderErrorName;
|
|
5828
|
+
}
|
|
5829
|
+
if (this.mediaRecorderError?.mediaRecorderErrorMessage) {
|
|
5830
|
+
diagnostics.mediaRecorderErrorMessage = this.mediaRecorderError.mediaRecorderErrorMessage;
|
|
5831
|
+
}
|
|
5832
|
+
return diagnostics;
|
|
5833
|
+
}
|
|
5834
|
+
getMediaRecorderChunkSize() {
|
|
5835
|
+
return this.mediaRecorderChunks.reduce((total, chunk) => total + chunk.size, 0);
|
|
5836
|
+
}
|
|
5603
5837
|
resetPauseState() {
|
|
5604
5838
|
this.totalPausedTime = 0;
|
|
5605
5839
|
this.pauseStartTime = null;
|
|
@@ -5679,6 +5913,8 @@ class StreamRecordingState {
|
|
|
5679
5913
|
this.mediaRecorderStopPromise = null;
|
|
5680
5914
|
this.mediaRecorderStopResolve = null;
|
|
5681
5915
|
this.mediaRecorderStopReject = null;
|
|
5916
|
+
this.mediaRecorderError = null;
|
|
5917
|
+
this.clearMediaRecorderErrorTimeout();
|
|
5682
5918
|
this.activeRouteDecision = DEFAULT_ROUTE_DECISION;
|
|
5683
5919
|
if (this.streamProcessor) {
|
|
5684
5920
|
this.streamProcessor.destroy();
|
|
@@ -5826,6 +6062,8 @@ var RECORDING_CODEC_UNSUPPORTED_CODES = new Set([
|
|
|
5826
6062
|
]);
|
|
5827
6063
|
var RECORDING_SERVER_TRANSCODE_FALLBACK_CODES = new Set([
|
|
5828
6064
|
"recording.post-recording-transcode-fallback",
|
|
6065
|
+
"recording.mediarecorder-empty",
|
|
6066
|
+
"recording.mediarecorder-error",
|
|
5829
6067
|
"transcode.post-recording-fallback",
|
|
5830
6068
|
"route.post-recording-transcode-fallback"
|
|
5831
6069
|
]);
|
|
@@ -6061,7 +6299,7 @@ function resolveDeviceError(input) {
|
|
|
6061
6299
|
// package.json
|
|
6062
6300
|
var package_default = {
|
|
6063
6301
|
name: "@vidtreo/recorder",
|
|
6064
|
-
version: "1.
|
|
6302
|
+
version: "1.7.0",
|
|
6065
6303
|
type: "module",
|
|
6066
6304
|
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.",
|
|
6067
6305
|
main: "./dist/index.js",
|
|
@@ -6146,6 +6384,13 @@ var MAX_RETRY_ATTEMPTS = 3;
|
|
|
6146
6384
|
var MAX_PENDING_EVENTS = 100;
|
|
6147
6385
|
var BRACKET_ERROR_CODE_PATTERN = /\[([a-z]+(?:[.-][a-z0-9]+)+)\]/i;
|
|
6148
6386
|
var INLINE_ERROR_CODE_PATTERN = /\b([a-z]+(?:[.-][a-z0-9]+)+)\b/i;
|
|
6387
|
+
var MEDIA_RECORDER_ERROR_PROPERTY_KEYS = [
|
|
6388
|
+
"mediaRecorderErrorName",
|
|
6389
|
+
"mediaRecorderErrorMessage",
|
|
6390
|
+
"mediaRecorderMimeType",
|
|
6391
|
+
"mediaRecorderChunkCount",
|
|
6392
|
+
"mediaRecorderChunkSize"
|
|
6393
|
+
];
|
|
6149
6394
|
function resolveInstallationId(dependencies) {
|
|
6150
6395
|
const storageProvider = dependencies.storageProvider;
|
|
6151
6396
|
const stored = storageProvider?.getItem(TELEMETRY_STORAGE_KEY);
|
|
@@ -6206,7 +6451,7 @@ var TELEMETRY_EVENT_CATEGORY_MAP = {
|
|
|
6206
6451
|
"audio.acquisition.retry": "lifecycle",
|
|
6207
6452
|
"audio.acquisition.recovered": "lifecycle",
|
|
6208
6453
|
"audio.acquisition.failed": "error",
|
|
6209
|
-
"audio.warning": "
|
|
6454
|
+
"audio.warning": "performance",
|
|
6210
6455
|
"recording.audio-missing": "error",
|
|
6211
6456
|
"storage.init.failed": "error",
|
|
6212
6457
|
"storage.write.probe.failed": "error"
|
|
@@ -6487,6 +6732,23 @@ class TelemetryClient {
|
|
|
6487
6732
|
recordingPipelineErrorCode: recordingPipelineCode
|
|
6488
6733
|
};
|
|
6489
6734
|
}
|
|
6735
|
+
return {
|
|
6736
|
+
...properties,
|
|
6737
|
+
...this.buildMediaRecorderErrorProperties(error)
|
|
6738
|
+
};
|
|
6739
|
+
}
|
|
6740
|
+
buildMediaRecorderErrorProperties(error) {
|
|
6741
|
+
if (!(error && typeof error === "object")) {
|
|
6742
|
+
return {};
|
|
6743
|
+
}
|
|
6744
|
+
const candidate = error;
|
|
6745
|
+
let properties = {};
|
|
6746
|
+
for (const key of MEDIA_RECORDER_ERROR_PROPERTY_KEYS) {
|
|
6747
|
+
const value = candidate[key];
|
|
6748
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
6749
|
+
properties = { ...properties, [key]: value };
|
|
6750
|
+
}
|
|
6751
|
+
}
|
|
6490
6752
|
return properties;
|
|
6491
6753
|
}
|
|
6492
6754
|
buildFingerprint() {
|
|
@@ -22557,6 +22819,9 @@ class RecordingManager {
|
|
|
22557
22819
|
if (stopResult.encoderAcceleration !== undefined) {
|
|
22558
22820
|
telemetryProperties.encoderAcceleration = stopResult.encoderAcceleration;
|
|
22559
22821
|
}
|
|
22822
|
+
if (stopResult.mediaRecorderDiagnostics !== undefined) {
|
|
22823
|
+
Object.assign(telemetryProperties, stopResult.mediaRecorderDiagnostics);
|
|
22824
|
+
}
|
|
22560
22825
|
return {
|
|
22561
22826
|
blob: finalBlob,
|
|
22562
22827
|
telemetryProperties
|
|
@@ -22566,6 +22831,10 @@ class RecordingManager {
|
|
|
22566
22831
|
this.handleError(normalizedError);
|
|
22567
22832
|
this.recordingState = RECORDING_STATE_IDLE;
|
|
22568
22833
|
this.callbacks.onStateChange(this.recordingState);
|
|
22834
|
+
if (this.streamProcessor) {
|
|
22835
|
+
this.streamProcessor.destroy();
|
|
22836
|
+
this.streamProcessor = null;
|
|
22837
|
+
}
|
|
22569
22838
|
throw normalizedError;
|
|
22570
22839
|
} finally {
|
|
22571
22840
|
this.activeRecordingConfig = null;
|
|
@@ -22867,6 +23136,7 @@ class RecorderController {
|
|
|
22867
23136
|
isDestroyed = false;
|
|
22868
23137
|
enableTabVisibilityOverlay = false;
|
|
22869
23138
|
tabVisibilityOverlayText;
|
|
23139
|
+
isUploadInProgress = false;
|
|
22870
23140
|
recordingWarmupTimeoutId = null;
|
|
22871
23141
|
audioTelemetryUnsub = null;
|
|
22872
23142
|
constructor(callbacks = {}) {
|
|
@@ -23010,6 +23280,9 @@ class RecorderController {
|
|
|
23010
23280
|
succeededEvent: "recording.start.succeeded",
|
|
23011
23281
|
failedEvent: "recording.start.failed",
|
|
23012
23282
|
action: async () => {
|
|
23283
|
+
if (this.isUploading()) {
|
|
23284
|
+
throw createRecordingLifecycleError(ERROR_RECORDING_UPLOAD_IN_PROGRESS, "Previous recording is still being uploaded");
|
|
23285
|
+
}
|
|
23013
23286
|
await this.ensureConfigReady();
|
|
23014
23287
|
await this.streamManager.waitForAudio();
|
|
23015
23288
|
await this.recordingManager.startRecording();
|
|
@@ -23021,6 +23294,7 @@ class RecorderController {
|
|
|
23021
23294
|
}
|
|
23022
23295
|
async stopRecording() {
|
|
23023
23296
|
const sourceType = this.getCurrentSourceType();
|
|
23297
|
+
let shouldStopStream = true;
|
|
23024
23298
|
try {
|
|
23025
23299
|
const stopResult = await this.telemetryManager.executeActionWithResult({
|
|
23026
23300
|
requestedEvent: "recording.stop.requested",
|
|
@@ -23031,7 +23305,6 @@ class RecorderController {
|
|
|
23031
23305
|
await this.sourceSwitchManager.handleRecordingStop().catch(() => {
|
|
23032
23306
|
throw new Error("Source switch cleanup failed");
|
|
23033
23307
|
});
|
|
23034
|
-
this.streamManager.stopStream();
|
|
23035
23308
|
return recordingStopResult;
|
|
23036
23309
|
},
|
|
23037
23310
|
properties: {
|
|
@@ -23043,11 +23316,22 @@ class RecorderController {
|
|
|
23043
23316
|
});
|
|
23044
23317
|
return stopResult.blob;
|
|
23045
23318
|
} catch (error) {
|
|
23319
|
+
shouldStopStream = this.shouldStopStreamAfterStopFailure(error);
|
|
23046
23320
|
if (isAudioMissingError(error)) {
|
|
23047
23321
|
this.sendAudioMissingTelemetry(error);
|
|
23048
23322
|
}
|
|
23049
23323
|
throw error;
|
|
23324
|
+
} finally {
|
|
23325
|
+
if (shouldStopStream) {
|
|
23326
|
+
this.streamManager.stopStream();
|
|
23327
|
+
}
|
|
23328
|
+
}
|
|
23329
|
+
}
|
|
23330
|
+
shouldStopStreamAfterStopFailure(error) {
|
|
23331
|
+
if (!(error && typeof error === "object" && ("code" in error))) {
|
|
23332
|
+
return true;
|
|
23050
23333
|
}
|
|
23334
|
+
return error.code !== ERROR_RECORDING_STOP_NOT_READY;
|
|
23051
23335
|
}
|
|
23052
23336
|
sendAudioMissingTelemetry(error) {
|
|
23053
23337
|
const properties = {
|
|
@@ -23063,7 +23347,7 @@ class RecorderController {
|
|
|
23063
23347
|
properties.silenceRatio = error.details.durationMs > 0 ? error.details.mutedDurationMs / error.details.durationMs : 0;
|
|
23064
23348
|
properties.hadTrackEndedWarning = error.details.hadTrackEndedWarning;
|
|
23065
23349
|
}
|
|
23066
|
-
this.telemetryManager.sendEvent("recording.audio-missing", properties);
|
|
23350
|
+
this.telemetryManager.sendEvent("recording.audio-missing", properties, error);
|
|
23067
23351
|
}
|
|
23068
23352
|
getTabVisibilityOverlayConfig() {
|
|
23069
23353
|
return {
|
|
@@ -23124,8 +23408,15 @@ class RecorderController {
|
|
|
23124
23408
|
getAudioLevel() {
|
|
23125
23409
|
return this.audioLevelAnalyzer.getAudioLevel();
|
|
23126
23410
|
}
|
|
23127
|
-
|
|
23411
|
+
uploadVideo(blob, apiKey, backendUrl, metadata) {
|
|
23128
23412
|
this.uploadCallbacks.onClearStatus();
|
|
23413
|
+
this.isUploadInProgress = true;
|
|
23414
|
+
return this.uploadVideoAfterStatusClear(blob, apiKey, backendUrl, metadata).catch((error) => {
|
|
23415
|
+
this.isUploadInProgress = false;
|
|
23416
|
+
throw error;
|
|
23417
|
+
});
|
|
23418
|
+
}
|
|
23419
|
+
async uploadVideoAfterStatusClear(blob, apiKey, backendUrl, metadata) {
|
|
23129
23420
|
const duration = await extractVideoDuration(blob);
|
|
23130
23421
|
const outputFormat = this.configManager.getConfigForRecording().format;
|
|
23131
23422
|
const filename = `recording-${Date.now()}.${getFileExtensionForOutputFormat(outputFormat)}`;
|
|
@@ -23206,6 +23497,8 @@ class RecorderController {
|
|
|
23206
23497
|
sourceType: options.sourceType
|
|
23207
23498
|
}, uploadError);
|
|
23208
23499
|
throw uploadError;
|
|
23500
|
+
} finally {
|
|
23501
|
+
this.isUploadInProgress = false;
|
|
23209
23502
|
}
|
|
23210
23503
|
}
|
|
23211
23504
|
getStream() {
|
|
@@ -23229,6 +23522,7 @@ class RecorderController {
|
|
|
23229
23522
|
}
|
|
23230
23523
|
cleanup() {
|
|
23231
23524
|
this.isDestroyed = true;
|
|
23525
|
+
this.isUploadInProgress = false;
|
|
23232
23526
|
if (this.recordingWarmupTimeoutId !== null) {
|
|
23233
23527
|
clearTimeout(this.recordingWarmupTimeoutId);
|
|
23234
23528
|
this.recordingWarmupTimeoutId = null;
|
|
@@ -23270,6 +23564,9 @@ class RecorderController {
|
|
|
23270
23564
|
getUploadService() {
|
|
23271
23565
|
return this.uploadService;
|
|
23272
23566
|
}
|
|
23567
|
+
isUploading() {
|
|
23568
|
+
return this.isUploadInProgress;
|
|
23569
|
+
}
|
|
23273
23570
|
isRecording() {
|
|
23274
23571
|
return this.streamManager.isRecording();
|
|
23275
23572
|
}
|
|
@@ -23368,7 +23665,10 @@ class RecorderController {
|
|
|
23368
23665
|
isSecureContext: profile.features.isSecureContext,
|
|
23369
23666
|
hasMediaRecorder: profile.features.hasMediaRecorder,
|
|
23370
23667
|
hasVideoEncoder: profile.features.hasVideoEncoder,
|
|
23371
|
-
supportedCodecIds: profile.codecProbeResults.filter((result) => result.supported).map((result) => result.id)
|
|
23668
|
+
supportedCodecIds: profile.codecProbeResults.filter((result) => result.supported).map((result) => result.id),
|
|
23669
|
+
supportedEncodingCodecIds: profile.codecProbeResults.filter((result) => result.encodingCapability?.supported === true).map((result) => result.id),
|
|
23670
|
+
smoothEncodingCodecIds: profile.codecProbeResults.filter((result) => result.encodingCapability?.smooth === true).map((result) => result.id),
|
|
23671
|
+
powerEfficientEncodingCodecIds: profile.codecProbeResults.filter((result) => result.encodingCapability?.powerEfficient === true).map((result) => result.id)
|
|
23372
23672
|
}
|
|
23373
23673
|
};
|
|
23374
23674
|
}
|
|
@@ -23422,9 +23722,10 @@ class RecorderController {
|
|
|
23422
23722
|
const probeResult = this.storageManager.getWriteProbeResult();
|
|
23423
23723
|
if (!probeResult?.ok) {
|
|
23424
23724
|
const reason = probeResult?.reason ?? "Storage write probe did not complete";
|
|
23725
|
+
const storageWriteProbeError = new Error(reason);
|
|
23425
23726
|
this.telemetryManager.sendEvent("storage.write.probe.failed", {
|
|
23426
23727
|
reason
|
|
23427
|
-
});
|
|
23728
|
+
}, storageWriteProbeError);
|
|
23428
23729
|
const onStorageWriteError = resolveStorageWriteErrorCallback(this.callbacks);
|
|
23429
23730
|
onStorageWriteError(reason);
|
|
23430
23731
|
return;
|
|
@@ -23439,6 +23740,7 @@ class RecorderController {
|
|
|
23439
23740
|
this.uploadCallbacks.onProgress(progress);
|
|
23440
23741
|
},
|
|
23441
23742
|
onUploadComplete: (id, result) => {
|
|
23743
|
+
this.isUploadInProgress = false;
|
|
23442
23744
|
this.uploadCallbacks.onSuccess(result);
|
|
23443
23745
|
const metadata = this.uploadMetadataManager.getMetadata(id);
|
|
23444
23746
|
if (metadata) {
|
|
@@ -23452,6 +23754,7 @@ class RecorderController {
|
|
|
23452
23754
|
}
|
|
23453
23755
|
},
|
|
23454
23756
|
onUploadError: (id, error) => {
|
|
23757
|
+
this.isUploadInProgress = false;
|
|
23455
23758
|
this.uploadCallbacks.onError(error);
|
|
23456
23759
|
const metadata = this.uploadMetadataManager.getMetadata(id);
|
|
23457
23760
|
if (metadata) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vidtreo/recorder",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
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",
|