@vidtreo/recorder 1.6.1 → 1.6.3
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 +2451 -2428
- package/dist/index.js +205 -69
- package/package.json +1 -1
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,7 @@ 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;
|
|
1410
1479
|
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
1480
|
function hasSufficientDeviceMemory(profile, minDeviceMemory) {
|
|
1412
1481
|
const deviceMemory = profile.resources.deviceMemory;
|
|
@@ -1416,18 +1485,14 @@ function hasSufficientHardwareConcurrency(profile, minHardwareConcurrency) {
|
|
|
1416
1485
|
const hardwareConcurrency = profile.resources.hardwareConcurrency;
|
|
1417
1486
|
return hardwareConcurrency === undefined || hardwareConcurrency >= minHardwareConcurrency;
|
|
1418
1487
|
}
|
|
1419
|
-
function
|
|
1488
|
+
function findSupportedTargetCodec(profile, targetCodecIds) {
|
|
1420
1489
|
const targetCodecIdSet = new Set(targetCodecIds);
|
|
1421
|
-
|
|
1422
|
-
return supportedCodec?.id;
|
|
1490
|
+
return profile.codecProbeResults.find((result) => targetCodecIdSet.has(result.id) && result.supported);
|
|
1423
1491
|
}
|
|
1424
1492
|
function canUsePostRecordingTranscodeRoute(profile, hasSupportedTargetCodec) {
|
|
1425
1493
|
return profile.features.isSecureContext && profile.features.hasMediaRecorder && profile.features.hasVideoEncoder && hasSupportedTargetCodec && profile.network.online !== false;
|
|
1426
1494
|
}
|
|
1427
|
-
function
|
|
1428
|
-
return profile.os.name.toLowerCase() === "android";
|
|
1429
|
-
}
|
|
1430
|
-
function buildLocalRouteBlockers(profile, hasSupportedTargetCodec, hasEnoughDeviceMemory, hasEnoughHardwareConcurrency) {
|
|
1495
|
+
function buildLocalRouteBlockers(profile, hasSupportedTargetCodec, hasEnoughDeviceMemory, hasEnoughHardwareConcurrency, hasSupportedEncodingCapability, hasSmoothEncoding, hasPowerEfficientEncoding) {
|
|
1431
1496
|
const reasonCodes = [];
|
|
1432
1497
|
if (!profile.features.isSecureContext) {
|
|
1433
1498
|
reasonCodes.push("insecure-context");
|
|
@@ -1444,8 +1509,14 @@ function buildLocalRouteBlockers(profile, hasSupportedTargetCodec, hasEnoughDevi
|
|
|
1444
1509
|
if (!hasEnoughHardwareConcurrency) {
|
|
1445
1510
|
reasonCodes.push("insufficient-hardware-concurrency");
|
|
1446
1511
|
}
|
|
1447
|
-
if (
|
|
1448
|
-
reasonCodes.push("
|
|
1512
|
+
if (!hasSupportedEncodingCapability) {
|
|
1513
|
+
reasonCodes.push("encoding-capability-unsupported");
|
|
1514
|
+
}
|
|
1515
|
+
if (!hasSmoothEncoding) {
|
|
1516
|
+
reasonCodes.push("encoding-not-smooth");
|
|
1517
|
+
}
|
|
1518
|
+
if (!hasPowerEfficientEncoding) {
|
|
1519
|
+
reasonCodes.push("encoding-not-power-efficient");
|
|
1449
1520
|
}
|
|
1450
1521
|
return reasonCodes;
|
|
1451
1522
|
}
|
|
@@ -1459,15 +1530,31 @@ function buildFallbackRouteBlockers(profile) {
|
|
|
1459
1530
|
}
|
|
1460
1531
|
return reasonCodes;
|
|
1461
1532
|
}
|
|
1533
|
+
function hasSupportedEncodingCapability(selectedCodec) {
|
|
1534
|
+
const encodingCapability = selectedCodec?.encodingCapability;
|
|
1535
|
+
return encodingCapability === undefined || encodingCapability.errorCode !== undefined || encodingCapability.supported === true;
|
|
1536
|
+
}
|
|
1537
|
+
function hasSmoothEncodingCapability(selectedCodec) {
|
|
1538
|
+
const encodingCapability = selectedCodec?.encodingCapability;
|
|
1539
|
+
return encodingCapability === undefined || encodingCapability.errorCode !== undefined || encodingCapability.supported !== true || encodingCapability.smooth === true;
|
|
1540
|
+
}
|
|
1541
|
+
function hasPowerEfficientEncodingCapability(selectedCodec) {
|
|
1542
|
+
const encodingCapability = selectedCodec?.encodingCapability;
|
|
1543
|
+
return encodingCapability === undefined || encodingCapability.errorCode !== undefined || encodingCapability.supported !== true || encodingCapability.powerEfficient === true;
|
|
1544
|
+
}
|
|
1462
1545
|
function decideRecordingRoute(options) {
|
|
1463
1546
|
const minDeviceMemory = options.resourceThresholds?.minDeviceMemory ?? DEFAULT_MIN_DEVICE_MEMORY_GB;
|
|
1464
1547
|
const minHardwareConcurrency = options.resourceThresholds?.minHardwareConcurrency ?? DEFAULT_MIN_HARDWARE_CONCURRENCY;
|
|
1465
|
-
const
|
|
1548
|
+
const selectedCodec = findSupportedTargetCodec(options.profile, options.targetCodecIds);
|
|
1549
|
+
const selectedCodecId = selectedCodec?.id;
|
|
1466
1550
|
const hasSupportedTargetCodec = selectedCodecId !== undefined;
|
|
1467
1551
|
const hasEnoughDeviceMemory = hasSufficientDeviceMemory(options.profile, minDeviceMemory);
|
|
1468
1552
|
const hasEnoughHardwareConcurrency = hasSufficientHardwareConcurrency(options.profile, minHardwareConcurrency);
|
|
1469
|
-
const
|
|
1470
|
-
|
|
1553
|
+
const hasSupportedEncoding = hasSupportedEncodingCapability(selectedCodec);
|
|
1554
|
+
const hasSmoothEncoding = hasSmoothEncodingCapability(selectedCodec);
|
|
1555
|
+
const hasPowerEfficientEncoding = hasPowerEfficientEncodingCapability(selectedCodec);
|
|
1556
|
+
const localRouteBlockers = buildLocalRouteBlockers(options.profile, hasSupportedTargetCodec, hasEnoughDeviceMemory, hasEnoughHardwareConcurrency, hasSupportedEncoding, hasSmoothEncoding, hasPowerEfficientEncoding);
|
|
1557
|
+
if (options.profile.features.isSecureContext && options.profile.features.hasVideoEncoder && hasSupportedTargetCodec && options.profile.network.online !== false && hasEnoughDeviceMemory && hasEnoughHardwareConcurrency && hasSupportedEncoding && hasSmoothEncoding && hasPowerEfficientEncoding) {
|
|
1471
1558
|
return {
|
|
1472
1559
|
route: "local-webcodecs",
|
|
1473
1560
|
reasonCodes: ["webcodecs-supported", "target-codec-supported"],
|
|
@@ -1695,6 +1782,36 @@ async function resolveAudioCodecFromPolicy(options) {
|
|
|
1695
1782
|
return options.policy.preferredAudioCodec;
|
|
1696
1783
|
}
|
|
1697
1784
|
|
|
1785
|
+
// src/core/transcode/output-format.ts
|
|
1786
|
+
function getMimeTypeForOutputFormat(format) {
|
|
1787
|
+
switch (format) {
|
|
1788
|
+
case "mp4":
|
|
1789
|
+
return "video/mp4";
|
|
1790
|
+
case "webm":
|
|
1791
|
+
return "video/webm";
|
|
1792
|
+
case "mkv":
|
|
1793
|
+
return "video/x-matroska";
|
|
1794
|
+
case "mov":
|
|
1795
|
+
return "video/quicktime";
|
|
1796
|
+
default:
|
|
1797
|
+
throw new Error(`Unsupported output format: ${format}`);
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
function getFileExtensionForOutputFormat(format) {
|
|
1801
|
+
switch (format) {
|
|
1802
|
+
case "mp4":
|
|
1803
|
+
return "mp4";
|
|
1804
|
+
case "webm":
|
|
1805
|
+
return "webm";
|
|
1806
|
+
case "mkv":
|
|
1807
|
+
return "mkv";
|
|
1808
|
+
case "mov":
|
|
1809
|
+
return "mov";
|
|
1810
|
+
default:
|
|
1811
|
+
throw new Error(`Unsupported output format: ${format}`);
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1698
1815
|
// src/core/transcode/video-transcoder.ts
|
|
1699
1816
|
var ALLOW_ROTATION_METADATA = false;
|
|
1700
1817
|
function createSource(input) {
|
|
@@ -1720,20 +1837,6 @@ function createOutputFormat(format) {
|
|
|
1720
1837
|
throw new Error(`Unsupported output format: ${format}`);
|
|
1721
1838
|
}
|
|
1722
1839
|
}
|
|
1723
|
-
function getMimeTypeForFormat(format) {
|
|
1724
|
-
switch (format) {
|
|
1725
|
-
case "mp4":
|
|
1726
|
-
return "video/mp4";
|
|
1727
|
-
case "webm":
|
|
1728
|
-
return "video/webm";
|
|
1729
|
-
case "mkv":
|
|
1730
|
-
return "video/x-matroska";
|
|
1731
|
-
case "mov":
|
|
1732
|
-
return "video/quicktime";
|
|
1733
|
-
default:
|
|
1734
|
-
throw new Error(`Unsupported output format: ${format}`);
|
|
1735
|
-
}
|
|
1736
|
-
}
|
|
1737
1840
|
function createConversionOptions(config, optimizeForSpeed = false) {
|
|
1738
1841
|
const video = {
|
|
1739
1842
|
fit: "contain",
|
|
@@ -1828,7 +1931,7 @@ async function transcodeVideo(input, config = {}, onProgress) {
|
|
|
1828
1931
|
if (!buffer) {
|
|
1829
1932
|
throw new Error("Transcoding completed but no output buffer was generated");
|
|
1830
1933
|
}
|
|
1831
|
-
const mimeType =
|
|
1934
|
+
const mimeType = getMimeTypeForOutputFormat(finalConfig.format);
|
|
1832
1935
|
return {
|
|
1833
1936
|
buffer,
|
|
1834
1937
|
blob: new Blob([buffer], { type: mimeType })
|
|
@@ -1899,7 +2002,7 @@ async function transcodeVideoForNativeCamera(file, config = {}, onProgress) {
|
|
|
1899
2002
|
if (!buffer) {
|
|
1900
2003
|
throw new Error("Transcoding completed but no output buffer was generated");
|
|
1901
2004
|
}
|
|
1902
|
-
const mimeType =
|
|
2005
|
+
const mimeType = getMimeTypeForOutputFormat(finalConfig.format);
|
|
1903
2006
|
return {
|
|
1904
2007
|
buffer,
|
|
1905
2008
|
blob: new Blob([buffer], { type: mimeType })
|
|
@@ -4936,6 +5039,7 @@ class StreamManager {
|
|
|
4936
5039
|
var ERROR_RECORDING_STOP_NOT_READY = "recording.stop-not-ready";
|
|
4937
5040
|
var ERROR_RECORDING_FINALIZE_NOT_READY = "recording.finalize-not-ready";
|
|
4938
5041
|
var ERROR_RECORDING_FINALIZE_TIMEOUT = "recording.finalize-timeout";
|
|
5042
|
+
var ERROR_RECORDING_UPLOAD_IN_PROGRESS = "recording.upload-in-progress";
|
|
4939
5043
|
function createRecordingLifecycleError(code, message, details) {
|
|
4940
5044
|
const error = new Error(`${message} [${code}]`);
|
|
4941
5045
|
error.code = code;
|
|
@@ -4962,7 +5066,7 @@ function normalizeRecordingLifecycleError(error) {
|
|
|
4962
5066
|
}
|
|
4963
5067
|
function isRecordingLifecycleError(error) {
|
|
4964
5068
|
const errorWithCode = error;
|
|
4965
|
-
return errorWithCode.code === ERROR_RECORDING_STOP_NOT_READY || errorWithCode.code === ERROR_RECORDING_FINALIZE_NOT_READY || errorWithCode.code === ERROR_RECORDING_FINALIZE_TIMEOUT;
|
|
5069
|
+
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;
|
|
4966
5070
|
}
|
|
4967
5071
|
|
|
4968
5072
|
// src/core/utils/formatters.ts
|
|
@@ -5142,11 +5246,6 @@ var LOW_RESOURCE_SAFE_CAPTURE_CONSTRAINTS = {
|
|
|
5142
5246
|
height: { ideal: 480, max: 480 },
|
|
5143
5247
|
frameRate: { ideal: 20, max: 20 }
|
|
5144
5248
|
};
|
|
5145
|
-
var ANDROID_SAFE_CAPTURE_CONSTRAINTS = {
|
|
5146
|
-
width: { ideal: 1280, max: 1280 },
|
|
5147
|
-
height: { ideal: 720, max: 720 },
|
|
5148
|
-
frameRate: { ideal: 24, max: 24 }
|
|
5149
|
-
};
|
|
5150
5249
|
function createUnsupportedRouteError(routeDecision) {
|
|
5151
5250
|
const guidance = routeDecision.guidance?.message ?? "This browser or device cannot safely record video. Try a supported browser or upload an existing video file.";
|
|
5152
5251
|
const error = new Error(guidance);
|
|
@@ -5533,12 +5632,9 @@ class StreamRecordingState {
|
|
|
5533
5632
|
}
|
|
5534
5633
|
resolveSafeCaptureVideoConstraints() {
|
|
5535
5634
|
const reasonCodes = this.activeRouteDecision.reasonCodes;
|
|
5536
|
-
if (reasonCodes.includes("insufficient-device-memory") || reasonCodes.includes("insufficient-hardware-concurrency")) {
|
|
5635
|
+
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")) {
|
|
5537
5636
|
return LOW_RESOURCE_SAFE_CAPTURE_CONSTRAINTS;
|
|
5538
5637
|
}
|
|
5539
|
-
if (reasonCodes.includes("android-post-recording-transcode")) {
|
|
5540
|
-
return ANDROID_SAFE_CAPTURE_CONSTRAINTS;
|
|
5541
|
-
}
|
|
5542
5638
|
return null;
|
|
5543
5639
|
}
|
|
5544
5640
|
async stopSafeCaptureRecording() {
|
|
@@ -6045,7 +6141,7 @@ function resolveDeviceError(input) {
|
|
|
6045
6141
|
// package.json
|
|
6046
6142
|
var package_default = {
|
|
6047
6143
|
name: "@vidtreo/recorder",
|
|
6048
|
-
version: "1.6.
|
|
6144
|
+
version: "1.6.3",
|
|
6049
6145
|
type: "module",
|
|
6050
6146
|
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.",
|
|
6051
6147
|
main: "./dist/index.js",
|
|
@@ -6190,7 +6286,7 @@ var TELEMETRY_EVENT_CATEGORY_MAP = {
|
|
|
6190
6286
|
"audio.acquisition.retry": "lifecycle",
|
|
6191
6287
|
"audio.acquisition.recovered": "lifecycle",
|
|
6192
6288
|
"audio.acquisition.failed": "error",
|
|
6193
|
-
"audio.warning": "
|
|
6289
|
+
"audio.warning": "performance",
|
|
6194
6290
|
"recording.audio-missing": "error",
|
|
6195
6291
|
"storage.init.failed": "error",
|
|
6196
6292
|
"storage.write.probe.failed": "error"
|
|
@@ -6852,6 +6948,7 @@ class UploadQueueManager {
|
|
|
6852
6948
|
apiKey: upload.apiKey,
|
|
6853
6949
|
backendUrl: upload.backendUrl,
|
|
6854
6950
|
filename: upload.filename,
|
|
6951
|
+
mimeType: upload.mimeType,
|
|
6855
6952
|
metadata: upload.metadata,
|
|
6856
6953
|
userMetadata: upload.userMetadata,
|
|
6857
6954
|
onProgress: (progress) => {
|
|
@@ -6899,6 +6996,7 @@ class UploadQueueManager {
|
|
|
6899
6996
|
|
|
6900
6997
|
// src/core/upload/upload-service.ts
|
|
6901
6998
|
var API_UPLOAD_PATH = "/api/v1/videos/upload";
|
|
6999
|
+
var MIME_TYPE_PARAMETER_SEPARATOR = ";";
|
|
6902
7000
|
|
|
6903
7001
|
class VideoUploadService {
|
|
6904
7002
|
uploadVideo(blob, options) {
|
|
@@ -6913,6 +7011,7 @@ class VideoUploadService {
|
|
|
6913
7011
|
uploadVideoFile(blob, options) {
|
|
6914
7012
|
return new Promise((resolve, reject) => {
|
|
6915
7013
|
const xhr = new XMLHttpRequest;
|
|
7014
|
+
const uploadBlob = this.createUploadBlob(blob, options.mimeType);
|
|
6916
7015
|
if (options.onProgress) {
|
|
6917
7016
|
const onProgress = options.onProgress;
|
|
6918
7017
|
xhr.upload.addEventListener("progress", (event) => {
|
|
@@ -6939,10 +7038,10 @@ class VideoUploadService {
|
|
|
6939
7038
|
xhr.open("POST", url);
|
|
6940
7039
|
xhr.setRequestHeader("Authorization", `Bearer ${options.apiKey}`);
|
|
6941
7040
|
const formData = new FormData;
|
|
6942
|
-
formData.append("file",
|
|
7041
|
+
formData.append("file", uploadBlob, options.filename);
|
|
6943
7042
|
const metadata = {
|
|
6944
7043
|
filename: options.filename,
|
|
6945
|
-
mimeType:
|
|
7044
|
+
mimeType: uploadBlob.type
|
|
6946
7045
|
};
|
|
6947
7046
|
if (options.userMetadata) {
|
|
6948
7047
|
metadata.userMetadata = options.userMetadata;
|
|
@@ -6951,6 +7050,26 @@ class VideoUploadService {
|
|
|
6951
7050
|
xhr.send(formData);
|
|
6952
7051
|
});
|
|
6953
7052
|
}
|
|
7053
|
+
createUploadBlob(blob, mimeType) {
|
|
7054
|
+
const normalizedMimeType = this.resolveUploadMimeType(blob, mimeType);
|
|
7055
|
+
if (normalizedMimeType === blob.type) {
|
|
7056
|
+
return blob;
|
|
7057
|
+
}
|
|
7058
|
+
return new Blob([blob], { type: normalizedMimeType });
|
|
7059
|
+
}
|
|
7060
|
+
resolveUploadMimeType(blob, mimeType) {
|
|
7061
|
+
if (mimeType && mimeType.trim().length > 0) {
|
|
7062
|
+
return this.normalizeMimeType(mimeType);
|
|
7063
|
+
}
|
|
7064
|
+
return this.normalizeMimeType(blob.type);
|
|
7065
|
+
}
|
|
7066
|
+
normalizeMimeType(mimeType) {
|
|
7067
|
+
const [baseMimeType] = mimeType.split(MIME_TYPE_PARAMETER_SEPARATOR);
|
|
7068
|
+
if (baseMimeType === undefined) {
|
|
7069
|
+
return mimeType.trim().toLowerCase();
|
|
7070
|
+
}
|
|
7071
|
+
return baseMimeType.trim().toLowerCase();
|
|
7072
|
+
}
|
|
6954
7073
|
parseSuccessResponse(xhr, resolve, reject) {
|
|
6955
7074
|
const parsed = this.safeParseJsonFromXhr(xhr);
|
|
6956
7075
|
if (!parsed) {
|
|
@@ -22276,23 +22395,9 @@ var DEFAULT_ROUTE_DECISION2 = {
|
|
|
22276
22395
|
var DEFAULT_RECORDING_MANAGER_DEPENDENCIES = {
|
|
22277
22396
|
transcodeVideo
|
|
22278
22397
|
};
|
|
22279
|
-
function getExpectedMimeTypePrefix(config) {
|
|
22280
|
-
switch (config.format) {
|
|
22281
|
-
case "mp4":
|
|
22282
|
-
return "video/mp4";
|
|
22283
|
-
case "webm":
|
|
22284
|
-
return "video/webm";
|
|
22285
|
-
case "mkv":
|
|
22286
|
-
return "video/x-matroska";
|
|
22287
|
-
case "mov":
|
|
22288
|
-
return "video/quicktime";
|
|
22289
|
-
default:
|
|
22290
|
-
return null;
|
|
22291
|
-
}
|
|
22292
|
-
}
|
|
22293
22398
|
function isBlobAlreadyInTargetFormat(blob, config) {
|
|
22294
|
-
const expectedMimeTypePrefix =
|
|
22295
|
-
return
|
|
22399
|
+
const expectedMimeTypePrefix = getMimeTypeForOutputFormat(config.format);
|
|
22400
|
+
return blob.type.toLowerCase().startsWith(expectedMimeTypePrefix);
|
|
22296
22401
|
}
|
|
22297
22402
|
|
|
22298
22403
|
class RecordingManager {
|
|
@@ -22842,6 +22947,7 @@ class RecorderController {
|
|
|
22842
22947
|
isDestroyed = false;
|
|
22843
22948
|
enableTabVisibilityOverlay = false;
|
|
22844
22949
|
tabVisibilityOverlayText;
|
|
22950
|
+
isUploadInProgress = false;
|
|
22845
22951
|
recordingWarmupTimeoutId = null;
|
|
22846
22952
|
audioTelemetryUnsub = null;
|
|
22847
22953
|
constructor(callbacks = {}) {
|
|
@@ -22985,6 +23091,9 @@ class RecorderController {
|
|
|
22985
23091
|
succeededEvent: "recording.start.succeeded",
|
|
22986
23092
|
failedEvent: "recording.start.failed",
|
|
22987
23093
|
action: async () => {
|
|
23094
|
+
if (this.isUploading()) {
|
|
23095
|
+
throw createRecordingLifecycleError(ERROR_RECORDING_UPLOAD_IN_PROGRESS, "Previous recording is still being uploaded");
|
|
23096
|
+
}
|
|
22988
23097
|
await this.ensureConfigReady();
|
|
22989
23098
|
await this.streamManager.waitForAudio();
|
|
22990
23099
|
await this.recordingManager.startRecording();
|
|
@@ -23038,7 +23147,7 @@ class RecorderController {
|
|
|
23038
23147
|
properties.silenceRatio = error.details.durationMs > 0 ? error.details.mutedDurationMs / error.details.durationMs : 0;
|
|
23039
23148
|
properties.hadTrackEndedWarning = error.details.hadTrackEndedWarning;
|
|
23040
23149
|
}
|
|
23041
|
-
this.telemetryManager.sendEvent("recording.audio-missing", properties);
|
|
23150
|
+
this.telemetryManager.sendEvent("recording.audio-missing", properties, error);
|
|
23042
23151
|
}
|
|
23043
23152
|
getTabVisibilityOverlayConfig() {
|
|
23044
23153
|
return {
|
|
@@ -23099,10 +23208,19 @@ class RecorderController {
|
|
|
23099
23208
|
getAudioLevel() {
|
|
23100
23209
|
return this.audioLevelAnalyzer.getAudioLevel();
|
|
23101
23210
|
}
|
|
23102
|
-
|
|
23211
|
+
uploadVideo(blob, apiKey, backendUrl, metadata) {
|
|
23103
23212
|
this.uploadCallbacks.onClearStatus();
|
|
23213
|
+
this.isUploadInProgress = true;
|
|
23214
|
+
return this.uploadVideoAfterStatusClear(blob, apiKey, backendUrl, metadata).catch((error) => {
|
|
23215
|
+
this.isUploadInProgress = false;
|
|
23216
|
+
throw error;
|
|
23217
|
+
});
|
|
23218
|
+
}
|
|
23219
|
+
async uploadVideoAfterStatusClear(blob, apiKey, backendUrl, metadata) {
|
|
23104
23220
|
const duration = await extractVideoDuration(blob);
|
|
23105
|
-
const
|
|
23221
|
+
const outputFormat = this.configManager.getConfigForRecording().format;
|
|
23222
|
+
const filename = `recording-${Date.now()}.${getFileExtensionForOutputFormat(outputFormat)}`;
|
|
23223
|
+
const mimeType = getMimeTypeForOutputFormat(outputFormat);
|
|
23106
23224
|
const sourceType = this.getCurrentSourceType();
|
|
23107
23225
|
let userMetadata;
|
|
23108
23226
|
if (Object.keys(metadata).length > 0) {
|
|
@@ -23119,6 +23237,7 @@ class RecorderController {
|
|
|
23119
23237
|
apiKey,
|
|
23120
23238
|
backendUrl,
|
|
23121
23239
|
filename,
|
|
23240
|
+
mimeType,
|
|
23122
23241
|
duration,
|
|
23123
23242
|
sourceType,
|
|
23124
23243
|
userMetadata
|
|
@@ -23130,6 +23249,7 @@ class RecorderController {
|
|
|
23130
23249
|
apiKey,
|
|
23131
23250
|
backendUrl,
|
|
23132
23251
|
filename,
|
|
23252
|
+
mimeType,
|
|
23133
23253
|
duration,
|
|
23134
23254
|
metadata: undefined,
|
|
23135
23255
|
userMetadata
|
|
@@ -23156,6 +23276,7 @@ class RecorderController {
|
|
|
23156
23276
|
apiKey: options.apiKey,
|
|
23157
23277
|
backendUrl: options.backendUrl,
|
|
23158
23278
|
filename: options.filename,
|
|
23279
|
+
mimeType: options.mimeType,
|
|
23159
23280
|
metadata: undefined,
|
|
23160
23281
|
userMetadata: options.userMetadata,
|
|
23161
23282
|
onProgress: this.uploadCallbacks.onProgress
|
|
@@ -23176,6 +23297,8 @@ class RecorderController {
|
|
|
23176
23297
|
sourceType: options.sourceType
|
|
23177
23298
|
}, uploadError);
|
|
23178
23299
|
throw uploadError;
|
|
23300
|
+
} finally {
|
|
23301
|
+
this.isUploadInProgress = false;
|
|
23179
23302
|
}
|
|
23180
23303
|
}
|
|
23181
23304
|
getStream() {
|
|
@@ -23199,6 +23322,7 @@ class RecorderController {
|
|
|
23199
23322
|
}
|
|
23200
23323
|
cleanup() {
|
|
23201
23324
|
this.isDestroyed = true;
|
|
23325
|
+
this.isUploadInProgress = false;
|
|
23202
23326
|
if (this.recordingWarmupTimeoutId !== null) {
|
|
23203
23327
|
clearTimeout(this.recordingWarmupTimeoutId);
|
|
23204
23328
|
this.recordingWarmupTimeoutId = null;
|
|
@@ -23240,6 +23364,9 @@ class RecorderController {
|
|
|
23240
23364
|
getUploadService() {
|
|
23241
23365
|
return this.uploadService;
|
|
23242
23366
|
}
|
|
23367
|
+
isUploading() {
|
|
23368
|
+
return this.isUploadInProgress;
|
|
23369
|
+
}
|
|
23243
23370
|
isRecording() {
|
|
23244
23371
|
return this.streamManager.isRecording();
|
|
23245
23372
|
}
|
|
@@ -23338,7 +23465,10 @@ class RecorderController {
|
|
|
23338
23465
|
isSecureContext: profile.features.isSecureContext,
|
|
23339
23466
|
hasMediaRecorder: profile.features.hasMediaRecorder,
|
|
23340
23467
|
hasVideoEncoder: profile.features.hasVideoEncoder,
|
|
23341
|
-
supportedCodecIds: profile.codecProbeResults.filter((result) => result.supported).map((result) => result.id)
|
|
23468
|
+
supportedCodecIds: profile.codecProbeResults.filter((result) => result.supported).map((result) => result.id),
|
|
23469
|
+
supportedEncodingCodecIds: profile.codecProbeResults.filter((result) => result.encodingCapability?.supported === true).map((result) => result.id),
|
|
23470
|
+
smoothEncodingCodecIds: profile.codecProbeResults.filter((result) => result.encodingCapability?.smooth === true).map((result) => result.id),
|
|
23471
|
+
powerEfficientEncodingCodecIds: profile.codecProbeResults.filter((result) => result.encodingCapability?.powerEfficient === true).map((result) => result.id)
|
|
23342
23472
|
}
|
|
23343
23473
|
};
|
|
23344
23474
|
}
|
|
@@ -23392,9 +23522,10 @@ class RecorderController {
|
|
|
23392
23522
|
const probeResult = this.storageManager.getWriteProbeResult();
|
|
23393
23523
|
if (!probeResult?.ok) {
|
|
23394
23524
|
const reason = probeResult?.reason ?? "Storage write probe did not complete";
|
|
23525
|
+
const storageWriteProbeError = new Error(reason);
|
|
23395
23526
|
this.telemetryManager.sendEvent("storage.write.probe.failed", {
|
|
23396
23527
|
reason
|
|
23397
|
-
});
|
|
23528
|
+
}, storageWriteProbeError);
|
|
23398
23529
|
const onStorageWriteError = resolveStorageWriteErrorCallback(this.callbacks);
|
|
23399
23530
|
onStorageWriteError(reason);
|
|
23400
23531
|
return;
|
|
@@ -23409,6 +23540,7 @@ class RecorderController {
|
|
|
23409
23540
|
this.uploadCallbacks.onProgress(progress);
|
|
23410
23541
|
},
|
|
23411
23542
|
onUploadComplete: (id, result) => {
|
|
23543
|
+
this.isUploadInProgress = false;
|
|
23412
23544
|
this.uploadCallbacks.onSuccess(result);
|
|
23413
23545
|
const metadata = this.uploadMetadataManager.getMetadata(id);
|
|
23414
23546
|
if (metadata) {
|
|
@@ -23422,6 +23554,7 @@ class RecorderController {
|
|
|
23422
23554
|
}
|
|
23423
23555
|
},
|
|
23424
23556
|
onUploadError: (id, error) => {
|
|
23557
|
+
this.isUploadInProgress = false;
|
|
23425
23558
|
this.uploadCallbacks.onError(error);
|
|
23426
23559
|
const metadata = this.uploadMetadataManager.getMetadata(id);
|
|
23427
23560
|
if (metadata) {
|
|
@@ -23854,8 +23987,10 @@ class VidtreoRecorder {
|
|
|
23854
23987
|
async stopRecording() {
|
|
23855
23988
|
await this.ensureInitialized();
|
|
23856
23989
|
const blob = await this.controller.stopRecording();
|
|
23990
|
+
const outputFormat = (await this.controller.getConfig()).format;
|
|
23857
23991
|
const timestamp = Date.now();
|
|
23858
|
-
const filename = `recording-${timestamp}
|
|
23992
|
+
const filename = `recording-${timestamp}.${getFileExtensionForOutputFormat(outputFormat)}`;
|
|
23993
|
+
const mimeType = getMimeTypeForOutputFormat(outputFormat);
|
|
23859
23994
|
const uploadService = this.controller.getUploadService();
|
|
23860
23995
|
if (!uploadService) {
|
|
23861
23996
|
throw new Error("Upload service not available");
|
|
@@ -23864,6 +23999,7 @@ class VidtreoRecorder {
|
|
|
23864
23999
|
apiKey: this.config.apiKey,
|
|
23865
24000
|
backendUrl: this.config.apiUrl,
|
|
23866
24001
|
filename,
|
|
24002
|
+
mimeType,
|
|
23867
24003
|
userMetadata: this.config.userMetadata,
|
|
23868
24004
|
onProgress: this.config.onUploadProgress
|
|
23869
24005
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vidtreo/recorder",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.3",
|
|
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",
|