@vidtreo/recorder 1.2.1 → 1.3.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/README.md +5 -0
- package/dist/index.d.ts +127 -8
- package/dist/index.js +847 -282
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -215,42 +215,102 @@ function validateBrowserSupport() {
|
|
|
215
215
|
throw error;
|
|
216
216
|
}
|
|
217
217
|
}
|
|
218
|
-
// src/core/processor/format-codec-mapper.ts
|
|
219
|
-
var FORMAT_DEFAULT_CODECS = {
|
|
220
|
-
mp4: "aac",
|
|
221
|
-
mov: "aac",
|
|
222
|
-
mkv: "opus",
|
|
223
|
-
webm: "opus"
|
|
224
|
-
};
|
|
225
|
-
function getDefaultAudioCodecForFormat(format) {
|
|
226
|
-
return FORMAT_DEFAULT_CODECS[format];
|
|
227
|
-
}
|
|
228
|
-
function getAudioCodecForFormat(format, overrideCodec) {
|
|
229
|
-
if (overrideCodec) {
|
|
230
|
-
return overrideCodec;
|
|
231
|
-
}
|
|
232
|
-
return getDefaultAudioCodecForFormat(format);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
218
|
// src/core/config/config-constants.ts
|
|
236
219
|
var BYTES_PER_MEGABYTE = 1e6;
|
|
237
220
|
var BITS_PER_BYTE = 8;
|
|
238
221
|
var SECONDS_PER_MINUTE = 60;
|
|
239
|
-
var
|
|
240
|
-
var
|
|
241
|
-
var
|
|
242
|
-
var
|
|
243
|
-
var
|
|
244
|
-
var
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
222
|
+
var SD_SIZE_LIMIT_MB_PER_MINUTE = 4;
|
|
223
|
+
var HD_SIZE_LIMIT_MB_PER_MINUTE = 8;
|
|
224
|
+
var FHD_SIZE_LIMIT_MB_PER_MINUTE = 18;
|
|
225
|
+
var K4_SIZE_LIMIT_MB_PER_MINUTE = 46;
|
|
226
|
+
var MP4_AUDIO_BITRATE = 128000;
|
|
227
|
+
var WEBM_AUDIO_BITRATE = 96000;
|
|
228
|
+
var VIDEO_CODEC_AVC = "avc";
|
|
229
|
+
var VIDEO_CODEC_VP9 = "vp9";
|
|
230
|
+
var VIDEO_CODEC_AV1 = "av1";
|
|
231
|
+
var VIDEO_CODEC_HEVC = "hevc";
|
|
232
|
+
var VIDEO_CODEC_VP8 = "vp8";
|
|
233
|
+
var AUDIO_CODEC_AAC = "aac";
|
|
234
|
+
var AUDIO_CODEC_OPUS = "opus";
|
|
235
|
+
var FORMAT_COMPATIBILITY_POLICY = {
|
|
236
|
+
mp4: {
|
|
237
|
+
preferredVideoCodec: VIDEO_CODEC_AVC,
|
|
238
|
+
preferredAudioCodec: AUDIO_CODEC_AAC,
|
|
239
|
+
audioBitrate: MP4_AUDIO_BITRATE,
|
|
240
|
+
videoCodecFallbackOrder: [
|
|
241
|
+
VIDEO_CODEC_AVC,
|
|
242
|
+
VIDEO_CODEC_VP9,
|
|
243
|
+
VIDEO_CODEC_AV1,
|
|
244
|
+
VIDEO_CODEC_HEVC,
|
|
245
|
+
VIDEO_CODEC_VP8
|
|
246
|
+
],
|
|
247
|
+
audioCodecFallbackOrder: [AUDIO_CODEC_AAC, AUDIO_CODEC_OPUS]
|
|
248
|
+
},
|
|
249
|
+
mov: {
|
|
250
|
+
preferredVideoCodec: VIDEO_CODEC_AVC,
|
|
251
|
+
preferredAudioCodec: AUDIO_CODEC_AAC,
|
|
252
|
+
audioBitrate: MP4_AUDIO_BITRATE,
|
|
253
|
+
videoCodecFallbackOrder: [
|
|
254
|
+
VIDEO_CODEC_AVC,
|
|
255
|
+
VIDEO_CODEC_VP9,
|
|
256
|
+
VIDEO_CODEC_AV1,
|
|
257
|
+
VIDEO_CODEC_HEVC,
|
|
258
|
+
VIDEO_CODEC_VP8
|
|
259
|
+
],
|
|
260
|
+
audioCodecFallbackOrder: [AUDIO_CODEC_AAC, AUDIO_CODEC_OPUS]
|
|
261
|
+
},
|
|
262
|
+
mkv: {
|
|
263
|
+
preferredVideoCodec: VIDEO_CODEC_AVC,
|
|
264
|
+
preferredAudioCodec: AUDIO_CODEC_AAC,
|
|
265
|
+
audioBitrate: MP4_AUDIO_BITRATE,
|
|
266
|
+
videoCodecFallbackOrder: [
|
|
267
|
+
VIDEO_CODEC_AVC,
|
|
268
|
+
VIDEO_CODEC_VP9,
|
|
269
|
+
VIDEO_CODEC_AV1,
|
|
270
|
+
VIDEO_CODEC_HEVC,
|
|
271
|
+
VIDEO_CODEC_VP8
|
|
272
|
+
],
|
|
273
|
+
audioCodecFallbackOrder: [AUDIO_CODEC_AAC, AUDIO_CODEC_OPUS]
|
|
274
|
+
},
|
|
275
|
+
webm: {
|
|
276
|
+
preferredVideoCodec: VIDEO_CODEC_VP9,
|
|
277
|
+
preferredAudioCodec: AUDIO_CODEC_OPUS,
|
|
278
|
+
audioBitrate: WEBM_AUDIO_BITRATE,
|
|
279
|
+
videoCodecFallbackOrder: [
|
|
280
|
+
VIDEO_CODEC_VP9,
|
|
281
|
+
VIDEO_CODEC_AV1,
|
|
282
|
+
VIDEO_CODEC_VP8
|
|
283
|
+
],
|
|
284
|
+
audioCodecFallbackOrder: [AUDIO_CODEC_OPUS]
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
var PRESET_SIZE_LIMIT_MB_PER_MINUTE = {
|
|
288
|
+
sd: SD_SIZE_LIMIT_MB_PER_MINUTE,
|
|
289
|
+
hd: HD_SIZE_LIMIT_MB_PER_MINUTE,
|
|
290
|
+
fhd: FHD_SIZE_LIMIT_MB_PER_MINUTE,
|
|
291
|
+
"4k": K4_SIZE_LIMIT_MB_PER_MINUTE
|
|
249
292
|
};
|
|
250
|
-
function
|
|
293
|
+
function resolveLinuxAudioPolicy(formatCompatibilityPolicy) {
|
|
294
|
+
return {
|
|
295
|
+
...formatCompatibilityPolicy,
|
|
296
|
+
preferredAudioCodec: AUDIO_CODEC_OPUS,
|
|
297
|
+
audioCodecFallbackOrder: [AUDIO_CODEC_OPUS]
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
function getFormatCompatibilityPolicy(format, formatCompatibilityContext) {
|
|
301
|
+
const formatCompatibilityPolicy = FORMAT_COMPATIBILITY_POLICY[format];
|
|
302
|
+
if (!formatCompatibilityContext) {
|
|
303
|
+
return formatCompatibilityPolicy;
|
|
304
|
+
}
|
|
305
|
+
if (!formatCompatibilityContext.isLinuxPlatform) {
|
|
306
|
+
return formatCompatibilityPolicy;
|
|
307
|
+
}
|
|
308
|
+
return resolveLinuxAudioPolicy(formatCompatibilityPolicy);
|
|
309
|
+
}
|
|
310
|
+
function calculateTotalBitrateFromMbPerMinute(sizeMbPerMinute) {
|
|
251
311
|
const bytesPerMinute = sizeMbPerMinute * BYTES_PER_MEGABYTE;
|
|
252
312
|
const bitsPerMinute = bytesPerMinute * BITS_PER_BYTE;
|
|
253
|
-
return Math.
|
|
313
|
+
return Math.floor(bitsPerMinute / SECONDS_PER_MINUTE);
|
|
254
314
|
}
|
|
255
315
|
function calculateVideoBitrate(totalBitrate, audioBitrate) {
|
|
256
316
|
let videoBitrate = totalBitrate - audioBitrate;
|
|
@@ -259,11 +319,24 @@ function calculateVideoBitrate(totalBitrate, audioBitrate) {
|
|
|
259
319
|
}
|
|
260
320
|
return videoBitrate;
|
|
261
321
|
}
|
|
322
|
+
function getPresetTotalBitrate(preset) {
|
|
323
|
+
const sizeLimit = PRESET_SIZE_LIMIT_MB_PER_MINUTE[preset];
|
|
324
|
+
return calculateTotalBitrateFromMbPerMinute(sizeLimit);
|
|
325
|
+
}
|
|
326
|
+
function getPresetAudioBitrateForFormat(format) {
|
|
327
|
+
const policy = getFormatCompatibilityPolicy(format);
|
|
328
|
+
return policy.audioBitrate;
|
|
329
|
+
}
|
|
330
|
+
function getPresetVideoBitrateForFormat(preset, format) {
|
|
331
|
+
const totalBitrate = getPresetTotalBitrate(preset);
|
|
332
|
+
const audioBitrate = getPresetAudioBitrateForFormat(format);
|
|
333
|
+
return calculateVideoBitrate(totalBitrate, audioBitrate);
|
|
334
|
+
}
|
|
262
335
|
var PRESET_VIDEO_BITRATE_MAP = {
|
|
263
|
-
sd:
|
|
264
|
-
hd:
|
|
265
|
-
fhd:
|
|
266
|
-
"4k":
|
|
336
|
+
sd: getPresetVideoBitrateForFormat("sd", "mp4"),
|
|
337
|
+
hd: getPresetVideoBitrateForFormat("hd", "mp4"),
|
|
338
|
+
fhd: getPresetVideoBitrateForFormat("fhd", "mp4"),
|
|
339
|
+
"4k": getPresetVideoBitrateForFormat("4k", "mp4")
|
|
267
340
|
};
|
|
268
341
|
var RESOLUTION_MAP = {
|
|
269
342
|
sd: { width: 854, height: 480 },
|
|
@@ -283,9 +356,9 @@ var DEFAULT_TRANSCODE_CONFIG = Object.freeze({
|
|
|
283
356
|
fps: 30,
|
|
284
357
|
width: RESOLUTION_MAP.fhd.width,
|
|
285
358
|
height: RESOLUTION_MAP.fhd.height,
|
|
286
|
-
bitrate:
|
|
287
|
-
audioCodec: "
|
|
288
|
-
audioBitrate:
|
|
359
|
+
bitrate: getPresetVideoBitrateForFormat("fhd", "mp4"),
|
|
360
|
+
audioCodec: getFormatCompatibilityPolicy("mp4").preferredAudioCodec,
|
|
361
|
+
audioBitrate: getPresetAudioBitrateForFormat("mp4"),
|
|
289
362
|
watermark: {
|
|
290
363
|
url: "https://avatars.githubusercontent.com/u/244247750?s=200&v=4",
|
|
291
364
|
opacity: 1,
|
|
@@ -293,171 +366,130 @@ var DEFAULT_TRANSCODE_CONFIG = Object.freeze({
|
|
|
293
366
|
}
|
|
294
367
|
});
|
|
295
368
|
function getDefaultConfigForFormat(format) {
|
|
369
|
+
const policy = getFormatCompatibilityPolicy(format);
|
|
296
370
|
return {
|
|
297
371
|
...DEFAULT_TRANSCODE_CONFIG,
|
|
298
372
|
format,
|
|
299
|
-
|
|
373
|
+
bitrate: getPresetVideoBitrateForFormat("fhd", format),
|
|
374
|
+
audioCodec: policy.preferredAudioCodec,
|
|
375
|
+
audioBitrate: policy.audioBitrate
|
|
300
376
|
};
|
|
301
377
|
}
|
|
302
|
-
// src/core/
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
var AUDIO_CODEC_CANDIDATES = [
|
|
310
|
-
AUDIO_CODEC_AAC,
|
|
311
|
-
AUDIO_CODEC_OPUS
|
|
312
|
-
];
|
|
313
|
-
async function resolveMediabunnyModule(dependencies) {
|
|
314
|
-
if (dependencies?.loadMediabunny) {
|
|
315
|
-
return await dependencies.loadMediabunny();
|
|
316
|
-
}
|
|
317
|
-
return await import("mediabunny");
|
|
318
|
-
}
|
|
319
|
-
function buildVideoCodecCheckOptions(width, height, bitrate) {
|
|
320
|
-
let checkOptions = {};
|
|
321
|
-
if (width !== undefined) {
|
|
322
|
-
checkOptions = { ...checkOptions, width };
|
|
323
|
-
}
|
|
324
|
-
if (height !== undefined) {
|
|
325
|
-
checkOptions = { ...checkOptions, height };
|
|
378
|
+
// src/core/utils/device-detector.ts
|
|
379
|
+
import { UAParser as UAParser2 } from "ua-parser-js";
|
|
380
|
+
function isMobileDevice() {
|
|
381
|
+
const parser = new UAParser2;
|
|
382
|
+
const device = parser.getDevice();
|
|
383
|
+
if (device.type === "mobile") {
|
|
384
|
+
return true;
|
|
326
385
|
}
|
|
327
|
-
if (
|
|
328
|
-
|
|
386
|
+
if (device.type === "tablet") {
|
|
387
|
+
return true;
|
|
329
388
|
}
|
|
330
|
-
return
|
|
389
|
+
return false;
|
|
331
390
|
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
391
|
+
|
|
392
|
+
// src/core/utils/platform-detection.ts
|
|
393
|
+
var PLATFORM_KEYWORD_LINUX = "linux";
|
|
394
|
+
var PLATFORM_KEYWORD_ANDROID = "android";
|
|
395
|
+
var RUNTIME_MARKER_BUN = "bun/";
|
|
396
|
+
var RUNTIME_MARKER_HAPPY_DOM = "happydom";
|
|
397
|
+
var RUNTIME_MARKER_NODE = "node.js";
|
|
398
|
+
function normalizeValue(value) {
|
|
399
|
+
if (value === undefined) {
|
|
400
|
+
return "";
|
|
336
401
|
}
|
|
337
|
-
return
|
|
338
|
-
}
|
|
339
|
-
function createMediabunnyModuleResult(module) {
|
|
340
|
-
return { kind: "result", module };
|
|
341
|
-
}
|
|
342
|
-
function createMediabunnyModuleError(error) {
|
|
343
|
-
return { kind: "error", error };
|
|
344
|
-
}
|
|
345
|
-
function createVideoCodecSupportResult(supported) {
|
|
346
|
-
return { kind: "result", supported };
|
|
347
|
-
}
|
|
348
|
-
function createVideoCodecSupportError(error) {
|
|
349
|
-
return { kind: "error", error };
|
|
402
|
+
return value.toLowerCase();
|
|
350
403
|
}
|
|
351
|
-
|
|
352
|
-
return
|
|
404
|
+
function hasPlatformKeyword(value, keyword) {
|
|
405
|
+
return value.includes(keyword);
|
|
353
406
|
}
|
|
354
|
-
function
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
function createAudioCodecLookupError(error) {
|
|
358
|
-
return { kind: "error", error };
|
|
359
|
-
}
|
|
360
|
-
async function detectBestCodec(width, height, bitrate, dependencies) {
|
|
361
|
-
const mediabunnyModuleResult = await resolveMediabunnyModule(dependencies).then((module) => createMediabunnyModuleResult(module)).catch((error) => createMediabunnyModuleError(error));
|
|
362
|
-
if (mediabunnyModuleResult.kind === "error") {
|
|
363
|
-
return VIDEO_CODEC_AVC;
|
|
364
|
-
}
|
|
365
|
-
const { canEncodeVideo } = mediabunnyModuleResult.module;
|
|
366
|
-
if (typeof canEncodeVideo !== "function") {
|
|
367
|
-
return VIDEO_CODEC_AVC;
|
|
407
|
+
function isNonBrowserRuntime(userAgent) {
|
|
408
|
+
if (userAgent.includes(RUNTIME_MARKER_BUN)) {
|
|
409
|
+
return true;
|
|
368
410
|
}
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
if (videoCodecSupportResult.kind === "error") {
|
|
372
|
-
return VIDEO_CODEC_AVC;
|
|
411
|
+
if (userAgent.includes(RUNTIME_MARKER_HAPPY_DOM)) {
|
|
412
|
+
return true;
|
|
373
413
|
}
|
|
374
|
-
if (
|
|
375
|
-
return
|
|
414
|
+
if (userAgent.includes(RUNTIME_MARKER_NODE)) {
|
|
415
|
+
return true;
|
|
376
416
|
}
|
|
377
|
-
return
|
|
417
|
+
return false;
|
|
378
418
|
}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
return VIDEO_CODEC_VP9;
|
|
383
|
-
}
|
|
384
|
-
const { canEncodeVideo } = mediabunnyModuleResult.module;
|
|
385
|
-
if (typeof canEncodeVideo !== "function") {
|
|
386
|
-
return VIDEO_CODEC_VP9;
|
|
419
|
+
function resolveNavigatorProvider(navigatorProvider) {
|
|
420
|
+
if (navigatorProvider) {
|
|
421
|
+
return navigatorProvider;
|
|
387
422
|
}
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
if (av1SupportResult.kind === "error") {
|
|
391
|
-
return VIDEO_CODEC_VP9;
|
|
392
|
-
}
|
|
393
|
-
if (av1SupportResult.supported) {
|
|
394
|
-
return VIDEO_CODEC_AV1;
|
|
395
|
-
}
|
|
396
|
-
const vp9SupportResult = await getVideoCodecSupport(canEncodeVideo, VIDEO_CODEC_VP9, checkOptions);
|
|
397
|
-
if (vp9SupportResult.kind === "error") {
|
|
398
|
-
return VIDEO_CODEC_VP9;
|
|
399
|
-
}
|
|
400
|
-
if (vp9SupportResult.supported) {
|
|
401
|
-
return VIDEO_CODEC_VP9;
|
|
423
|
+
if (typeof navigator === "undefined") {
|
|
424
|
+
return null;
|
|
402
425
|
}
|
|
403
|
-
|
|
426
|
+
const globalNavigator = navigator;
|
|
427
|
+
return {
|
|
428
|
+
platform: globalNavigator.platform,
|
|
429
|
+
userAgent: globalNavigator.userAgent,
|
|
430
|
+
userAgentData: globalNavigator.userAgentData
|
|
431
|
+
};
|
|
404
432
|
}
|
|
405
|
-
|
|
406
|
-
const
|
|
407
|
-
if (
|
|
408
|
-
return
|
|
433
|
+
function isLinuxPlatform(navigatorProvider) {
|
|
434
|
+
const resolvedNavigatorProvider = resolveNavigatorProvider(navigatorProvider);
|
|
435
|
+
if (resolvedNavigatorProvider === null) {
|
|
436
|
+
return false;
|
|
409
437
|
}
|
|
410
|
-
const
|
|
411
|
-
if (
|
|
412
|
-
return
|
|
438
|
+
const userAgent = normalizeValue(resolvedNavigatorProvider.userAgent);
|
|
439
|
+
if (isNonBrowserRuntime(userAgent)) {
|
|
440
|
+
return false;
|
|
413
441
|
}
|
|
414
|
-
const
|
|
415
|
-
const
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
throw new Error(ERROR_NO_SUPPORTED_AUDIO_CODEC);
|
|
420
|
-
}
|
|
421
|
-
return AUDIO_CODEC_AAC;
|
|
442
|
+
const platform = normalizeValue(resolvedNavigatorProvider.platform);
|
|
443
|
+
const userAgentDataPlatform = normalizeValue(resolvedNavigatorProvider.userAgentData?.platform);
|
|
444
|
+
const isAndroidPlatform = hasPlatformKeyword(platform, PLATFORM_KEYWORD_ANDROID) || hasPlatformKeyword(userAgentDataPlatform, PLATFORM_KEYWORD_ANDROID) || hasPlatformKeyword(userAgent, PLATFORM_KEYWORD_ANDROID);
|
|
445
|
+
if (isAndroidPlatform) {
|
|
446
|
+
return false;
|
|
422
447
|
}
|
|
423
|
-
if (
|
|
424
|
-
|
|
448
|
+
if (platform.includes(PLATFORM_KEYWORD_LINUX)) {
|
|
449
|
+
return true;
|
|
425
450
|
}
|
|
426
|
-
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
// src/core/utils/device-detector.ts
|
|
430
|
-
import { UAParser as UAParser2 } from "ua-parser-js";
|
|
431
|
-
function isMobileDevice() {
|
|
432
|
-
const parser = new UAParser2;
|
|
433
|
-
const device = parser.getDevice();
|
|
434
|
-
if (device.type === "mobile") {
|
|
451
|
+
if (userAgentDataPlatform.includes(PLATFORM_KEYWORD_LINUX)) {
|
|
435
452
|
return true;
|
|
436
453
|
}
|
|
437
|
-
if (
|
|
454
|
+
if (userAgent.includes(PLATFORM_KEYWORD_LINUX)) {
|
|
438
455
|
return true;
|
|
439
456
|
}
|
|
440
457
|
return false;
|
|
441
458
|
}
|
|
442
459
|
|
|
443
460
|
// src/core/config/preset-mapper.ts
|
|
444
|
-
|
|
461
|
+
function mapPresetToConfig(options) {
|
|
445
462
|
const { preset, outputFormat, watermark, isMobile } = options;
|
|
446
|
-
if (!(preset in
|
|
447
|
-
|
|
463
|
+
if (!(preset in RESOLUTION_MAP)) {
|
|
464
|
+
return Promise.reject(new Error(`Invalid preset: ${preset}`));
|
|
465
|
+
}
|
|
466
|
+
let isMobileDeviceDetected = isMobile;
|
|
467
|
+
if (isMobileDeviceDetected === undefined) {
|
|
468
|
+
isMobileDeviceDetected = isMobileDevice();
|
|
469
|
+
}
|
|
470
|
+
let resolutionMap = RESOLUTION_MAP;
|
|
471
|
+
if (isMobileDeviceDetected) {
|
|
472
|
+
resolutionMap = MOBILE_RESOLUTION_MAP;
|
|
448
473
|
}
|
|
449
|
-
const isMobileDeviceDetected = isMobile === undefined ? isMobileDevice() : isMobile;
|
|
450
|
-
const resolutionMap = isMobileDeviceDetected ? MOBILE_RESOLUTION_MAP : RESOLUTION_MAP;
|
|
451
474
|
const { width, height } = resolutionMap[preset];
|
|
452
|
-
|
|
453
|
-
|
|
475
|
+
let format = outputFormat;
|
|
476
|
+
if (format === undefined) {
|
|
477
|
+
format = "mp4";
|
|
478
|
+
}
|
|
479
|
+
let isLinuxPlatformDetected = options.isLinuxPlatform;
|
|
480
|
+
if (isLinuxPlatformDetected === undefined) {
|
|
481
|
+
isLinuxPlatformDetected = isLinuxPlatform();
|
|
482
|
+
}
|
|
483
|
+
const policy = getFormatCompatibilityPolicy(format, {
|
|
484
|
+
isLinuxPlatform: isLinuxPlatformDetected
|
|
485
|
+
});
|
|
454
486
|
const config = {
|
|
455
487
|
format,
|
|
456
488
|
width,
|
|
457
489
|
height,
|
|
458
|
-
bitrate:
|
|
459
|
-
audioCodec,
|
|
460
|
-
audioBitrate:
|
|
490
|
+
bitrate: getPresetVideoBitrateForFormat(preset, format),
|
|
491
|
+
audioCodec: policy.preferredAudioCodec,
|
|
492
|
+
audioBitrate: policy.audioBitrate
|
|
461
493
|
};
|
|
462
494
|
if (watermark) {
|
|
463
495
|
config.watermark = {
|
|
@@ -466,7 +498,7 @@ async function mapPresetToConfig(options) {
|
|
|
466
498
|
position: watermark.position
|
|
467
499
|
};
|
|
468
500
|
}
|
|
469
|
-
return config;
|
|
501
|
+
return Promise.resolve(config);
|
|
470
502
|
}
|
|
471
503
|
|
|
472
504
|
// src/core/config/config-service.ts
|
|
@@ -589,14 +621,7 @@ class ConfigManager {
|
|
|
589
621
|
apiKey,
|
|
590
622
|
backendUrl: normalizedBackendUrl
|
|
591
623
|
});
|
|
592
|
-
this.
|
|
593
|
-
this.currentConfig = config;
|
|
594
|
-
this.configReady = this.configService?.isConfigReady() ?? false;
|
|
595
|
-
this.configFetched = true;
|
|
596
|
-
}).catch(() => {
|
|
597
|
-
this.configReady = false;
|
|
598
|
-
this.configFetched = true;
|
|
599
|
-
});
|
|
624
|
+
this.fetchConfigInBackground();
|
|
600
625
|
}
|
|
601
626
|
async fetchConfig() {
|
|
602
627
|
if (!this.configService) {
|
|
@@ -612,6 +637,12 @@ class ConfigManager {
|
|
|
612
637
|
}
|
|
613
638
|
return this.currentConfig;
|
|
614
639
|
}
|
|
640
|
+
getConfigForRecording() {
|
|
641
|
+
if (this.configService && !this.configFetched) {
|
|
642
|
+
this.fetchConfigInBackground();
|
|
643
|
+
}
|
|
644
|
+
return this.currentConfig;
|
|
645
|
+
}
|
|
615
646
|
isConfigReady() {
|
|
616
647
|
return this.configReady;
|
|
617
648
|
}
|
|
@@ -621,6 +652,12 @@ class ConfigManager {
|
|
|
621
652
|
}
|
|
622
653
|
this.configService.clearCache();
|
|
623
654
|
}
|
|
655
|
+
fetchConfigInBackground() {
|
|
656
|
+
this.fetchConfig().catch(() => {
|
|
657
|
+
this.configReady = false;
|
|
658
|
+
this.configFetched = true;
|
|
659
|
+
});
|
|
660
|
+
}
|
|
624
661
|
}
|
|
625
662
|
// src/core/device/device-manager.ts
|
|
626
663
|
class DeviceManager {
|
|
@@ -764,8 +801,160 @@ import {
|
|
|
764
801
|
Output,
|
|
765
802
|
WebMOutputFormat
|
|
766
803
|
} from "mediabunny";
|
|
804
|
+
|
|
805
|
+
// src/core/processor/codec-policy-resolver.ts
|
|
806
|
+
var VIDEO_HARDWARE_ACCELERATION_PREFERENCE = "prefer-hardware";
|
|
807
|
+
var FIRST_RESULT_INDEX = 0;
|
|
808
|
+
async function loadMediabunnyModuleDependency() {
|
|
809
|
+
const importResults = await Promise.allSettled([import("mediabunny")]);
|
|
810
|
+
const importResult = importResults[FIRST_RESULT_INDEX];
|
|
811
|
+
if (importResult.status === "fulfilled") {
|
|
812
|
+
return importResult.value;
|
|
813
|
+
}
|
|
814
|
+
return null;
|
|
815
|
+
}
|
|
816
|
+
var DEFAULT_CODEC_POLICY_RESOLVER_DEPENDENCIES = {
|
|
817
|
+
loadMediabunnyModule: loadMediabunnyModuleDependency
|
|
818
|
+
};
|
|
819
|
+
function resolveCodecPolicyResolverDependencies(dependencies) {
|
|
820
|
+
const loadMediabunnyModuleDependency2 = dependencies?.loadMediabunnyModule;
|
|
821
|
+
let resolvedLoadMediabunnyModuleDependency;
|
|
822
|
+
if (loadMediabunnyModuleDependency2 === undefined) {
|
|
823
|
+
resolvedLoadMediabunnyModuleDependency = DEFAULT_CODEC_POLICY_RESOLVER_DEPENDENCIES.loadMediabunnyModule;
|
|
824
|
+
} else {
|
|
825
|
+
resolvedLoadMediabunnyModuleDependency = loadMediabunnyModuleDependency2;
|
|
826
|
+
}
|
|
827
|
+
return {
|
|
828
|
+
loadMediabunnyModule: resolvedLoadMediabunnyModuleDependency
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
function addUniqueCandidate(candidates, candidate) {
|
|
832
|
+
if (candidate === undefined) {
|
|
833
|
+
return candidates;
|
|
834
|
+
}
|
|
835
|
+
const hasCandidate = candidates.includes(candidate);
|
|
836
|
+
if (hasCandidate) {
|
|
837
|
+
return candidates;
|
|
838
|
+
}
|
|
839
|
+
return [...candidates, candidate];
|
|
840
|
+
}
|
|
841
|
+
function buildVideoCandidates(overrideCodec, policy) {
|
|
842
|
+
let candidates = [];
|
|
843
|
+
if (overrideCodec !== undefined) {
|
|
844
|
+
const canUseOverride = policy.videoCodecFallbackOrder.includes(overrideCodec);
|
|
845
|
+
if (canUseOverride) {
|
|
846
|
+
candidates = addUniqueCandidate(candidates, overrideCodec);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
candidates = addUniqueCandidate(candidates, policy.preferredVideoCodec);
|
|
850
|
+
for (const fallbackCodec of policy.videoCodecFallbackOrder) {
|
|
851
|
+
candidates = addUniqueCandidate(candidates, fallbackCodec);
|
|
852
|
+
}
|
|
853
|
+
return candidates;
|
|
854
|
+
}
|
|
855
|
+
function buildAudioCandidates(overrideCodec, policy) {
|
|
856
|
+
let candidates = [];
|
|
857
|
+
if (overrideCodec !== undefined) {
|
|
858
|
+
const canUseOverride = policy.audioCodecFallbackOrder.includes(overrideCodec);
|
|
859
|
+
if (canUseOverride) {
|
|
860
|
+
candidates = addUniqueCandidate(candidates, overrideCodec);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
candidates = addUniqueCandidate(candidates, policy.preferredAudioCodec);
|
|
864
|
+
for (const fallbackCodec of policy.audioCodecFallbackOrder) {
|
|
865
|
+
candidates = addUniqueCandidate(candidates, fallbackCodec);
|
|
866
|
+
}
|
|
867
|
+
return candidates;
|
|
868
|
+
}
|
|
869
|
+
async function resolveVideoCodecFromPolicy(options) {
|
|
870
|
+
const dependencies = resolveCodecPolicyResolverDependencies(options.dependencies);
|
|
871
|
+
const mediabunnyModule = await dependencies.loadMediabunnyModule();
|
|
872
|
+
if (mediabunnyModule === null) {
|
|
873
|
+
return options.policy.preferredVideoCodec;
|
|
874
|
+
}
|
|
875
|
+
const canEncodeVideo = mediabunnyModule.canEncodeVideo;
|
|
876
|
+
if (typeof canEncodeVideo !== "function") {
|
|
877
|
+
return options.policy.preferredVideoCodec;
|
|
878
|
+
}
|
|
879
|
+
const candidates = buildVideoCandidates(options.overrideCodec, options.policy);
|
|
880
|
+
const videoCodecOptions = {
|
|
881
|
+
hardwareAcceleration: VIDEO_HARDWARE_ACCELERATION_PREFERENCE
|
|
882
|
+
};
|
|
883
|
+
if (options.width !== undefined) {
|
|
884
|
+
videoCodecOptions.width = options.width;
|
|
885
|
+
}
|
|
886
|
+
if (options.height !== undefined) {
|
|
887
|
+
videoCodecOptions.height = options.height;
|
|
888
|
+
}
|
|
889
|
+
if (options.bitrate !== undefined) {
|
|
890
|
+
videoCodecOptions.bitrate = options.bitrate;
|
|
891
|
+
}
|
|
892
|
+
const probeResults = await Promise.allSettled(candidates.map(async (codec) => {
|
|
893
|
+
const canEncode = await canEncodeVideo(codec, videoCodecOptions);
|
|
894
|
+
return {
|
|
895
|
+
codec,
|
|
896
|
+
canEncode
|
|
897
|
+
};
|
|
898
|
+
}));
|
|
899
|
+
let hasProbeError = false;
|
|
900
|
+
for (const probeResult of probeResults) {
|
|
901
|
+
if (probeResult.status === "rejected") {
|
|
902
|
+
hasProbeError = true;
|
|
903
|
+
continue;
|
|
904
|
+
}
|
|
905
|
+
if (probeResult.value.canEncode) {
|
|
906
|
+
return probeResult.value.codec;
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
if (hasProbeError) {
|
|
910
|
+
return options.policy.preferredVideoCodec;
|
|
911
|
+
}
|
|
912
|
+
if (options.shouldThrowIfNoCodecAvailable) {
|
|
913
|
+
throw new Error(`No encodable video codec available for format: ${options.format}`);
|
|
914
|
+
}
|
|
915
|
+
return options.policy.preferredVideoCodec;
|
|
916
|
+
}
|
|
917
|
+
async function resolveAudioCodecFromPolicy(options) {
|
|
918
|
+
const dependencies = resolveCodecPolicyResolverDependencies(options.dependencies);
|
|
919
|
+
const mediabunnyModule = await dependencies.loadMediabunnyModule();
|
|
920
|
+
if (mediabunnyModule === null) {
|
|
921
|
+
return options.policy.preferredAudioCodec;
|
|
922
|
+
}
|
|
923
|
+
const getFirstEncodableAudioCodec = mediabunnyModule.getFirstEncodableAudioCodec;
|
|
924
|
+
if (typeof getFirstEncodableAudioCodec !== "function") {
|
|
925
|
+
return options.policy.preferredAudioCodec;
|
|
926
|
+
}
|
|
927
|
+
const candidates = buildAudioCandidates(options.overrideCodec, options.policy);
|
|
928
|
+
const audioCodecOptions = {};
|
|
929
|
+
if (options.bitrate !== undefined) {
|
|
930
|
+
audioCodecOptions.bitrate = options.bitrate;
|
|
931
|
+
}
|
|
932
|
+
const probeResults = await Promise.allSettled([
|
|
933
|
+
getFirstEncodableAudioCodec(candidates, audioCodecOptions)
|
|
934
|
+
]);
|
|
935
|
+
const firstProbeResult = probeResults[FIRST_RESULT_INDEX];
|
|
936
|
+
if (firstProbeResult.status === "rejected") {
|
|
937
|
+
return options.policy.preferredAudioCodec;
|
|
938
|
+
}
|
|
939
|
+
const resolvedCodec = firstProbeResult.value;
|
|
940
|
+
if (resolvedCodec === null) {
|
|
941
|
+
if (options.shouldThrowIfNoCodecAvailable) {
|
|
942
|
+
throw new Error(`No encodable audio codec available for format: ${options.format}`);
|
|
943
|
+
}
|
|
944
|
+
return options.policy.preferredAudioCodec;
|
|
945
|
+
}
|
|
946
|
+
const canUseResolvedCodec = candidates.includes(resolvedCodec);
|
|
947
|
+
if (canUseResolvedCodec) {
|
|
948
|
+
return resolvedCodec;
|
|
949
|
+
}
|
|
950
|
+
if (options.shouldThrowIfNoCodecAvailable) {
|
|
951
|
+
throw new Error(`No encodable audio codec available for format: ${options.format}`);
|
|
952
|
+
}
|
|
953
|
+
return options.policy.preferredAudioCodec;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// src/core/transcode/video-transcoder.ts
|
|
767
957
|
var ALLOW_ROTATION_METADATA = false;
|
|
768
|
-
var OUTPUT_FORMAT_WEBM = "webm";
|
|
769
958
|
function createSource(input) {
|
|
770
959
|
if (typeof input === "string") {
|
|
771
960
|
return new FilePathSource(input);
|
|
@@ -803,14 +992,7 @@ function getMimeTypeForFormat(format) {
|
|
|
803
992
|
throw new Error(`Unsupported output format: ${format}`);
|
|
804
993
|
}
|
|
805
994
|
}
|
|
806
|
-
async function resolvePreferredVideoCodec(config) {
|
|
807
|
-
if (config.format === OUTPUT_FORMAT_WEBM) {
|
|
808
|
-
return await detectBestWebmCodec(config.width, config.height, config.bitrate);
|
|
809
|
-
}
|
|
810
|
-
return await detectBestCodec(config.width, config.height, config.bitrate);
|
|
811
|
-
}
|
|
812
995
|
function createConversionOptions(config, optimizeForSpeed = false) {
|
|
813
|
-
const audioCodec = getAudioCodecForFormat(config.format, config.audioCodec);
|
|
814
996
|
const video = {
|
|
815
997
|
fit: "contain",
|
|
816
998
|
forceTranscode: true,
|
|
@@ -838,7 +1020,7 @@ function createConversionOptions(config, optimizeForSpeed = false) {
|
|
|
838
1020
|
video.keyFrameInterval = 2;
|
|
839
1021
|
}
|
|
840
1022
|
const audio = {
|
|
841
|
-
codec: audioCodec,
|
|
1023
|
+
codec: config.audioCodec,
|
|
842
1024
|
forceTranscode: true,
|
|
843
1025
|
...optimizeForSpeed && { bitrateMode: "variable" }
|
|
844
1026
|
};
|
|
@@ -851,15 +1033,35 @@ function validateConversion(conversion) {
|
|
|
851
1033
|
}
|
|
852
1034
|
}
|
|
853
1035
|
async function transcodeVideo(input, config = {}, onProgress) {
|
|
1036
|
+
let resolvedFormat = config.format;
|
|
1037
|
+
if (resolvedFormat === undefined) {
|
|
1038
|
+
resolvedFormat = DEFAULT_TRANSCODE_CONFIG.format;
|
|
1039
|
+
}
|
|
854
1040
|
const finalConfig = {
|
|
855
1041
|
...DEFAULT_TRANSCODE_CONFIG,
|
|
856
1042
|
...config,
|
|
857
|
-
format:
|
|
1043
|
+
format: resolvedFormat
|
|
858
1044
|
};
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
}
|
|
862
|
-
finalConfig.
|
|
1045
|
+
const policy = getFormatCompatibilityPolicy(finalConfig.format, {
|
|
1046
|
+
isLinuxPlatform: isLinuxPlatform()
|
|
1047
|
+
});
|
|
1048
|
+
if (finalConfig.audioBitrate === undefined) {
|
|
1049
|
+
finalConfig.audioBitrate = getPresetAudioBitrateForFormat(finalConfig.format);
|
|
1050
|
+
}
|
|
1051
|
+
finalConfig.audioCodec = await resolveAudioCodecFromPolicy({
|
|
1052
|
+
format: finalConfig.format,
|
|
1053
|
+
overrideCodec: finalConfig.audioCodec,
|
|
1054
|
+
policy,
|
|
1055
|
+
bitrate: finalConfig.audioBitrate
|
|
1056
|
+
});
|
|
1057
|
+
finalConfig.codec = await resolveVideoCodecFromPolicy({
|
|
1058
|
+
format: finalConfig.format,
|
|
1059
|
+
overrideCodec: finalConfig.codec,
|
|
1060
|
+
policy,
|
|
1061
|
+
width: finalConfig.width,
|
|
1062
|
+
height: finalConfig.height,
|
|
1063
|
+
bitrate: finalConfig.bitrate
|
|
1064
|
+
});
|
|
863
1065
|
const source = createSource(input);
|
|
864
1066
|
const mediabunnyInput = new Input2({
|
|
865
1067
|
formats: ALL_FORMATS,
|
|
@@ -891,15 +1093,35 @@ async function transcodeVideo(input, config = {}, onProgress) {
|
|
|
891
1093
|
};
|
|
892
1094
|
}
|
|
893
1095
|
async function transcodeVideoForNativeCamera(file, config = {}, onProgress) {
|
|
1096
|
+
let resolvedFormat = config.format;
|
|
1097
|
+
if (resolvedFormat === undefined) {
|
|
1098
|
+
resolvedFormat = DEFAULT_TRANSCODE_CONFIG.format;
|
|
1099
|
+
}
|
|
894
1100
|
const finalConfig = {
|
|
895
1101
|
...DEFAULT_TRANSCODE_CONFIG,
|
|
896
1102
|
...config,
|
|
897
|
-
format:
|
|
1103
|
+
format: resolvedFormat
|
|
898
1104
|
};
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
}
|
|
902
|
-
finalConfig.
|
|
1105
|
+
const policy = getFormatCompatibilityPolicy(finalConfig.format, {
|
|
1106
|
+
isLinuxPlatform: isLinuxPlatform()
|
|
1107
|
+
});
|
|
1108
|
+
if (finalConfig.audioBitrate === undefined) {
|
|
1109
|
+
finalConfig.audioBitrate = getPresetAudioBitrateForFormat(finalConfig.format);
|
|
1110
|
+
}
|
|
1111
|
+
finalConfig.audioCodec = await resolveAudioCodecFromPolicy({
|
|
1112
|
+
format: finalConfig.format,
|
|
1113
|
+
overrideCodec: finalConfig.audioCodec,
|
|
1114
|
+
policy,
|
|
1115
|
+
bitrate: finalConfig.audioBitrate
|
|
1116
|
+
});
|
|
1117
|
+
finalConfig.codec = await resolveVideoCodecFromPolicy({
|
|
1118
|
+
format: finalConfig.format,
|
|
1119
|
+
overrideCodec: finalConfig.codec,
|
|
1120
|
+
policy,
|
|
1121
|
+
width: finalConfig.width,
|
|
1122
|
+
height: finalConfig.height,
|
|
1123
|
+
bitrate: finalConfig.bitrate
|
|
1124
|
+
});
|
|
903
1125
|
const source = new BlobSource2(file);
|
|
904
1126
|
const mediabunnyInput = new Input2({
|
|
905
1127
|
formats: ALL_FORMATS,
|
|
@@ -1067,6 +1289,25 @@ class NativeCameraHandler {
|
|
|
1067
1289
|
}
|
|
1068
1290
|
}
|
|
1069
1291
|
}
|
|
1292
|
+
// src/core/processor/format-codec-mapper.ts
|
|
1293
|
+
var FORMAT_COMPATIBILITY_CONTEXT = {
|
|
1294
|
+
isLinuxPlatform: isLinuxPlatform()
|
|
1295
|
+
};
|
|
1296
|
+
var FORMAT_DEFAULT_CODECS = {
|
|
1297
|
+
mp4: getFormatCompatibilityPolicy("mp4", FORMAT_COMPATIBILITY_CONTEXT).preferredAudioCodec,
|
|
1298
|
+
mov: getFormatCompatibilityPolicy("mov", FORMAT_COMPATIBILITY_CONTEXT).preferredAudioCodec,
|
|
1299
|
+
mkv: getFormatCompatibilityPolicy("mkv", FORMAT_COMPATIBILITY_CONTEXT).preferredAudioCodec,
|
|
1300
|
+
webm: getFormatCompatibilityPolicy("webm", FORMAT_COMPATIBILITY_CONTEXT).preferredAudioCodec
|
|
1301
|
+
};
|
|
1302
|
+
function getDefaultAudioCodecForFormat(format) {
|
|
1303
|
+
return FORMAT_DEFAULT_CODECS[format];
|
|
1304
|
+
}
|
|
1305
|
+
function getAudioCodecForFormat(format, overrideCodec) {
|
|
1306
|
+
if (overrideCodec) {
|
|
1307
|
+
return overrideCodec;
|
|
1308
|
+
}
|
|
1309
|
+
return getDefaultAudioCodecForFormat(format);
|
|
1310
|
+
}
|
|
1070
1311
|
// src/core/processor/worker/probe-worker-url.ts
|
|
1071
1312
|
var PROBE_WORKER_CODE = `"use strict";
|
|
1072
1313
|
var PROBE_MESSAGE_TYPE="probe";
|
|
@@ -1119,12 +1360,48 @@ var AUDIO_PATH_MAIN_THREAD_AUDIO_STREAM = "main-thread-audio-stream";
|
|
|
1119
1360
|
var AUDIO_PATH_AUDIO_WORKLET_CHUNKS = "audio-worklet-chunks";
|
|
1120
1361
|
var AUDIO_PATH_NONE_REQUIRED = "none-required";
|
|
1121
1362
|
var AUDIO_PATH_UNAVAILABLE = "unavailable";
|
|
1363
|
+
var SUPPORT_CACHE_KEY_SEPARATOR = "|";
|
|
1364
|
+
var SUPPORT_CACHE_AUDIO_PREFIX = "audio";
|
|
1365
|
+
var SUPPORT_CACHE_WATERMARK_PREFIX = "watermark";
|
|
1366
|
+
var SUPPORT_CACHE_TRUE_VALUE = "1";
|
|
1367
|
+
var SUPPORT_CACHE_FALSE_VALUE = "0";
|
|
1368
|
+
var NODE_ENV_TEST = "test";
|
|
1369
|
+
var supportReportCache = new Map;
|
|
1370
|
+
var supportReportPromiseCache = new Map;
|
|
1122
1371
|
function resolveBooleanOption(value, defaultValue) {
|
|
1123
1372
|
if (typeof value === "boolean") {
|
|
1124
1373
|
return value;
|
|
1125
1374
|
}
|
|
1126
1375
|
return defaultValue;
|
|
1127
1376
|
}
|
|
1377
|
+
function resolveSupportCacheBooleanValue(value) {
|
|
1378
|
+
if (value) {
|
|
1379
|
+
return SUPPORT_CACHE_TRUE_VALUE;
|
|
1380
|
+
}
|
|
1381
|
+
return SUPPORT_CACHE_FALSE_VALUE;
|
|
1382
|
+
}
|
|
1383
|
+
function createSupportCacheKey(requiresAudio, requiresWatermark) {
|
|
1384
|
+
const audioValue = resolveSupportCacheBooleanValue(requiresAudio);
|
|
1385
|
+
const watermarkValue = resolveSupportCacheBooleanValue(requiresWatermark);
|
|
1386
|
+
return [
|
|
1387
|
+
SUPPORT_CACHE_AUDIO_PREFIX,
|
|
1388
|
+
audioValue,
|
|
1389
|
+
SUPPORT_CACHE_WATERMARK_PREFIX,
|
|
1390
|
+
watermarkValue
|
|
1391
|
+
].join(SUPPORT_CACHE_KEY_SEPARATOR);
|
|
1392
|
+
}
|
|
1393
|
+
function shouldUseSupportCache() {
|
|
1394
|
+
if (typeof process === "undefined") {
|
|
1395
|
+
return true;
|
|
1396
|
+
}
|
|
1397
|
+
if (!process.env) {
|
|
1398
|
+
return true;
|
|
1399
|
+
}
|
|
1400
|
+
if (NODE_ENV_TEST === "development") {
|
|
1401
|
+
return false;
|
|
1402
|
+
}
|
|
1403
|
+
return true;
|
|
1404
|
+
}
|
|
1128
1405
|
function resolveVideoPath(inputs) {
|
|
1129
1406
|
if (inputs.probeResult.hasMediaStreamTrackProcessor) {
|
|
1130
1407
|
return VIDEO_PATH_WORKER_TRACK;
|
|
@@ -1257,6 +1534,30 @@ function getIsProbeFeaturesComplete(probeResult, requiresWatermark) {
|
|
|
1257
1534
|
async function checkRecorderSupport(options = {}) {
|
|
1258
1535
|
const requiresAudio = resolveBooleanOption(options.requiresAudio, true);
|
|
1259
1536
|
const requiresWatermark = resolveBooleanOption(options.requiresWatermark, false);
|
|
1537
|
+
if (!shouldUseSupportCache()) {
|
|
1538
|
+
return await buildSupportReport(requiresAudio, requiresWatermark);
|
|
1539
|
+
}
|
|
1540
|
+
const supportCacheKey = createSupportCacheKey(requiresAudio, requiresWatermark);
|
|
1541
|
+
const cachedReport = supportReportCache.get(supportCacheKey);
|
|
1542
|
+
if (cachedReport) {
|
|
1543
|
+
return cachedReport;
|
|
1544
|
+
}
|
|
1545
|
+
const inflightReport = supportReportPromiseCache.get(supportCacheKey);
|
|
1546
|
+
if (inflightReport) {
|
|
1547
|
+
return await inflightReport;
|
|
1548
|
+
}
|
|
1549
|
+
const reportPromise = buildSupportReport(requiresAudio, requiresWatermark).then((report) => {
|
|
1550
|
+
supportReportCache.set(supportCacheKey, report);
|
|
1551
|
+
supportReportPromiseCache.delete(supportCacheKey);
|
|
1552
|
+
return report;
|
|
1553
|
+
}).catch((error) => {
|
|
1554
|
+
supportReportPromiseCache.delete(supportCacheKey);
|
|
1555
|
+
throw error;
|
|
1556
|
+
});
|
|
1557
|
+
supportReportPromiseCache.set(supportCacheKey, reportPromise);
|
|
1558
|
+
return await reportPromise;
|
|
1559
|
+
}
|
|
1560
|
+
async function buildSupportReport(requiresAudio, requiresWatermark) {
|
|
1260
1561
|
const hasWorker = typeof Worker !== "undefined";
|
|
1261
1562
|
const audioContextClass = getAudioContextClass();
|
|
1262
1563
|
const hasAudioContext = audioContextClass !== null;
|
|
@@ -2962,6 +3263,11 @@ function formatTime(totalSeconds) {
|
|
|
2962
3263
|
}
|
|
2963
3264
|
|
|
2964
3265
|
// src/core/utils/tab-visibility-tracker.ts
|
|
3266
|
+
var MILLISECONDS_PER_SECOND = 1000;
|
|
3267
|
+
var DEFAULT_TAB_VISIBILITY_TRACKER_DEPENDENCIES = {
|
|
3268
|
+
getCurrentTimestamp: () => performance.now()
|
|
3269
|
+
};
|
|
3270
|
+
|
|
2965
3271
|
class TabVisibilityTracker {
|
|
2966
3272
|
recordingStartTime = 0;
|
|
2967
3273
|
totalPausedTime = 0;
|
|
@@ -2972,10 +3278,17 @@ class TabVisibilityTracker {
|
|
|
2972
3278
|
visibilityChangeHandler;
|
|
2973
3279
|
blurHandler;
|
|
2974
3280
|
focusHandler;
|
|
2975
|
-
|
|
3281
|
+
getCurrentTimestamp;
|
|
3282
|
+
constructor(dependencies) {
|
|
2976
3283
|
this.visibilityChangeHandler = this.handleVisibilityChange.bind(this);
|
|
2977
3284
|
this.blurHandler = this.handleBlur.bind(this);
|
|
2978
3285
|
this.focusHandler = this.handleFocus.bind(this);
|
|
3286
|
+
const getCurrentTimestampDependency = dependencies?.getCurrentTimestamp;
|
|
3287
|
+
if (getCurrentTimestampDependency === undefined) {
|
|
3288
|
+
this.getCurrentTimestamp = DEFAULT_TAB_VISIBILITY_TRACKER_DEPENDENCIES.getCurrentTimestamp;
|
|
3289
|
+
} else {
|
|
3290
|
+
this.getCurrentTimestamp = getCurrentTimestampDependency;
|
|
3291
|
+
}
|
|
2979
3292
|
}
|
|
2980
3293
|
start(recordingStartTime) {
|
|
2981
3294
|
if (this.isTracking) {
|
|
@@ -3000,14 +3313,14 @@ class TabVisibilityTracker {
|
|
|
3000
3313
|
if (!this.isTracking || this.pauseStartTime !== null) {
|
|
3001
3314
|
return;
|
|
3002
3315
|
}
|
|
3003
|
-
this.pauseStartTime =
|
|
3316
|
+
this.pauseStartTime = this.getCurrentTimestamp();
|
|
3004
3317
|
this.endCurrentIntervalIfActive();
|
|
3005
3318
|
}
|
|
3006
3319
|
resume() {
|
|
3007
3320
|
if (!this.isTracking || this.pauseStartTime === null) {
|
|
3008
3321
|
return;
|
|
3009
3322
|
}
|
|
3010
|
-
const pausedDuration =
|
|
3323
|
+
const pausedDuration = this.getCurrentTimestamp() - this.pauseStartTime;
|
|
3011
3324
|
this.totalPausedTime += pausedDuration;
|
|
3012
3325
|
this.pauseStartTime = null;
|
|
3013
3326
|
}
|
|
@@ -3067,13 +3380,13 @@ class TabVisibilityTracker {
|
|
|
3067
3380
|
if (this.pauseStartTime !== null) {
|
|
3068
3381
|
return;
|
|
3069
3382
|
}
|
|
3070
|
-
this.currentIntervalStart =
|
|
3383
|
+
this.currentIntervalStart = this.getCurrentTimestamp();
|
|
3071
3384
|
}
|
|
3072
3385
|
endCurrentIntervalIfActive() {
|
|
3073
3386
|
if (this.currentIntervalStart === null) {
|
|
3074
3387
|
return;
|
|
3075
3388
|
}
|
|
3076
|
-
const endTime =
|
|
3389
|
+
const endTime = this.getCurrentTimestamp();
|
|
3077
3390
|
const startTime = this.currentIntervalStart;
|
|
3078
3391
|
if (endTime > startTime) {
|
|
3079
3392
|
this.intervals.push({
|
|
@@ -3085,7 +3398,7 @@ class TabVisibilityTracker {
|
|
|
3085
3398
|
}
|
|
3086
3399
|
normalizeTimestamp(absoluteTime) {
|
|
3087
3400
|
const elapsed = absoluteTime - this.recordingStartTime;
|
|
3088
|
-
const normalized = (elapsed - this.totalPausedTime) /
|
|
3401
|
+
const normalized = (elapsed - this.totalPausedTime) / MILLISECONDS_PER_SECOND;
|
|
3089
3402
|
return Math.max(0, normalized);
|
|
3090
3403
|
}
|
|
3091
3404
|
}
|
|
@@ -3093,7 +3406,12 @@ class TabVisibilityTracker {
|
|
|
3093
3406
|
// src/core/stream/stream-recording-state.ts
|
|
3094
3407
|
var TIMER_INTERVAL = 1000;
|
|
3095
3408
|
var SECONDS_PER_MINUTE2 = 60;
|
|
3096
|
-
var
|
|
3409
|
+
var MILLISECONDS_PER_SECOND2 = 1000;
|
|
3410
|
+
var DEFAULT_TAB_VISIBILITY_OVERLAY_TEXT = "User in another tab";
|
|
3411
|
+
var DEFAULT_STREAM_RECORDING_STATE_DEPENDENCIES = {
|
|
3412
|
+
checkRecorderSupport,
|
|
3413
|
+
getCurrentTimestamp: () => performance.now()
|
|
3414
|
+
};
|
|
3097
3415
|
|
|
3098
3416
|
class StreamRecordingState {
|
|
3099
3417
|
recordingStartTime = 0;
|
|
@@ -3107,8 +3425,27 @@ class StreamRecordingState {
|
|
|
3107
3425
|
blurHandler = null;
|
|
3108
3426
|
focusHandler = null;
|
|
3109
3427
|
streamManager;
|
|
3110
|
-
|
|
3428
|
+
dependencies;
|
|
3429
|
+
constructor(streamManager, dependencies) {
|
|
3111
3430
|
this.streamManager = streamManager;
|
|
3431
|
+
const checkRecorderSupportDependency = dependencies?.checkRecorderSupport;
|
|
3432
|
+
let resolvedCheckRecorderSupportDependency;
|
|
3433
|
+
if (checkRecorderSupportDependency === undefined) {
|
|
3434
|
+
resolvedCheckRecorderSupportDependency = DEFAULT_STREAM_RECORDING_STATE_DEPENDENCIES.checkRecorderSupport;
|
|
3435
|
+
} else {
|
|
3436
|
+
resolvedCheckRecorderSupportDependency = checkRecorderSupportDependency;
|
|
3437
|
+
}
|
|
3438
|
+
const getCurrentTimestampDependency = dependencies?.getCurrentTimestamp;
|
|
3439
|
+
let resolvedGetCurrentTimestampDependency;
|
|
3440
|
+
if (getCurrentTimestampDependency === undefined) {
|
|
3441
|
+
resolvedGetCurrentTimestampDependency = DEFAULT_STREAM_RECORDING_STATE_DEPENDENCIES.getCurrentTimestamp;
|
|
3442
|
+
} else {
|
|
3443
|
+
resolvedGetCurrentTimestampDependency = getCurrentTimestampDependency;
|
|
3444
|
+
}
|
|
3445
|
+
this.dependencies = {
|
|
3446
|
+
checkRecorderSupport: resolvedCheckRecorderSupportDependency,
|
|
3447
|
+
getCurrentTimestamp: resolvedGetCurrentTimestampDependency
|
|
3448
|
+
};
|
|
3112
3449
|
}
|
|
3113
3450
|
isRecording() {
|
|
3114
3451
|
return this.streamManager.getState() === "recording";
|
|
@@ -3127,11 +3464,15 @@ class StreamRecordingState {
|
|
|
3127
3464
|
}
|
|
3128
3465
|
async startRecording(processor, config, enableTabVisibilityOverlay, tabVisibilityOverlayText) {
|
|
3129
3466
|
const mediaStream = this.streamManager.getStream();
|
|
3467
|
+
let audioTrackCount = 0;
|
|
3468
|
+
if (mediaStream !== null) {
|
|
3469
|
+
audioTrackCount = mediaStream.getAudioTracks().length;
|
|
3470
|
+
}
|
|
3130
3471
|
logger.debug("[StreamRecordingState] startRecording called", {
|
|
3131
3472
|
hasMediaStream: !!mediaStream,
|
|
3132
3473
|
isRecording: this.isRecording(),
|
|
3133
3474
|
hasProcessor: !!processor,
|
|
3134
|
-
audioTracks:
|
|
3475
|
+
audioTracks: audioTrackCount
|
|
3135
3476
|
});
|
|
3136
3477
|
if (!mediaStream) {
|
|
3137
3478
|
throw new Error("Stream must be started before recording");
|
|
@@ -3142,7 +3483,7 @@ class StreamRecordingState {
|
|
|
3142
3483
|
}
|
|
3143
3484
|
const hasAudioTracks = mediaStream.getAudioTracks().length > 0;
|
|
3144
3485
|
const requiresWatermark = config.watermark !== undefined;
|
|
3145
|
-
const supportReport = await checkRecorderSupport({
|
|
3486
|
+
const supportReport = await this.dependencies.checkRecorderSupport({
|
|
3146
3487
|
requiresAudio: hasAudioTracks,
|
|
3147
3488
|
requiresWatermark
|
|
3148
3489
|
});
|
|
@@ -3170,11 +3511,15 @@ class StreamRecordingState {
|
|
|
3170
3511
|
this.streamManager.emit("recordingbufferupdate", { size, formatted });
|
|
3171
3512
|
}, TIMER_INTERVAL);
|
|
3172
3513
|
this.resetRecordingState();
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3514
|
+
let overlayConfig;
|
|
3515
|
+
if (enableTabVisibilityOverlay === true) {
|
|
3516
|
+
const resolvedOverlayText = this.resolveTabVisibilityOverlayText(tabVisibilityOverlayText);
|
|
3517
|
+
overlayConfig = {
|
|
3518
|
+
enabled: true,
|
|
3519
|
+
text: resolvedOverlayText,
|
|
3520
|
+
recordingStartTime: this.recordingStartTime
|
|
3521
|
+
};
|
|
3522
|
+
}
|
|
3178
3523
|
logger.debug("[StreamRecordingState] Overlay config", {
|
|
3179
3524
|
enableTabVisibilityOverlay,
|
|
3180
3525
|
hasOverlayText: !!tabVisibilityOverlayText,
|
|
@@ -3197,7 +3542,7 @@ class StreamRecordingState {
|
|
|
3197
3542
|
this.startRecordingTimer();
|
|
3198
3543
|
}
|
|
3199
3544
|
async stopRecording() {
|
|
3200
|
-
const recordingElapsedSeconds = (
|
|
3545
|
+
const recordingElapsedSeconds = (this.dependencies.getCurrentTimestamp() - this.recordingStartTime - this.totalPausedTime) / MILLISECONDS_PER_SECOND2;
|
|
3201
3546
|
logger.debug("[StreamRecordingState] stopRecording called", {
|
|
3202
3547
|
hasStreamProcessor: !!this.streamProcessor,
|
|
3203
3548
|
isRecording: this.isRecording(),
|
|
@@ -3244,7 +3589,7 @@ class StreamRecordingState {
|
|
|
3244
3589
|
pauseRecording() {
|
|
3245
3590
|
this.clearRecordingTimer();
|
|
3246
3591
|
if (this.pauseStartTime === null) {
|
|
3247
|
-
this.pauseStartTime =
|
|
3592
|
+
this.pauseStartTime = this.dependencies.getCurrentTimestamp();
|
|
3248
3593
|
}
|
|
3249
3594
|
if (this.tabVisibilityTracker) {
|
|
3250
3595
|
this.tabVisibilityTracker.pause();
|
|
@@ -3255,7 +3600,7 @@ class StreamRecordingState {
|
|
|
3255
3600
|
}
|
|
3256
3601
|
resumeRecording() {
|
|
3257
3602
|
if (this.pauseStartTime !== null) {
|
|
3258
|
-
const pausedDuration =
|
|
3603
|
+
const pausedDuration = this.dependencies.getCurrentTimestamp() - this.pauseStartTime;
|
|
3259
3604
|
this.totalPausedTime += pausedDuration;
|
|
3260
3605
|
this.pauseStartTime = null;
|
|
3261
3606
|
}
|
|
@@ -3326,7 +3671,7 @@ class StreamRecordingState {
|
|
|
3326
3671
|
}
|
|
3327
3672
|
startRecordingTimer() {
|
|
3328
3673
|
this.recordingTimer = window.setInterval(() => {
|
|
3329
|
-
const elapsed = (
|
|
3674
|
+
const elapsed = (this.dependencies.getCurrentTimestamp() - this.recordingStartTime - this.totalPausedTime) / MILLISECONDS_PER_SECOND2;
|
|
3330
3675
|
const formatted = this.formatTimeElapsed(elapsed);
|
|
3331
3676
|
this.streamManager.emit("recordingtimeupdate", { elapsed, formatted });
|
|
3332
3677
|
}, TIMER_INTERVAL);
|
|
@@ -3344,7 +3689,7 @@ class StreamRecordingState {
|
|
|
3344
3689
|
}
|
|
3345
3690
|
}
|
|
3346
3691
|
resetRecordingState() {
|
|
3347
|
-
this.recordingStartTime =
|
|
3692
|
+
this.recordingStartTime = this.dependencies.getCurrentTimestamp();
|
|
3348
3693
|
this.totalPausedTime = 0;
|
|
3349
3694
|
this.pauseStartTime = null;
|
|
3350
3695
|
}
|
|
@@ -3352,6 +3697,16 @@ class StreamRecordingState {
|
|
|
3352
3697
|
this.totalPausedTime = 0;
|
|
3353
3698
|
this.pauseStartTime = null;
|
|
3354
3699
|
}
|
|
3700
|
+
resolveTabVisibilityOverlayText(tabVisibilityOverlayText) {
|
|
3701
|
+
if (tabVisibilityOverlayText === undefined) {
|
|
3702
|
+
return DEFAULT_TAB_VISIBILITY_OVERLAY_TEXT;
|
|
3703
|
+
}
|
|
3704
|
+
const trimmedText = tabVisibilityOverlayText.trim();
|
|
3705
|
+
if (trimmedText.length === 0) {
|
|
3706
|
+
return DEFAULT_TAB_VISIBILITY_OVERLAY_TEXT;
|
|
3707
|
+
}
|
|
3708
|
+
return tabVisibilityOverlayText;
|
|
3709
|
+
}
|
|
3355
3710
|
setupVisibilityUpdates(processor) {
|
|
3356
3711
|
if (typeof document === "undefined" || typeof window === "undefined") {
|
|
3357
3712
|
logger.warn("[StreamRecordingState] Cannot setup visibility updates - document/window not available");
|
|
@@ -3362,7 +3717,7 @@ class StreamRecordingState {
|
|
|
3362
3717
|
return;
|
|
3363
3718
|
}
|
|
3364
3719
|
const isHidden = document.visibilityState === "hidden";
|
|
3365
|
-
const timestamp =
|
|
3720
|
+
const timestamp = this.dependencies.getCurrentTimestamp();
|
|
3366
3721
|
logger.debug("[StreamRecordingState] Visibility change", {
|
|
3367
3722
|
isHidden,
|
|
3368
3723
|
timestamp,
|
|
@@ -3371,12 +3726,12 @@ class StreamRecordingState {
|
|
|
3371
3726
|
processor.updateTabVisibility(isHidden, timestamp);
|
|
3372
3727
|
};
|
|
3373
3728
|
this.blurHandler = () => {
|
|
3374
|
-
const timestamp =
|
|
3729
|
+
const timestamp = this.dependencies.getCurrentTimestamp();
|
|
3375
3730
|
logger.debug("[StreamRecordingState] Window blur", { timestamp });
|
|
3376
3731
|
processor.updateTabVisibility(true, timestamp);
|
|
3377
3732
|
};
|
|
3378
3733
|
this.focusHandler = () => {
|
|
3379
|
-
const timestamp =
|
|
3734
|
+
const timestamp = this.dependencies.getCurrentTimestamp();
|
|
3380
3735
|
logger.debug("[StreamRecordingState] Window focus", { timestamp });
|
|
3381
3736
|
processor.updateTabVisibility(false, timestamp);
|
|
3382
3737
|
};
|
|
@@ -3385,7 +3740,7 @@ class StreamRecordingState {
|
|
|
3385
3740
|
window.addEventListener("focus", this.focusHandler);
|
|
3386
3741
|
const initialHidden = document.visibilityState === "hidden";
|
|
3387
3742
|
if (initialHidden) {
|
|
3388
|
-
const timestamp =
|
|
3743
|
+
const timestamp = this.dependencies.getCurrentTimestamp();
|
|
3389
3744
|
logger.debug("[StreamRecordingState] Initial state is hidden", {
|
|
3390
3745
|
timestamp
|
|
3391
3746
|
});
|
|
@@ -3523,7 +3878,7 @@ class CameraStreamManager {
|
|
|
3523
3878
|
// package.json
|
|
3524
3879
|
var package_default = {
|
|
3525
3880
|
name: "@vidtreo/recorder",
|
|
3526
|
-
version: "1.
|
|
3881
|
+
version: "1.3.0",
|
|
3527
3882
|
type: "module",
|
|
3528
3883
|
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.",
|
|
3529
3884
|
main: "./dist/index.js",
|
|
@@ -13844,6 +14199,8 @@ var WORKER_AUDIO_SAMPLE_FORMAT_F32_PLANAR = "f32-planar";
|
|
|
13844
14199
|
var MILLISECONDS_PER_SECOND3 = 1000;
|
|
13845
14200
|
var OVERLAY_LOG_FRAME_INTERVAL = 90;
|
|
13846
14201
|
var RECORDER_WORKER_LOG_PREFIX3 = "[RecorderWorker]";
|
|
14202
|
+
var INITIAL_INTERVAL_INDEX = 0;
|
|
14203
|
+
var INITIAL_LAST_OVERLAY_TIMESTAMP = -1;
|
|
13847
14204
|
|
|
13848
14205
|
class VisibilityTracker {
|
|
13849
14206
|
hiddenIntervals = [];
|
|
@@ -13851,6 +14208,8 @@ class VisibilityTracker {
|
|
|
13851
14208
|
pendingVisibilityUpdates = [];
|
|
13852
14209
|
recordingStartTime = 0;
|
|
13853
14210
|
isScreenCapture = false;
|
|
14211
|
+
intervalCursor = INITIAL_INTERVAL_INDEX;
|
|
14212
|
+
lastOverlayTimestamp = INITIAL_LAST_OVERLAY_TIMESTAMP;
|
|
13854
14213
|
logger;
|
|
13855
14214
|
constructor(dependencies) {
|
|
13856
14215
|
this.logger = dependencies.logger;
|
|
@@ -13861,6 +14220,8 @@ class VisibilityTracker {
|
|
|
13861
14220
|
this.pendingVisibilityUpdates = [];
|
|
13862
14221
|
this.recordingStartTime = recordingStartTime;
|
|
13863
14222
|
this.isScreenCapture = isScreenCapture;
|
|
14223
|
+
this.intervalCursor = INITIAL_INTERVAL_INDEX;
|
|
14224
|
+
this.lastOverlayTimestamp = INITIAL_LAST_OVERLAY_TIMESTAMP;
|
|
13864
14225
|
}
|
|
13865
14226
|
setRecordingStartTime(recordingStartTime) {
|
|
13866
14227
|
this.recordingStartTime = recordingStartTime;
|
|
@@ -13871,32 +14232,52 @@ class VisibilityTracker {
|
|
|
13871
14232
|
getPendingUpdatesCount() {
|
|
13872
14233
|
return this.pendingVisibilityUpdates.length;
|
|
13873
14234
|
}
|
|
13874
|
-
shouldApplyOverlay(
|
|
13875
|
-
if (!parameters.overlayEnabled) {
|
|
13876
|
-
return false;
|
|
13877
|
-
}
|
|
14235
|
+
shouldApplyOverlay(timestamp, frameCount) {
|
|
13878
14236
|
if (this.isScreenCapture) {
|
|
13879
14237
|
return false;
|
|
13880
14238
|
}
|
|
13881
|
-
const
|
|
13882
|
-
const currentIntervalMatch = this.currentHiddenIntervalStart !== null && parameters.timestamp >= this.currentHiddenIntervalStart;
|
|
14239
|
+
const shouldApplyCurrentInterval = this.currentHiddenIntervalStart !== null && timestamp >= this.currentHiddenIntervalStart;
|
|
13883
14240
|
let shouldApply = false;
|
|
13884
|
-
if (
|
|
14241
|
+
if (shouldApplyCurrentInterval) {
|
|
13885
14242
|
shouldApply = true;
|
|
13886
14243
|
}
|
|
13887
|
-
if (
|
|
13888
|
-
shouldApply =
|
|
14244
|
+
if (!shouldApplyCurrentInterval) {
|
|
14245
|
+
shouldApply = this.shouldApplyCompletedIntervalOverlay(timestamp);
|
|
13889
14246
|
}
|
|
13890
|
-
if (
|
|
14247
|
+
if (frameCount % OVERLAY_LOG_FRAME_INTERVAL === 0) {
|
|
13891
14248
|
this.logger.debug(\`\${RECORDER_WORKER_LOG_PREFIX3} Overlay check\`, {
|
|
13892
|
-
timestamp
|
|
14249
|
+
timestamp,
|
|
13893
14250
|
shouldApply,
|
|
13894
|
-
frameCount
|
|
13895
|
-
intervalsCount: this.hiddenIntervals.length
|
|
14251
|
+
frameCount,
|
|
14252
|
+
intervalsCount: this.hiddenIntervals.length,
|
|
14253
|
+
intervalCursor: this.intervalCursor
|
|
13896
14254
|
});
|
|
13897
14255
|
}
|
|
13898
14256
|
return shouldApply;
|
|
13899
14257
|
}
|
|
14258
|
+
shouldApplyCompletedIntervalOverlay(timestamp) {
|
|
14259
|
+
if (this.hiddenIntervals.length === 0) {
|
|
14260
|
+
return false;
|
|
14261
|
+
}
|
|
14262
|
+
if (this.lastOverlayTimestamp !== INITIAL_LAST_OVERLAY_TIMESTAMP && timestamp < this.lastOverlayTimestamp) {
|
|
14263
|
+
this.intervalCursor = INITIAL_INTERVAL_INDEX;
|
|
14264
|
+
}
|
|
14265
|
+
this.lastOverlayTimestamp = timestamp;
|
|
14266
|
+
let activeCursor = this.intervalCursor;
|
|
14267
|
+
while (activeCursor < this.hiddenIntervals.length) {
|
|
14268
|
+
const interval = this.hiddenIntervals[activeCursor];
|
|
14269
|
+
if (timestamp < interval.start) {
|
|
14270
|
+
break;
|
|
14271
|
+
}
|
|
14272
|
+
if (timestamp <= interval.end) {
|
|
14273
|
+
this.intervalCursor = activeCursor;
|
|
14274
|
+
return true;
|
|
14275
|
+
}
|
|
14276
|
+
activeCursor += 1;
|
|
14277
|
+
}
|
|
14278
|
+
this.intervalCursor = activeCursor;
|
|
14279
|
+
return false;
|
|
14280
|
+
}
|
|
13900
14281
|
handleUpdateVisibility(isHidden, timestamp, hasBaseVideoTimestamp, pausedDuration) {
|
|
13901
14282
|
if (!hasBaseVideoTimestamp) {
|
|
13902
14283
|
this.pendingVisibilityUpdates = [
|
|
@@ -13962,6 +14343,9 @@ var STEREO_CHANNEL_COUNT = 2;
|
|
|
13962
14343
|
var AUDIO_SAMPLE_AVERAGE_SCALE = 0.5;
|
|
13963
14344
|
var STOP_PENDING_WRITES_TIMEOUT_MILLISECONDS2 = 500;
|
|
13964
14345
|
var MP4_FAST_START_DISABLED = false;
|
|
14346
|
+
var VIDEO_LATENCY_MODE_REALTIME = "realtime";
|
|
14347
|
+
var VIDEO_CONTENT_HINT_MOTION = "motion";
|
|
14348
|
+
var VIDEO_HARDWARE_ACCELERATION_PREFERENCE = "prefer-hardware";
|
|
13965
14349
|
|
|
13966
14350
|
class RecorderWorker {
|
|
13967
14351
|
output = null;
|
|
@@ -14230,9 +14614,9 @@ class RecorderWorker {
|
|
|
14230
14614
|
sizeChangeBehavior: "contain",
|
|
14231
14615
|
alpha: "discard",
|
|
14232
14616
|
bitrateMode: "variable",
|
|
14233
|
-
latencyMode:
|
|
14234
|
-
contentHint:
|
|
14235
|
-
hardwareAcceleration:
|
|
14617
|
+
latencyMode: VIDEO_LATENCY_MODE_REALTIME,
|
|
14618
|
+
contentHint: VIDEO_CONTENT_HINT_MOTION,
|
|
14619
|
+
hardwareAcceleration: VIDEO_HARDWARE_ACCELERATION_PREFERENCE,
|
|
14236
14620
|
keyFrameInterval: keyFrameIntervalSeconds,
|
|
14237
14621
|
bitrate: this.deserializeBitrate(config.bitrate)
|
|
14238
14622
|
};
|
|
@@ -14413,18 +14797,14 @@ class RecorderWorker {
|
|
|
14413
14797
|
},
|
|
14414
14798
|
isScreenCapture: this.isScreenCapture
|
|
14415
14799
|
});
|
|
14416
|
-
|
|
14417
|
-
|
|
14418
|
-
|
|
14419
|
-
|
|
14420
|
-
|
|
14421
|
-
timestamp: frameTimestamp,
|
|
14422
|
-
overlayEnabled,
|
|
14423
|
-
frameCount: this.timestampManager.getFrameCount()
|
|
14424
|
-
});
|
|
14800
|
+
const overlayConfig = this.overlayConfig;
|
|
14801
|
+
let shouldApplyOverlay = false;
|
|
14802
|
+
if (overlayConfig?.enabled && !this.isScreenCapture) {
|
|
14803
|
+
shouldApplyOverlay = this.visibilityTracker.shouldApplyOverlay(frameTimestamp, this.timestampManager.getFrameCount());
|
|
14804
|
+
}
|
|
14425
14805
|
const compositionResult = this.frameCompositor.composeFrame({
|
|
14426
14806
|
videoFrame,
|
|
14427
|
-
overlayConfig
|
|
14807
|
+
overlayConfig,
|
|
14428
14808
|
shouldApplyOverlay,
|
|
14429
14809
|
config
|
|
14430
14810
|
});
|
|
@@ -15001,9 +15381,50 @@ var KEY_FRAME_INTERVAL_SECONDS = 5;
|
|
|
15001
15381
|
var DEFAULT_SWITCH_SOURCE_FPS = 30;
|
|
15002
15382
|
var WORKER_PROBE_TIMEOUT_MILLISECONDS = 2000;
|
|
15003
15383
|
var FINALIZE_TIMEOUT_MILLISECONDS = 30000;
|
|
15004
|
-
var
|
|
15384
|
+
var MILLISECONDS_PER_SECOND3 = 1000;
|
|
15005
15385
|
var DEFAULT_RECORDING_FORMAT = "mp4";
|
|
15006
|
-
var
|
|
15386
|
+
var CODEC_CACHE_MAX_ENTRIES = 50;
|
|
15387
|
+
var CODEC_CACHE_KEY_SEPARATOR = "|";
|
|
15388
|
+
var CODEC_CACHE_ARRAY_SEPARATOR = ",";
|
|
15389
|
+
var CODEC_CACHE_UNDEFINED = "undefined";
|
|
15390
|
+
var CODEC_CACHE_FORMAT = "format";
|
|
15391
|
+
var CODEC_CACHE_AUDIO_OVERRIDE = "audioOverride";
|
|
15392
|
+
var CODEC_CACHE_VIDEO_OVERRIDE = "videoOverride";
|
|
15393
|
+
var CODEC_CACHE_AUDIO_BITRATE = "audioBitrate";
|
|
15394
|
+
var CODEC_CACHE_VIDEO_BITRATE = "videoBitrate";
|
|
15395
|
+
var CODEC_CACHE_WIDTH = "width";
|
|
15396
|
+
var CODEC_CACHE_HEIGHT = "height";
|
|
15397
|
+
var CODEC_CACHE_POLICY_PREFERRED_AUDIO = "policyPreferredAudio";
|
|
15398
|
+
var CODEC_CACHE_POLICY_PREFERRED_VIDEO = "policyPreferredVideo";
|
|
15399
|
+
var CODEC_CACHE_POLICY_AUDIO_FALLBACK = "policyAudioFallback";
|
|
15400
|
+
var CODEC_CACHE_POLICY_VIDEO_FALLBACK = "policyVideoFallback";
|
|
15401
|
+
var resolvedAudioCodecCache = new Map;
|
|
15402
|
+
var resolvedVideoCodecCache = new Map;
|
|
15403
|
+
function formatCacheValue(value) {
|
|
15404
|
+
if (value === undefined) {
|
|
15405
|
+
return CODEC_CACHE_UNDEFINED;
|
|
15406
|
+
}
|
|
15407
|
+
return String(value);
|
|
15408
|
+
}
|
|
15409
|
+
function formatCacheArray(values) {
|
|
15410
|
+
return values.join(CODEC_CACHE_ARRAY_SEPARATOR);
|
|
15411
|
+
}
|
|
15412
|
+
function buildCacheEntry(segment, value) {
|
|
15413
|
+
return `${segment}=${value}`;
|
|
15414
|
+
}
|
|
15415
|
+
function setCodecCacheValue(cache, cacheKey, value) {
|
|
15416
|
+
if (cache.has(cacheKey)) {
|
|
15417
|
+
cache.delete(cacheKey);
|
|
15418
|
+
}
|
|
15419
|
+
cache.set(cacheKey, value);
|
|
15420
|
+
if (cache.size <= CODEC_CACHE_MAX_ENTRIES) {
|
|
15421
|
+
return;
|
|
15422
|
+
}
|
|
15423
|
+
const oldestEntry = cache.keys().next();
|
|
15424
|
+
if (!oldestEntry.done) {
|
|
15425
|
+
cache.delete(oldestEntry.value);
|
|
15426
|
+
}
|
|
15427
|
+
}
|
|
15007
15428
|
|
|
15008
15429
|
class WorkerProcessor {
|
|
15009
15430
|
worker = null;
|
|
@@ -15024,6 +15445,7 @@ class WorkerProcessor {
|
|
|
15024
15445
|
workerProbeManager;
|
|
15025
15446
|
canUseMainThreadVideoProcessorFn;
|
|
15026
15447
|
createVideoStreamFromTrackFn;
|
|
15448
|
+
isLinuxPlatformFn;
|
|
15027
15449
|
constructor(dependencies = {}) {
|
|
15028
15450
|
let createWorkerFn = (workerUrl) => new Worker(workerUrl, { type: "classic" });
|
|
15029
15451
|
if (dependencies.createWorker) {
|
|
@@ -15043,8 +15465,13 @@ class WorkerProcessor {
|
|
|
15043
15465
|
if (dependencies.createVideoStreamFromTrack) {
|
|
15044
15466
|
createVideoStreamFromTrackFn = dependencies.createVideoStreamFromTrack;
|
|
15045
15467
|
}
|
|
15468
|
+
let isLinuxPlatformFn = () => isLinuxPlatform();
|
|
15469
|
+
if (dependencies.isLinuxPlatform) {
|
|
15470
|
+
isLinuxPlatformFn = dependencies.isLinuxPlatform;
|
|
15471
|
+
}
|
|
15046
15472
|
this.canUseMainThreadVideoProcessorFn = canUseMainThreadVideoProcessorFn;
|
|
15047
15473
|
this.createVideoStreamFromTrackFn = createVideoStreamFromTrackFn;
|
|
15474
|
+
this.isLinuxPlatformFn = isLinuxPlatformFn;
|
|
15048
15475
|
const hasWorkerFactory = !!dependencies.createWorker;
|
|
15049
15476
|
this.workerProbeManager = new WorkerProbeManager({
|
|
15050
15477
|
setTimeout: window.setTimeout.bind(window),
|
|
@@ -15171,8 +15598,12 @@ class WorkerProcessor {
|
|
|
15171
15598
|
this.resetProcessingState(overlayConfig);
|
|
15172
15599
|
this.stopAudioWorklet();
|
|
15173
15600
|
const format = this.resolveRecordingFormat(config);
|
|
15174
|
-
const
|
|
15175
|
-
|
|
15601
|
+
const policy = getFormatCompatibilityPolicy(format, {
|
|
15602
|
+
isLinuxPlatform: this.isLinuxPlatformFn()
|
|
15603
|
+
});
|
|
15604
|
+
const audioBitrate = this.resolveAudioBitrate(config, format);
|
|
15605
|
+
const audioCodec = await this.resolveAudioCodecWithCache(config, format, policy, audioBitrate);
|
|
15606
|
+
const codec = await this.resolveVideoCodecWithCache(config, format, policy);
|
|
15176
15607
|
const isScreenCapture = isScreenCaptureStream(stream);
|
|
15177
15608
|
logger.debug("[WorkerProcessor] Starting processing", {
|
|
15178
15609
|
isScreenCapture,
|
|
@@ -15180,7 +15611,7 @@ class WorkerProcessor {
|
|
|
15180
15611
|
codec,
|
|
15181
15612
|
bitrate: config.bitrate
|
|
15182
15613
|
});
|
|
15183
|
-
const workerConfig = this.buildWorkerTranscodeConfig(config, audioCodec, codec, format);
|
|
15614
|
+
const workerConfig = this.buildWorkerTranscodeConfig(config, audioCodec, audioBitrate, codec, format);
|
|
15184
15615
|
const videoTracks = stream.getVideoTracks();
|
|
15185
15616
|
const audioTracks = stream.getAudioTracks();
|
|
15186
15617
|
logger.debug("[WorkerProcessor] Preparing to start processing", {
|
|
@@ -15256,27 +15687,88 @@ class WorkerProcessor {
|
|
|
15256
15687
|
}
|
|
15257
15688
|
return format;
|
|
15258
15689
|
}
|
|
15259
|
-
|
|
15260
|
-
|
|
15261
|
-
|
|
15262
|
-
audioCodec = await detectBestAudioCodec(config.audioBitrate);
|
|
15690
|
+
resolveAudioBitrate(config, format) {
|
|
15691
|
+
if (config.audioBitrate !== undefined) {
|
|
15692
|
+
return config.audioBitrate;
|
|
15263
15693
|
}
|
|
15264
|
-
return
|
|
15694
|
+
return getPresetAudioBitrateForFormat(format);
|
|
15265
15695
|
}
|
|
15266
|
-
async
|
|
15267
|
-
|
|
15268
|
-
|
|
15269
|
-
|
|
15270
|
-
|
|
15696
|
+
async resolveAudioCodec(config, format, policy, audioBitrate) {
|
|
15697
|
+
return await resolveAudioCodecFromPolicy({
|
|
15698
|
+
format,
|
|
15699
|
+
overrideCodec: config.audioCodec,
|
|
15700
|
+
policy,
|
|
15701
|
+
bitrate: audioBitrate
|
|
15702
|
+
});
|
|
15271
15703
|
}
|
|
15272
|
-
|
|
15704
|
+
async resolveVideoCodec(config, format, policy) {
|
|
15705
|
+
return await resolveVideoCodecFromPolicy({
|
|
15706
|
+
format,
|
|
15707
|
+
overrideCodec: config.codec,
|
|
15708
|
+
policy,
|
|
15709
|
+
width: config.width,
|
|
15710
|
+
height: config.height,
|
|
15711
|
+
bitrate: config.bitrate
|
|
15712
|
+
});
|
|
15713
|
+
}
|
|
15714
|
+
async resolveAudioCodecWithCache(config, format, policy, audioBitrate) {
|
|
15715
|
+
const audioCodecCacheKey = this.buildAudioCodecCacheKey(config, format, policy, audioBitrate);
|
|
15716
|
+
const cachedAudioCodec = resolvedAudioCodecCache.get(audioCodecCacheKey);
|
|
15717
|
+
if (cachedAudioCodec) {
|
|
15718
|
+
return cachedAudioCodec;
|
|
15719
|
+
}
|
|
15720
|
+
const resolvedAudioCodec = await this.resolveAudioCodec(config, format, policy, audioBitrate);
|
|
15721
|
+
setCodecCacheValue(resolvedAudioCodecCache, audioCodecCacheKey, resolvedAudioCodec);
|
|
15722
|
+
return resolvedAudioCodec;
|
|
15723
|
+
}
|
|
15724
|
+
async resolveVideoCodecWithCache(config, format, policy) {
|
|
15725
|
+
const videoCodecCacheKey = this.buildVideoCodecCacheKey(config, format, policy);
|
|
15726
|
+
const cachedVideoCodec = resolvedVideoCodecCache.get(videoCodecCacheKey);
|
|
15727
|
+
if (cachedVideoCodec) {
|
|
15728
|
+
return cachedVideoCodec;
|
|
15729
|
+
}
|
|
15730
|
+
const resolvedVideoCodec = await this.resolveVideoCodec(config, format, policy);
|
|
15731
|
+
setCodecCacheValue(resolvedVideoCodecCache, videoCodecCacheKey, resolvedVideoCodec);
|
|
15732
|
+
return resolvedVideoCodec;
|
|
15733
|
+
}
|
|
15734
|
+
buildAudioCodecCacheKey(config, format, policy, audioBitrate) {
|
|
15735
|
+
const audioOverride = formatCacheValue(config.audioCodec);
|
|
15736
|
+
const preferredAudioCodec = formatCacheValue(policy.preferredAudioCodec);
|
|
15737
|
+
const audioFallbackOrder = formatCacheArray(policy.audioCodecFallbackOrder);
|
|
15738
|
+
const audioBitrateValue = formatCacheValue(audioBitrate);
|
|
15739
|
+
return [
|
|
15740
|
+
buildCacheEntry(CODEC_CACHE_FORMAT, formatCacheValue(format)),
|
|
15741
|
+
buildCacheEntry(CODEC_CACHE_AUDIO_OVERRIDE, audioOverride),
|
|
15742
|
+
buildCacheEntry(CODEC_CACHE_AUDIO_BITRATE, audioBitrateValue),
|
|
15743
|
+
buildCacheEntry(CODEC_CACHE_POLICY_PREFERRED_AUDIO, preferredAudioCodec),
|
|
15744
|
+
buildCacheEntry(CODEC_CACHE_POLICY_AUDIO_FALLBACK, audioFallbackOrder)
|
|
15745
|
+
].join(CODEC_CACHE_KEY_SEPARATOR);
|
|
15746
|
+
}
|
|
15747
|
+
buildVideoCodecCacheKey(config, format, policy) {
|
|
15748
|
+
const videoOverride = formatCacheValue(config.codec);
|
|
15749
|
+
const widthValue = formatCacheValue(config.width);
|
|
15750
|
+
const heightValue = formatCacheValue(config.height);
|
|
15751
|
+
const bitrateValue = formatCacheValue(config.bitrate);
|
|
15752
|
+
const preferredVideoCodec = formatCacheValue(policy.preferredVideoCodec);
|
|
15753
|
+
const videoFallbackOrder = formatCacheArray(policy.videoCodecFallbackOrder);
|
|
15754
|
+
return [
|
|
15755
|
+
buildCacheEntry(CODEC_CACHE_FORMAT, formatCacheValue(format)),
|
|
15756
|
+
buildCacheEntry(CODEC_CACHE_VIDEO_OVERRIDE, videoOverride),
|
|
15757
|
+
buildCacheEntry(CODEC_CACHE_WIDTH, widthValue),
|
|
15758
|
+
buildCacheEntry(CODEC_CACHE_HEIGHT, heightValue),
|
|
15759
|
+
buildCacheEntry(CODEC_CACHE_VIDEO_BITRATE, bitrateValue),
|
|
15760
|
+
buildCacheEntry(CODEC_CACHE_POLICY_PREFERRED_VIDEO, preferredVideoCodec),
|
|
15761
|
+
buildCacheEntry(CODEC_CACHE_POLICY_VIDEO_FALLBACK, videoFallbackOrder)
|
|
15762
|
+
].join(CODEC_CACHE_KEY_SEPARATOR);
|
|
15763
|
+
}
|
|
15764
|
+
buildWorkerTranscodeConfig(config, audioCodec, audioBitrate, codec, format) {
|
|
15273
15765
|
return {
|
|
15274
15766
|
width: config.width,
|
|
15275
15767
|
height: config.height,
|
|
15276
15768
|
fps: config.fps,
|
|
15277
15769
|
bitrate: serializeBitrate(config.bitrate),
|
|
15278
15770
|
audioCodec,
|
|
15279
|
-
audioBitrate
|
|
15771
|
+
audioBitrate,
|
|
15280
15772
|
codec,
|
|
15281
15773
|
keyFrameInterval: KEY_FRAME_INTERVAL_SECONDS,
|
|
15282
15774
|
format,
|
|
@@ -15505,7 +15997,7 @@ class WorkerProcessor {
|
|
|
15505
15997
|
}
|
|
15506
15998
|
this.resetFinalizeRuntimeState();
|
|
15507
15999
|
logger.error("[WorkerProcessor] Finalize failed", {
|
|
15508
|
-
elapsedSeconds: (performance.now() - finalizeStartedAtMilliseconds) /
|
|
16000
|
+
elapsedSeconds: (performance.now() - finalizeStartedAtMilliseconds) / MILLISECONDS_PER_SECOND3,
|
|
15509
16001
|
error: response.error
|
|
15510
16002
|
});
|
|
15511
16003
|
reject(new Error(response.error));
|
|
@@ -15516,7 +16008,7 @@ class WorkerProcessor {
|
|
|
15516
16008
|
return;
|
|
15517
16009
|
}
|
|
15518
16010
|
logger.error("[WorkerProcessor] Finalize timeout reached", {
|
|
15519
|
-
elapsedSeconds: (performance.now() - finalizeStartedAtMilliseconds) /
|
|
16011
|
+
elapsedSeconds: (performance.now() - finalizeStartedAtMilliseconds) / MILLISECONDS_PER_SECOND3
|
|
15520
16012
|
});
|
|
15521
16013
|
this.resetFinalizeRuntimeState();
|
|
15522
16014
|
reject(new Error("Finalize timeout"));
|
|
@@ -15546,7 +16038,7 @@ class WorkerProcessor {
|
|
|
15546
16038
|
}
|
|
15547
16039
|
rejectFinalizeBlobCreationError(reject, error, finalizeStartedAtMilliseconds) {
|
|
15548
16040
|
logger.error("[WorkerProcessor] Finalize failed while creating blob", {
|
|
15549
|
-
elapsedSeconds: (performance.now() - finalizeStartedAtMilliseconds) /
|
|
16041
|
+
elapsedSeconds: (performance.now() - finalizeStartedAtMilliseconds) / MILLISECONDS_PER_SECOND3,
|
|
15550
16042
|
error: extractErrorMessage(error)
|
|
15551
16043
|
});
|
|
15552
16044
|
if (error instanceof Error) {
|
|
@@ -15584,7 +16076,7 @@ class WorkerProcessor {
|
|
|
15584
16076
|
logger.debug("[WorkerProcessor] Sending visibility update to worker", {
|
|
15585
16077
|
isHidden,
|
|
15586
16078
|
timestamp,
|
|
15587
|
-
timestampSeconds: timestamp /
|
|
16079
|
+
timestampSeconds: timestamp / MILLISECONDS_PER_SECOND3
|
|
15588
16080
|
});
|
|
15589
16081
|
const message = {
|
|
15590
16082
|
type: "updateVisibility",
|
|
@@ -15888,7 +16380,7 @@ class StreamProcessor {
|
|
|
15888
16380
|
|
|
15889
16381
|
// src/core/recording/recording-manager.ts
|
|
15890
16382
|
var DEFAULT_COUNTDOWN_DURATION = 5000;
|
|
15891
|
-
var
|
|
16383
|
+
var MILLISECONDS_PER_SECOND4 = 1000;
|
|
15892
16384
|
var COUNTDOWN_UPDATE_INTERVAL = 100;
|
|
15893
16385
|
var RECORDING_TIMER_INTERVAL = 1000;
|
|
15894
16386
|
var RECORDING_STATE_RECORDING = "recording";
|
|
@@ -15955,6 +16447,9 @@ class RecordingManager {
|
|
|
15955
16447
|
getOriginalCameraStream() {
|
|
15956
16448
|
return this.originalCameraStream;
|
|
15957
16449
|
}
|
|
16450
|
+
prewarmStreamProcessor() {
|
|
16451
|
+
this.getOrCreateStreamProcessor();
|
|
16452
|
+
}
|
|
15958
16453
|
async startRecording() {
|
|
15959
16454
|
try {
|
|
15960
16455
|
this.callbacks.onClearUploadStatus();
|
|
@@ -15971,7 +16466,7 @@ class RecordingManager {
|
|
|
15971
16466
|
}
|
|
15972
16467
|
startCountdown() {
|
|
15973
16468
|
this.recordingState = RECORDING_STATE_COUNTDOWN;
|
|
15974
|
-
this.countdownRemaining = Math.ceil(this.countdownDuration /
|
|
16469
|
+
this.countdownRemaining = Math.ceil(this.countdownDuration / MILLISECONDS_PER_SECOND4);
|
|
15975
16470
|
this.countdownStartTime = Date.now();
|
|
15976
16471
|
this.callbacks.onCountdownUpdate(this.recordingState, this.countdownRemaining);
|
|
15977
16472
|
this.countdownIntervalId = window.setInterval(() => {
|
|
@@ -15979,7 +16474,7 @@ class RecordingManager {
|
|
|
15979
16474
|
return;
|
|
15980
16475
|
}
|
|
15981
16476
|
const elapsed = Date.now() - this.countdownStartTime;
|
|
15982
|
-
const remaining = Math.max(0, Math.ceil((this.countdownDuration - elapsed) /
|
|
16477
|
+
const remaining = Math.max(0, Math.ceil((this.countdownDuration - elapsed) / MILLISECONDS_PER_SECOND4));
|
|
15983
16478
|
this.countdownRemaining = remaining;
|
|
15984
16479
|
this.callbacks.onCountdownUpdate(this.recordingState, this.countdownRemaining);
|
|
15985
16480
|
}, COUNTDOWN_UPDATE_INTERVAL);
|
|
@@ -15990,8 +16485,6 @@ class RecordingManager {
|
|
|
15990
16485
|
async doStartRecording() {
|
|
15991
16486
|
logger.debug("[RecordingManager] doStartRecording called");
|
|
15992
16487
|
this.cancelCountdown();
|
|
15993
|
-
this.recordingState = RECORDING_STATE_RECORDING;
|
|
15994
|
-
this.callbacks.onStateChange(this.recordingState);
|
|
15995
16488
|
this.resetRecordingState();
|
|
15996
16489
|
const currentStream = this.streamManager.getStream();
|
|
15997
16490
|
logger.debug("[RecordingManager] Current stream:", {
|
|
@@ -16007,24 +16500,30 @@ class RecordingManager {
|
|
|
16007
16500
|
return;
|
|
16008
16501
|
}
|
|
16009
16502
|
this.originalCameraStream = currentStream;
|
|
16010
|
-
logger.debug("[RecordingManager]
|
|
16011
|
-
|
|
16012
|
-
logger.debug("[RecordingManager] StreamProcessor
|
|
16013
|
-
|
|
16014
|
-
|
|
16015
|
-
|
|
16503
|
+
logger.debug("[RecordingManager] Ensuring stream processor");
|
|
16504
|
+
const streamProcessor = this.getOrCreateStreamProcessor();
|
|
16505
|
+
logger.debug("[RecordingManager] StreamProcessor ready:", !!streamProcessor);
|
|
16506
|
+
let recordingConfig = null;
|
|
16507
|
+
const configError = await this.callbacks.onGetConfig().then((resolvedConfig) => {
|
|
16508
|
+
recordingConfig = resolvedConfig;
|
|
16509
|
+
return null;
|
|
16510
|
+
}).catch((error) => {
|
|
16511
|
+
return error;
|
|
16512
|
+
});
|
|
16513
|
+
if (configError) {
|
|
16514
|
+
this.handleError(configError);
|
|
16016
16515
|
this.recordingState = RECORDING_STATE_IDLE;
|
|
16017
16516
|
this.callbacks.onStateChange(this.recordingState);
|
|
16018
16517
|
return;
|
|
16019
16518
|
}
|
|
16020
|
-
if (!
|
|
16519
|
+
if (!recordingConfig) {
|
|
16021
16520
|
this.handleError(new Error("Failed to get recording config"));
|
|
16022
16521
|
this.recordingState = RECORDING_STATE_IDLE;
|
|
16023
16522
|
this.callbacks.onStateChange(this.recordingState);
|
|
16024
16523
|
return;
|
|
16025
16524
|
}
|
|
16026
16525
|
logger.debug("[RecordingManager] Starting recording with stream manager");
|
|
16027
|
-
const recordingError = await this.streamManager.startRecording(
|
|
16526
|
+
const recordingError = await this.streamManager.startRecording(streamProcessor, recordingConfig, this.enableTabVisibilityOverlay, this.tabVisibilityOverlayText).then(() => {
|
|
16028
16527
|
logger.info("[RecordingManager] Recording started successfully");
|
|
16029
16528
|
return null;
|
|
16030
16529
|
}).catch((error) => {
|
|
@@ -16037,6 +16536,8 @@ class RecordingManager {
|
|
|
16037
16536
|
this.callbacks.onStateChange(this.recordingState);
|
|
16038
16537
|
return;
|
|
16039
16538
|
}
|
|
16539
|
+
this.recordingState = RECORDING_STATE_RECORDING;
|
|
16540
|
+
this.callbacks.onStateChange(this.recordingState);
|
|
16040
16541
|
this.startRecordingTimer();
|
|
16041
16542
|
this.recordingStartTime = Date.now();
|
|
16042
16543
|
if (this.maxRecordingTime && this.maxRecordingTime > 0) {
|
|
@@ -16115,6 +16616,12 @@ class RecordingManager {
|
|
|
16115
16616
|
this.recordingIntervalId = null;
|
|
16116
16617
|
this.clearTimer(this.maxTimeTimer, clearTimeout);
|
|
16117
16618
|
this.maxTimeTimer = null;
|
|
16619
|
+
if (this.streamProcessor) {
|
|
16620
|
+
this.streamProcessor.cancel().catch(() => {
|
|
16621
|
+
return;
|
|
16622
|
+
});
|
|
16623
|
+
this.streamProcessor = null;
|
|
16624
|
+
}
|
|
16118
16625
|
}
|
|
16119
16626
|
resetRecordingState() {
|
|
16120
16627
|
this.isPaused = false;
|
|
@@ -16129,6 +16636,14 @@ class RecordingManager {
|
|
|
16129
16636
|
this.pauseStartTime = null;
|
|
16130
16637
|
this.totalPausedTime = 0;
|
|
16131
16638
|
}
|
|
16639
|
+
getOrCreateStreamProcessor() {
|
|
16640
|
+
if (this.streamProcessor) {
|
|
16641
|
+
return this.streamProcessor;
|
|
16642
|
+
}
|
|
16643
|
+
const streamProcessor = new StreamProcessor;
|
|
16644
|
+
this.streamProcessor = streamProcessor;
|
|
16645
|
+
return streamProcessor;
|
|
16646
|
+
}
|
|
16132
16647
|
updatePausedDuration() {
|
|
16133
16648
|
if (this.pauseStartTime === null) {
|
|
16134
16649
|
throw new Error("Pause start time not set");
|
|
@@ -16259,6 +16774,7 @@ class UploadMetadataManager {
|
|
|
16259
16774
|
|
|
16260
16775
|
// src/core/recording/recorder-controller.ts
|
|
16261
16776
|
var LOGGER_PREFIX = "[RecorderController]";
|
|
16777
|
+
var RECORDING_WARMUP_DELAY_MILLISECONDS = 0;
|
|
16262
16778
|
|
|
16263
16779
|
class RecorderController {
|
|
16264
16780
|
streamManager;
|
|
@@ -16276,8 +16792,10 @@ class RecorderController {
|
|
|
16276
16792
|
uploadQueueManager = null;
|
|
16277
16793
|
isInitialized = false;
|
|
16278
16794
|
isDemo = false;
|
|
16795
|
+
isDestroyed = false;
|
|
16279
16796
|
enableTabVisibilityOverlay = false;
|
|
16280
16797
|
tabVisibilityOverlayText;
|
|
16798
|
+
recordingWarmupTimeoutId = null;
|
|
16281
16799
|
constructor(callbacks = {}) {
|
|
16282
16800
|
this.callbacks = callbacks;
|
|
16283
16801
|
this.streamManager = new CameraStreamManager;
|
|
@@ -16293,7 +16811,7 @@ class RecorderController {
|
|
|
16293
16811
|
this.uploadMetadataManager = new UploadMetadataManager;
|
|
16294
16812
|
const recordingCallbacks = createRecordingCallbacks(callbacks, {
|
|
16295
16813
|
stopAudioTracking: () => this.audioLevelAnalyzer.stopTracking(),
|
|
16296
|
-
getConfig: () => this.configManager.
|
|
16814
|
+
getConfig: () => Promise.resolve(this.configManager.getConfigForRecording())
|
|
16297
16815
|
});
|
|
16298
16816
|
this.recordingManager = new RecordingManager(this.streamManager, recordingCallbacks);
|
|
16299
16817
|
const sourceSwitchCallbacks = createSourceSwitchCallbacks(callbacks, {
|
|
@@ -16358,6 +16876,7 @@ class RecorderController {
|
|
|
16358
16876
|
this.applyRecordingConfig(config);
|
|
16359
16877
|
await this.initializeStorage();
|
|
16360
16878
|
this.isInitialized = true;
|
|
16879
|
+
this.scheduleRecordingWarmup();
|
|
16361
16880
|
}
|
|
16362
16881
|
});
|
|
16363
16882
|
}
|
|
@@ -16369,6 +16888,8 @@ class RecorderController {
|
|
|
16369
16888
|
action: async () => {
|
|
16370
16889
|
logger.debug(`${LOGGER_PREFIX} startStream called`);
|
|
16371
16890
|
await this.streamManager.startStream();
|
|
16891
|
+
this.ignorePromiseRejection(this.ensureConfigReady());
|
|
16892
|
+
this.recordingManager.prewarmStreamProcessor();
|
|
16372
16893
|
logger.debug(`${LOGGER_PREFIX} startStream completed`);
|
|
16373
16894
|
},
|
|
16374
16895
|
properties: {
|
|
@@ -16386,13 +16907,13 @@ class RecorderController {
|
|
|
16386
16907
|
return this.streamManager.switchAudioDevice(deviceId);
|
|
16387
16908
|
}
|
|
16388
16909
|
async startRecording() {
|
|
16389
|
-
await this.ensureConfigReady();
|
|
16390
16910
|
const sourceType = this.getCurrentSourceType();
|
|
16391
16911
|
await this.telemetryManager.executeAction({
|
|
16392
16912
|
requestedEvent: "recording.start.requested",
|
|
16393
16913
|
succeededEvent: "recording.start.succeeded",
|
|
16394
16914
|
failedEvent: "recording.start.failed",
|
|
16395
16915
|
action: async () => {
|
|
16916
|
+
await this.ensureConfigReady();
|
|
16396
16917
|
await this.recordingManager.startRecording();
|
|
16397
16918
|
},
|
|
16398
16919
|
properties: {
|
|
@@ -16516,13 +17037,25 @@ class RecorderController {
|
|
|
16516
17037
|
isConfigReady() {
|
|
16517
17038
|
return this.configManager.isConfigReady();
|
|
16518
17039
|
}
|
|
16519
|
-
|
|
17040
|
+
ensureConfigReady() {
|
|
16520
17041
|
if (this.isDemo) {
|
|
16521
|
-
return;
|
|
17042
|
+
return Promise.resolve();
|
|
16522
17043
|
}
|
|
16523
|
-
|
|
17044
|
+
if (this.configManager.isConfigReady()) {
|
|
17045
|
+
return Promise.resolve();
|
|
17046
|
+
}
|
|
17047
|
+
return this.configManager.fetchConfig().then(() => {
|
|
17048
|
+
return;
|
|
17049
|
+
}).catch(() => {
|
|
17050
|
+
return;
|
|
17051
|
+
});
|
|
16524
17052
|
}
|
|
16525
17053
|
cleanup() {
|
|
17054
|
+
this.isDestroyed = true;
|
|
17055
|
+
if (this.recordingWarmupTimeoutId !== null) {
|
|
17056
|
+
clearTimeout(this.recordingWarmupTimeoutId);
|
|
17057
|
+
this.recordingWarmupTimeoutId = null;
|
|
17058
|
+
}
|
|
16526
17059
|
this.storageManager.destroy();
|
|
16527
17060
|
this.recordingManager.cleanup();
|
|
16528
17061
|
this.audioLevelAnalyzer.stopTracking();
|
|
@@ -16642,6 +17175,30 @@ class RecorderController {
|
|
|
16642
17175
|
});
|
|
16643
17176
|
throw error;
|
|
16644
17177
|
}
|
|
17178
|
+
scheduleRecordingWarmup() {
|
|
17179
|
+
if (this.recordingWarmupTimeoutId !== null) {
|
|
17180
|
+
clearTimeout(this.recordingWarmupTimeoutId);
|
|
17181
|
+
this.recordingWarmupTimeoutId = null;
|
|
17182
|
+
}
|
|
17183
|
+
if (this.isDestroyed) {
|
|
17184
|
+
return;
|
|
17185
|
+
}
|
|
17186
|
+
this.recordingWarmupTimeoutId = setTimeout(() => {
|
|
17187
|
+
this.recordingWarmupTimeoutId = null;
|
|
17188
|
+
if (this.isDestroyed) {
|
|
17189
|
+
return;
|
|
17190
|
+
}
|
|
17191
|
+
this.ignorePromiseRejection(this.ensureConfigReady());
|
|
17192
|
+
this.ignorePromiseRejection(Promise.resolve().then(() => {
|
|
17193
|
+
this.recordingManager.prewarmStreamProcessor();
|
|
17194
|
+
}));
|
|
17195
|
+
}, RECORDING_WARMUP_DELAY_MILLISECONDS);
|
|
17196
|
+
}
|
|
17197
|
+
ignorePromiseRejection(promise) {
|
|
17198
|
+
promise.catch(() => {
|
|
17199
|
+
return;
|
|
17200
|
+
});
|
|
17201
|
+
}
|
|
16645
17202
|
}
|
|
16646
17203
|
// src/core/storage/quota-manager.ts
|
|
16647
17204
|
var PERCENTAGE_MULTIPLIER = 100;
|
|
@@ -17067,6 +17624,10 @@ export {
|
|
|
17067
17624
|
mapPresetToConfig,
|
|
17068
17625
|
logger,
|
|
17069
17626
|
isMobileDevice2 as isMobileDevice,
|
|
17627
|
+
getPresetVideoBitrateForFormat,
|
|
17628
|
+
getPresetTotalBitrate,
|
|
17629
|
+
getPresetAudioBitrateForFormat,
|
|
17630
|
+
getFormatCompatibilityPolicy,
|
|
17070
17631
|
getDefaultConfigForFormat,
|
|
17071
17632
|
getDefaultAudioCodecForFormat,
|
|
17072
17633
|
getBrowserName,
|
|
@@ -17079,6 +17640,8 @@ export {
|
|
|
17079
17640
|
extractLastFrame,
|
|
17080
17641
|
extractErrorMessage,
|
|
17081
17642
|
checkRecorderSupport,
|
|
17643
|
+
calculateVideoBitrate,
|
|
17644
|
+
calculateTotalBitrateFromMbPerMinute,
|
|
17082
17645
|
calculateBarColor,
|
|
17083
17646
|
VidtreoRecorder,
|
|
17084
17647
|
VideoUploadService,
|
|
@@ -17089,8 +17652,10 @@ export {
|
|
|
17089
17652
|
RecordingManager,
|
|
17090
17653
|
RecorderController,
|
|
17091
17654
|
QuotaManager,
|
|
17655
|
+
PRESET_SIZE_LIMIT_MB_PER_MINUTE,
|
|
17092
17656
|
NativeCameraHandler,
|
|
17093
17657
|
FORMAT_DEFAULT_CODECS,
|
|
17658
|
+
FORMAT_COMPATIBILITY_POLICY,
|
|
17094
17659
|
DeviceManager,
|
|
17095
17660
|
DEFAULT_TRANSCODE_CONFIG,
|
|
17096
17661
|
DEFAULT_STREAM_CONFIG,
|