@vidtreo/recorder 1.2.0 → 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 +1746 -1578
- package/dist/index.js +1228 -300
- 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
|
+
}
|
|
249
286
|
};
|
|
250
|
-
|
|
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
|
|
292
|
+
};
|
|
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,141 +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
|
-
AUDIO_CODEC_AAC,
|
|
310
|
-
AUDIO_CODEC_OPUS
|
|
311
|
-
];
|
|
312
|
-
async function resolveMediabunnyModule(dependencies) {
|
|
313
|
-
if (dependencies?.loadMediabunny) {
|
|
314
|
-
return await dependencies.loadMediabunny();
|
|
315
|
-
}
|
|
316
|
-
return await import("mediabunny");
|
|
317
|
-
}
|
|
318
|
-
function buildVideoCodecCheckOptions(width, height, bitrate) {
|
|
319
|
-
let checkOptions = {};
|
|
320
|
-
if (width !== undefined) {
|
|
321
|
-
checkOptions = { ...checkOptions, width };
|
|
322
|
-
}
|
|
323
|
-
if (height !== undefined) {
|
|
324
|
-
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;
|
|
325
385
|
}
|
|
326
|
-
if (
|
|
327
|
-
|
|
386
|
+
if (device.type === "tablet") {
|
|
387
|
+
return true;
|
|
328
388
|
}
|
|
329
|
-
return
|
|
389
|
+
return false;
|
|
330
390
|
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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 "";
|
|
335
401
|
}
|
|
336
|
-
return
|
|
337
|
-
}
|
|
338
|
-
function createMediabunnyModuleResult(module) {
|
|
339
|
-
return { kind: "result", module };
|
|
340
|
-
}
|
|
341
|
-
function createMediabunnyModuleError(error) {
|
|
342
|
-
return { kind: "error", error };
|
|
343
|
-
}
|
|
344
|
-
function createVideoCodecSupportResult(supported) {
|
|
345
|
-
return { kind: "result", supported };
|
|
402
|
+
return value.toLowerCase();
|
|
346
403
|
}
|
|
347
|
-
function
|
|
348
|
-
return
|
|
404
|
+
function hasPlatformKeyword(value, keyword) {
|
|
405
|
+
return value.includes(keyword);
|
|
349
406
|
}
|
|
350
|
-
function
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
function createAudioCodecLookupError(error) {
|
|
354
|
-
return { kind: "error", error };
|
|
355
|
-
}
|
|
356
|
-
async function detectBestCodec(width, height, bitrate, dependencies) {
|
|
357
|
-
const mediabunnyModuleResult = await resolveMediabunnyModule(dependencies).then((module) => createMediabunnyModuleResult(module)).catch((error) => createMediabunnyModuleError(error));
|
|
358
|
-
if (mediabunnyModuleResult.kind === "error") {
|
|
359
|
-
return VIDEO_CODEC_AVC;
|
|
407
|
+
function isNonBrowserRuntime(userAgent) {
|
|
408
|
+
if (userAgent.includes(RUNTIME_MARKER_BUN)) {
|
|
409
|
+
return true;
|
|
360
410
|
}
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
411
|
+
if (userAgent.includes(RUNTIME_MARKER_HAPPY_DOM)) {
|
|
412
|
+
return true;
|
|
413
|
+
}
|
|
414
|
+
if (userAgent.includes(RUNTIME_MARKER_NODE)) {
|
|
415
|
+
return true;
|
|
364
416
|
}
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
417
|
+
return false;
|
|
418
|
+
}
|
|
419
|
+
function resolveNavigatorProvider(navigatorProvider) {
|
|
420
|
+
if (navigatorProvider) {
|
|
421
|
+
return navigatorProvider;
|
|
369
422
|
}
|
|
370
|
-
if (
|
|
371
|
-
return
|
|
423
|
+
if (typeof navigator === "undefined") {
|
|
424
|
+
return null;
|
|
372
425
|
}
|
|
373
|
-
|
|
426
|
+
const globalNavigator = navigator;
|
|
427
|
+
return {
|
|
428
|
+
platform: globalNavigator.platform,
|
|
429
|
+
userAgent: globalNavigator.userAgent,
|
|
430
|
+
userAgentData: globalNavigator.userAgentData
|
|
431
|
+
};
|
|
374
432
|
}
|
|
375
|
-
|
|
376
|
-
const
|
|
377
|
-
if (
|
|
378
|
-
return
|
|
433
|
+
function isLinuxPlatform(navigatorProvider) {
|
|
434
|
+
const resolvedNavigatorProvider = resolveNavigatorProvider(navigatorProvider);
|
|
435
|
+
if (resolvedNavigatorProvider === null) {
|
|
436
|
+
return false;
|
|
379
437
|
}
|
|
380
|
-
const
|
|
381
|
-
if (
|
|
382
|
-
return
|
|
438
|
+
const userAgent = normalizeValue(resolvedNavigatorProvider.userAgent);
|
|
439
|
+
if (isNonBrowserRuntime(userAgent)) {
|
|
440
|
+
return false;
|
|
383
441
|
}
|
|
384
|
-
const
|
|
385
|
-
const
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
throw new Error(ERROR_NO_SUPPORTED_AUDIO_CODEC);
|
|
390
|
-
}
|
|
391
|
-
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;
|
|
392
447
|
}
|
|
393
|
-
if (
|
|
394
|
-
|
|
448
|
+
if (platform.includes(PLATFORM_KEYWORD_LINUX)) {
|
|
449
|
+
return true;
|
|
395
450
|
}
|
|
396
|
-
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
// src/core/utils/device-detector.ts
|
|
400
|
-
import { UAParser as UAParser2 } from "ua-parser-js";
|
|
401
|
-
function isMobileDevice() {
|
|
402
|
-
const parser = new UAParser2;
|
|
403
|
-
const device = parser.getDevice();
|
|
404
|
-
if (device.type === "mobile") {
|
|
451
|
+
if (userAgentDataPlatform.includes(PLATFORM_KEYWORD_LINUX)) {
|
|
405
452
|
return true;
|
|
406
453
|
}
|
|
407
|
-
if (
|
|
454
|
+
if (userAgent.includes(PLATFORM_KEYWORD_LINUX)) {
|
|
408
455
|
return true;
|
|
409
456
|
}
|
|
410
457
|
return false;
|
|
411
458
|
}
|
|
412
459
|
|
|
413
460
|
// src/core/config/preset-mapper.ts
|
|
414
|
-
|
|
461
|
+
function mapPresetToConfig(options) {
|
|
415
462
|
const { preset, outputFormat, watermark, isMobile } = options;
|
|
416
|
-
if (!(preset in
|
|
417
|
-
|
|
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;
|
|
418
473
|
}
|
|
419
|
-
const isMobileDeviceDetected = isMobile === undefined ? isMobileDevice() : isMobile;
|
|
420
|
-
const resolutionMap = isMobileDeviceDetected ? MOBILE_RESOLUTION_MAP : RESOLUTION_MAP;
|
|
421
474
|
const { width, height } = resolutionMap[preset];
|
|
422
|
-
|
|
423
|
-
|
|
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
|
+
});
|
|
424
486
|
const config = {
|
|
425
487
|
format,
|
|
426
488
|
width,
|
|
427
489
|
height,
|
|
428
|
-
bitrate:
|
|
429
|
-
audioCodec,
|
|
430
|
-
audioBitrate:
|
|
490
|
+
bitrate: getPresetVideoBitrateForFormat(preset, format),
|
|
491
|
+
audioCodec: policy.preferredAudioCodec,
|
|
492
|
+
audioBitrate: policy.audioBitrate
|
|
431
493
|
};
|
|
432
494
|
if (watermark) {
|
|
433
495
|
config.watermark = {
|
|
@@ -436,7 +498,7 @@ async function mapPresetToConfig(options) {
|
|
|
436
498
|
position: watermark.position
|
|
437
499
|
};
|
|
438
500
|
}
|
|
439
|
-
return config;
|
|
501
|
+
return Promise.resolve(config);
|
|
440
502
|
}
|
|
441
503
|
|
|
442
504
|
// src/core/config/config-service.ts
|
|
@@ -559,14 +621,7 @@ class ConfigManager {
|
|
|
559
621
|
apiKey,
|
|
560
622
|
backendUrl: normalizedBackendUrl
|
|
561
623
|
});
|
|
562
|
-
this.
|
|
563
|
-
this.currentConfig = config;
|
|
564
|
-
this.configReady = this.configService?.isConfigReady() ?? false;
|
|
565
|
-
this.configFetched = true;
|
|
566
|
-
}).catch(() => {
|
|
567
|
-
this.configReady = false;
|
|
568
|
-
this.configFetched = true;
|
|
569
|
-
});
|
|
624
|
+
this.fetchConfigInBackground();
|
|
570
625
|
}
|
|
571
626
|
async fetchConfig() {
|
|
572
627
|
if (!this.configService) {
|
|
@@ -582,6 +637,12 @@ class ConfigManager {
|
|
|
582
637
|
}
|
|
583
638
|
return this.currentConfig;
|
|
584
639
|
}
|
|
640
|
+
getConfigForRecording() {
|
|
641
|
+
if (this.configService && !this.configFetched) {
|
|
642
|
+
this.fetchConfigInBackground();
|
|
643
|
+
}
|
|
644
|
+
return this.currentConfig;
|
|
645
|
+
}
|
|
585
646
|
isConfigReady() {
|
|
586
647
|
return this.configReady;
|
|
587
648
|
}
|
|
@@ -591,6 +652,12 @@ class ConfigManager {
|
|
|
591
652
|
}
|
|
592
653
|
this.configService.clearCache();
|
|
593
654
|
}
|
|
655
|
+
fetchConfigInBackground() {
|
|
656
|
+
this.fetchConfig().catch(() => {
|
|
657
|
+
this.configReady = false;
|
|
658
|
+
this.configFetched = true;
|
|
659
|
+
});
|
|
660
|
+
}
|
|
594
661
|
}
|
|
595
662
|
// src/core/device/device-manager.ts
|
|
596
663
|
class DeviceManager {
|
|
@@ -734,6 +801,159 @@ import {
|
|
|
734
801
|
Output,
|
|
735
802
|
WebMOutputFormat
|
|
736
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
|
|
737
957
|
var ALLOW_ROTATION_METADATA = false;
|
|
738
958
|
function createSource(input) {
|
|
739
959
|
if (typeof input === "string") {
|
|
@@ -773,7 +993,6 @@ function getMimeTypeForFormat(format) {
|
|
|
773
993
|
}
|
|
774
994
|
}
|
|
775
995
|
function createConversionOptions(config, optimizeForSpeed = false) {
|
|
776
|
-
const audioCodec = getAudioCodecForFormat(config.format, config.audioCodec);
|
|
777
996
|
const video = {
|
|
778
997
|
fit: "contain",
|
|
779
998
|
forceTranscode: true,
|
|
@@ -801,7 +1020,7 @@ function createConversionOptions(config, optimizeForSpeed = false) {
|
|
|
801
1020
|
video.keyFrameInterval = 2;
|
|
802
1021
|
}
|
|
803
1022
|
const audio = {
|
|
804
|
-
codec: audioCodec,
|
|
1023
|
+
codec: config.audioCodec,
|
|
805
1024
|
forceTranscode: true,
|
|
806
1025
|
...optimizeForSpeed && { bitrateMode: "variable" }
|
|
807
1026
|
};
|
|
@@ -814,14 +1033,35 @@ function validateConversion(conversion) {
|
|
|
814
1033
|
}
|
|
815
1034
|
}
|
|
816
1035
|
async function transcodeVideo(input, config = {}, onProgress) {
|
|
1036
|
+
let resolvedFormat = config.format;
|
|
1037
|
+
if (resolvedFormat === undefined) {
|
|
1038
|
+
resolvedFormat = DEFAULT_TRANSCODE_CONFIG.format;
|
|
1039
|
+
}
|
|
817
1040
|
const finalConfig = {
|
|
818
1041
|
...DEFAULT_TRANSCODE_CONFIG,
|
|
819
1042
|
...config,
|
|
820
|
-
format:
|
|
1043
|
+
format: resolvedFormat
|
|
821
1044
|
};
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
}
|
|
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
|
+
});
|
|
825
1065
|
const source = createSource(input);
|
|
826
1066
|
const mediabunnyInput = new Input2({
|
|
827
1067
|
formats: ALL_FORMATS,
|
|
@@ -853,14 +1093,35 @@ async function transcodeVideo(input, config = {}, onProgress) {
|
|
|
853
1093
|
};
|
|
854
1094
|
}
|
|
855
1095
|
async function transcodeVideoForNativeCamera(file, config = {}, onProgress) {
|
|
1096
|
+
let resolvedFormat = config.format;
|
|
1097
|
+
if (resolvedFormat === undefined) {
|
|
1098
|
+
resolvedFormat = DEFAULT_TRANSCODE_CONFIG.format;
|
|
1099
|
+
}
|
|
856
1100
|
const finalConfig = {
|
|
857
1101
|
...DEFAULT_TRANSCODE_CONFIG,
|
|
858
1102
|
...config,
|
|
859
|
-
format:
|
|
1103
|
+
format: resolvedFormat
|
|
860
1104
|
};
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
}
|
|
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
|
+
});
|
|
864
1125
|
const source = new BlobSource2(file);
|
|
865
1126
|
const mediabunnyInput = new Input2({
|
|
866
1127
|
formats: ALL_FORMATS,
|
|
@@ -1028,6 +1289,25 @@ class NativeCameraHandler {
|
|
|
1028
1289
|
}
|
|
1029
1290
|
}
|
|
1030
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
|
+
}
|
|
1031
1311
|
// src/core/processor/worker/probe-worker-url.ts
|
|
1032
1312
|
var PROBE_WORKER_CODE = `"use strict";
|
|
1033
1313
|
var PROBE_MESSAGE_TYPE="probe";
|
|
@@ -1080,12 +1360,48 @@ var AUDIO_PATH_MAIN_THREAD_AUDIO_STREAM = "main-thread-audio-stream";
|
|
|
1080
1360
|
var AUDIO_PATH_AUDIO_WORKLET_CHUNKS = "audio-worklet-chunks";
|
|
1081
1361
|
var AUDIO_PATH_NONE_REQUIRED = "none-required";
|
|
1082
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;
|
|
1083
1371
|
function resolveBooleanOption(value, defaultValue) {
|
|
1084
1372
|
if (typeof value === "boolean") {
|
|
1085
1373
|
return value;
|
|
1086
1374
|
}
|
|
1087
1375
|
return defaultValue;
|
|
1088
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
|
+
}
|
|
1089
1405
|
function resolveVideoPath(inputs) {
|
|
1090
1406
|
if (inputs.probeResult.hasMediaStreamTrackProcessor) {
|
|
1091
1407
|
return VIDEO_PATH_WORKER_TRACK;
|
|
@@ -1218,6 +1534,30 @@ function getIsProbeFeaturesComplete(probeResult, requiresWatermark) {
|
|
|
1218
1534
|
async function checkRecorderSupport(options = {}) {
|
|
1219
1535
|
const requiresAudio = resolveBooleanOption(options.requiresAudio, true);
|
|
1220
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) {
|
|
1221
1561
|
const hasWorker = typeof Worker !== "undefined";
|
|
1222
1562
|
const audioContextClass = getAudioContextClass();
|
|
1223
1563
|
const hasAudioContext = audioContextClass !== null;
|
|
@@ -1341,7 +1681,7 @@ function getEmptyProbeResult() {
|
|
|
1341
1681
|
}
|
|
1342
1682
|
// src/core/storage/video-storage.ts
|
|
1343
1683
|
var DB_NAME = "vidtreo-recorder";
|
|
1344
|
-
var DB_VERSION =
|
|
1684
|
+
var DB_VERSION = 2;
|
|
1345
1685
|
var STORE_NAME = "pending-uploads";
|
|
1346
1686
|
var STATUS_INDEX = "status";
|
|
1347
1687
|
var CREATED_AT_INDEX = "createdAt";
|
|
@@ -1350,28 +1690,55 @@ var MAX_RETRIES = 10;
|
|
|
1350
1690
|
var MILLISECONDS_PER_HOUR = 60 * 60 * 1000;
|
|
1351
1691
|
var ID_PREFIX = "upload-";
|
|
1352
1692
|
var ID_RANDOM_LENGTH = 9;
|
|
1693
|
+
var VERSION_ERROR_NAME = "VersionError";
|
|
1694
|
+
var ERROR_SCHEMA_MISSING_STORE = "Database schema is missing required object store: pending-uploads";
|
|
1695
|
+
var ERROR_SCHEMA_MISSING_STATUS_INDEX = "Database schema is missing required index: status";
|
|
1696
|
+
var ERROR_SCHEMA_MISSING_CREATED_AT_INDEX = "Database schema is missing required index: createdAt";
|
|
1353
1697
|
|
|
1354
1698
|
class VideoStorageService {
|
|
1355
1699
|
db = null;
|
|
1700
|
+
databaseFactory;
|
|
1701
|
+
constructor(databaseFactory) {
|
|
1702
|
+
if (databaseFactory) {
|
|
1703
|
+
this.databaseFactory = databaseFactory;
|
|
1704
|
+
return;
|
|
1705
|
+
}
|
|
1706
|
+
this.databaseFactory = indexedDB;
|
|
1707
|
+
}
|
|
1356
1708
|
init() {
|
|
1357
1709
|
if (this.db) {
|
|
1358
1710
|
return Promise.resolve();
|
|
1359
1711
|
}
|
|
1712
|
+
return this.openDatabase(DB_VERSION, true);
|
|
1713
|
+
}
|
|
1714
|
+
openDatabase(databaseVersion, canRetryWithoutVersion) {
|
|
1360
1715
|
return new Promise((resolve, reject) => {
|
|
1361
|
-
const request =
|
|
1716
|
+
const request = this.createOpenRequest(databaseVersion);
|
|
1362
1717
|
request.onerror = () => {
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1718
|
+
const requestError = request.error;
|
|
1719
|
+
if (canRetryWithoutVersion && requestError && requestError.name === VERSION_ERROR_NAME) {
|
|
1720
|
+
this.openDatabase(undefined, false).then(resolve).catch(reject);
|
|
1721
|
+
return;
|
|
1722
|
+
}
|
|
1723
|
+
if (requestError) {
|
|
1724
|
+
reject(requestError);
|
|
1725
|
+
return;
|
|
1367
1726
|
}
|
|
1727
|
+
reject(new Error("Failed to open database"));
|
|
1368
1728
|
};
|
|
1369
1729
|
request.onsuccess = () => {
|
|
1370
1730
|
if (!request.result) {
|
|
1371
1731
|
reject(new Error("Database result is null"));
|
|
1372
1732
|
return;
|
|
1373
1733
|
}
|
|
1374
|
-
|
|
1734
|
+
const database = request.result;
|
|
1735
|
+
const schemaValidationError = this.validateRequiredSchema(database);
|
|
1736
|
+
if (schemaValidationError) {
|
|
1737
|
+
database.close();
|
|
1738
|
+
reject(schemaValidationError);
|
|
1739
|
+
return;
|
|
1740
|
+
}
|
|
1741
|
+
this.db = database;
|
|
1375
1742
|
resolve();
|
|
1376
1743
|
};
|
|
1377
1744
|
request.onupgradeneeded = (event) => {
|
|
@@ -1380,16 +1747,42 @@ class VideoStorageService {
|
|
|
1380
1747
|
reject(new Error("Database upgrade result is null"));
|
|
1381
1748
|
return;
|
|
1382
1749
|
}
|
|
1383
|
-
|
|
1384
|
-
const store = db.createObjectStore(STORE_NAME, { keyPath: "id" });
|
|
1385
|
-
store.createIndex(STATUS_INDEX, STATUS_INDEX, { unique: false });
|
|
1386
|
-
store.createIndex(CREATED_AT_INDEX, CREATED_AT_INDEX, {
|
|
1387
|
-
unique: false
|
|
1388
|
-
});
|
|
1389
|
-
}
|
|
1750
|
+
this.initializeStoreSchema(db);
|
|
1390
1751
|
};
|
|
1391
1752
|
});
|
|
1392
1753
|
}
|
|
1754
|
+
createOpenRequest(databaseVersion) {
|
|
1755
|
+
if (databaseVersion === undefined) {
|
|
1756
|
+
return this.databaseFactory.open(DB_NAME);
|
|
1757
|
+
}
|
|
1758
|
+
return this.databaseFactory.open(DB_NAME, databaseVersion);
|
|
1759
|
+
}
|
|
1760
|
+
initializeStoreSchema(database) {
|
|
1761
|
+
if (database.objectStoreNames.contains(STORE_NAME)) {
|
|
1762
|
+
return;
|
|
1763
|
+
}
|
|
1764
|
+
const objectStore = database.createObjectStore(STORE_NAME, {
|
|
1765
|
+
keyPath: "id"
|
|
1766
|
+
});
|
|
1767
|
+
objectStore.createIndex(STATUS_INDEX, STATUS_INDEX, { unique: false });
|
|
1768
|
+
objectStore.createIndex(CREATED_AT_INDEX, CREATED_AT_INDEX, {
|
|
1769
|
+
unique: false
|
|
1770
|
+
});
|
|
1771
|
+
}
|
|
1772
|
+
validateRequiredSchema(database) {
|
|
1773
|
+
if (!database.objectStoreNames.contains(STORE_NAME)) {
|
|
1774
|
+
return new Error(ERROR_SCHEMA_MISSING_STORE);
|
|
1775
|
+
}
|
|
1776
|
+
const transaction = database.transaction([STORE_NAME], "readonly");
|
|
1777
|
+
const store = transaction.objectStore(STORE_NAME);
|
|
1778
|
+
if (!store.indexNames.contains(STATUS_INDEX)) {
|
|
1779
|
+
return new Error(ERROR_SCHEMA_MISSING_STATUS_INDEX);
|
|
1780
|
+
}
|
|
1781
|
+
if (!store.indexNames.contains(CREATED_AT_INDEX)) {
|
|
1782
|
+
return new Error(ERROR_SCHEMA_MISSING_CREATED_AT_INDEX);
|
|
1783
|
+
}
|
|
1784
|
+
return null;
|
|
1785
|
+
}
|
|
1393
1786
|
isInitialized() {
|
|
1394
1787
|
return this.db !== null;
|
|
1395
1788
|
}
|
|
@@ -2870,6 +3263,11 @@ function formatTime(totalSeconds) {
|
|
|
2870
3263
|
}
|
|
2871
3264
|
|
|
2872
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
|
+
|
|
2873
3271
|
class TabVisibilityTracker {
|
|
2874
3272
|
recordingStartTime = 0;
|
|
2875
3273
|
totalPausedTime = 0;
|
|
@@ -2880,10 +3278,17 @@ class TabVisibilityTracker {
|
|
|
2880
3278
|
visibilityChangeHandler;
|
|
2881
3279
|
blurHandler;
|
|
2882
3280
|
focusHandler;
|
|
2883
|
-
|
|
3281
|
+
getCurrentTimestamp;
|
|
3282
|
+
constructor(dependencies) {
|
|
2884
3283
|
this.visibilityChangeHandler = this.handleVisibilityChange.bind(this);
|
|
2885
3284
|
this.blurHandler = this.handleBlur.bind(this);
|
|
2886
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
|
+
}
|
|
2887
3292
|
}
|
|
2888
3293
|
start(recordingStartTime) {
|
|
2889
3294
|
if (this.isTracking) {
|
|
@@ -2908,14 +3313,14 @@ class TabVisibilityTracker {
|
|
|
2908
3313
|
if (!this.isTracking || this.pauseStartTime !== null) {
|
|
2909
3314
|
return;
|
|
2910
3315
|
}
|
|
2911
|
-
this.pauseStartTime =
|
|
3316
|
+
this.pauseStartTime = this.getCurrentTimestamp();
|
|
2912
3317
|
this.endCurrentIntervalIfActive();
|
|
2913
3318
|
}
|
|
2914
3319
|
resume() {
|
|
2915
3320
|
if (!this.isTracking || this.pauseStartTime === null) {
|
|
2916
3321
|
return;
|
|
2917
3322
|
}
|
|
2918
|
-
const pausedDuration =
|
|
3323
|
+
const pausedDuration = this.getCurrentTimestamp() - this.pauseStartTime;
|
|
2919
3324
|
this.totalPausedTime += pausedDuration;
|
|
2920
3325
|
this.pauseStartTime = null;
|
|
2921
3326
|
}
|
|
@@ -2975,13 +3380,13 @@ class TabVisibilityTracker {
|
|
|
2975
3380
|
if (this.pauseStartTime !== null) {
|
|
2976
3381
|
return;
|
|
2977
3382
|
}
|
|
2978
|
-
this.currentIntervalStart =
|
|
3383
|
+
this.currentIntervalStart = this.getCurrentTimestamp();
|
|
2979
3384
|
}
|
|
2980
3385
|
endCurrentIntervalIfActive() {
|
|
2981
3386
|
if (this.currentIntervalStart === null) {
|
|
2982
3387
|
return;
|
|
2983
3388
|
}
|
|
2984
|
-
const endTime =
|
|
3389
|
+
const endTime = this.getCurrentTimestamp();
|
|
2985
3390
|
const startTime = this.currentIntervalStart;
|
|
2986
3391
|
if (endTime > startTime) {
|
|
2987
3392
|
this.intervals.push({
|
|
@@ -2993,7 +3398,7 @@ class TabVisibilityTracker {
|
|
|
2993
3398
|
}
|
|
2994
3399
|
normalizeTimestamp(absoluteTime) {
|
|
2995
3400
|
const elapsed = absoluteTime - this.recordingStartTime;
|
|
2996
|
-
const normalized = (elapsed - this.totalPausedTime) /
|
|
3401
|
+
const normalized = (elapsed - this.totalPausedTime) / MILLISECONDS_PER_SECOND;
|
|
2997
3402
|
return Math.max(0, normalized);
|
|
2998
3403
|
}
|
|
2999
3404
|
}
|
|
@@ -3001,7 +3406,12 @@ class TabVisibilityTracker {
|
|
|
3001
3406
|
// src/core/stream/stream-recording-state.ts
|
|
3002
3407
|
var TIMER_INTERVAL = 1000;
|
|
3003
3408
|
var SECONDS_PER_MINUTE2 = 60;
|
|
3004
|
-
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
|
+
};
|
|
3005
3415
|
|
|
3006
3416
|
class StreamRecordingState {
|
|
3007
3417
|
recordingStartTime = 0;
|
|
@@ -3015,8 +3425,27 @@ class StreamRecordingState {
|
|
|
3015
3425
|
blurHandler = null;
|
|
3016
3426
|
focusHandler = null;
|
|
3017
3427
|
streamManager;
|
|
3018
|
-
|
|
3428
|
+
dependencies;
|
|
3429
|
+
constructor(streamManager, dependencies) {
|
|
3019
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
|
+
};
|
|
3020
3449
|
}
|
|
3021
3450
|
isRecording() {
|
|
3022
3451
|
return this.streamManager.getState() === "recording";
|
|
@@ -3035,11 +3464,15 @@ class StreamRecordingState {
|
|
|
3035
3464
|
}
|
|
3036
3465
|
async startRecording(processor, config, enableTabVisibilityOverlay, tabVisibilityOverlayText) {
|
|
3037
3466
|
const mediaStream = this.streamManager.getStream();
|
|
3467
|
+
let audioTrackCount = 0;
|
|
3468
|
+
if (mediaStream !== null) {
|
|
3469
|
+
audioTrackCount = mediaStream.getAudioTracks().length;
|
|
3470
|
+
}
|
|
3038
3471
|
logger.debug("[StreamRecordingState] startRecording called", {
|
|
3039
3472
|
hasMediaStream: !!mediaStream,
|
|
3040
3473
|
isRecording: this.isRecording(),
|
|
3041
3474
|
hasProcessor: !!processor,
|
|
3042
|
-
audioTracks:
|
|
3475
|
+
audioTracks: audioTrackCount
|
|
3043
3476
|
});
|
|
3044
3477
|
if (!mediaStream) {
|
|
3045
3478
|
throw new Error("Stream must be started before recording");
|
|
@@ -3050,7 +3483,7 @@ class StreamRecordingState {
|
|
|
3050
3483
|
}
|
|
3051
3484
|
const hasAudioTracks = mediaStream.getAudioTracks().length > 0;
|
|
3052
3485
|
const requiresWatermark = config.watermark !== undefined;
|
|
3053
|
-
const supportReport = await checkRecorderSupport({
|
|
3486
|
+
const supportReport = await this.dependencies.checkRecorderSupport({
|
|
3054
3487
|
requiresAudio: hasAudioTracks,
|
|
3055
3488
|
requiresWatermark
|
|
3056
3489
|
});
|
|
@@ -3078,11 +3511,15 @@ class StreamRecordingState {
|
|
|
3078
3511
|
this.streamManager.emit("recordingbufferupdate", { size, formatted });
|
|
3079
3512
|
}, TIMER_INTERVAL);
|
|
3080
3513
|
this.resetRecordingState();
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
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
|
+
}
|
|
3086
3523
|
logger.debug("[StreamRecordingState] Overlay config", {
|
|
3087
3524
|
enableTabVisibilityOverlay,
|
|
3088
3525
|
hasOverlayText: !!tabVisibilityOverlayText,
|
|
@@ -3105,9 +3542,11 @@ class StreamRecordingState {
|
|
|
3105
3542
|
this.startRecordingTimer();
|
|
3106
3543
|
}
|
|
3107
3544
|
async stopRecording() {
|
|
3545
|
+
const recordingElapsedSeconds = (this.dependencies.getCurrentTimestamp() - this.recordingStartTime - this.totalPausedTime) / MILLISECONDS_PER_SECOND2;
|
|
3108
3546
|
logger.debug("[StreamRecordingState] stopRecording called", {
|
|
3109
3547
|
hasStreamProcessor: !!this.streamProcessor,
|
|
3110
|
-
isRecording: this.isRecording()
|
|
3548
|
+
isRecording: this.isRecording(),
|
|
3549
|
+
recordingElapsedSeconds
|
|
3111
3550
|
});
|
|
3112
3551
|
if (!(this.streamProcessor && this.isRecording())) {
|
|
3113
3552
|
throw new Error("Not currently recording");
|
|
@@ -3150,7 +3589,7 @@ class StreamRecordingState {
|
|
|
3150
3589
|
pauseRecording() {
|
|
3151
3590
|
this.clearRecordingTimer();
|
|
3152
3591
|
if (this.pauseStartTime === null) {
|
|
3153
|
-
this.pauseStartTime =
|
|
3592
|
+
this.pauseStartTime = this.dependencies.getCurrentTimestamp();
|
|
3154
3593
|
}
|
|
3155
3594
|
if (this.tabVisibilityTracker) {
|
|
3156
3595
|
this.tabVisibilityTracker.pause();
|
|
@@ -3161,7 +3600,7 @@ class StreamRecordingState {
|
|
|
3161
3600
|
}
|
|
3162
3601
|
resumeRecording() {
|
|
3163
3602
|
if (this.pauseStartTime !== null) {
|
|
3164
|
-
const pausedDuration =
|
|
3603
|
+
const pausedDuration = this.dependencies.getCurrentTimestamp() - this.pauseStartTime;
|
|
3165
3604
|
this.totalPausedTime += pausedDuration;
|
|
3166
3605
|
this.pauseStartTime = null;
|
|
3167
3606
|
}
|
|
@@ -3232,7 +3671,7 @@ class StreamRecordingState {
|
|
|
3232
3671
|
}
|
|
3233
3672
|
startRecordingTimer() {
|
|
3234
3673
|
this.recordingTimer = window.setInterval(() => {
|
|
3235
|
-
const elapsed = (
|
|
3674
|
+
const elapsed = (this.dependencies.getCurrentTimestamp() - this.recordingStartTime - this.totalPausedTime) / MILLISECONDS_PER_SECOND2;
|
|
3236
3675
|
const formatted = this.formatTimeElapsed(elapsed);
|
|
3237
3676
|
this.streamManager.emit("recordingtimeupdate", { elapsed, formatted });
|
|
3238
3677
|
}, TIMER_INTERVAL);
|
|
@@ -3250,7 +3689,7 @@ class StreamRecordingState {
|
|
|
3250
3689
|
}
|
|
3251
3690
|
}
|
|
3252
3691
|
resetRecordingState() {
|
|
3253
|
-
this.recordingStartTime =
|
|
3692
|
+
this.recordingStartTime = this.dependencies.getCurrentTimestamp();
|
|
3254
3693
|
this.totalPausedTime = 0;
|
|
3255
3694
|
this.pauseStartTime = null;
|
|
3256
3695
|
}
|
|
@@ -3258,6 +3697,16 @@ class StreamRecordingState {
|
|
|
3258
3697
|
this.totalPausedTime = 0;
|
|
3259
3698
|
this.pauseStartTime = null;
|
|
3260
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
|
+
}
|
|
3261
3710
|
setupVisibilityUpdates(processor) {
|
|
3262
3711
|
if (typeof document === "undefined" || typeof window === "undefined") {
|
|
3263
3712
|
logger.warn("[StreamRecordingState] Cannot setup visibility updates - document/window not available");
|
|
@@ -3268,7 +3717,7 @@ class StreamRecordingState {
|
|
|
3268
3717
|
return;
|
|
3269
3718
|
}
|
|
3270
3719
|
const isHidden = document.visibilityState === "hidden";
|
|
3271
|
-
const timestamp =
|
|
3720
|
+
const timestamp = this.dependencies.getCurrentTimestamp();
|
|
3272
3721
|
logger.debug("[StreamRecordingState] Visibility change", {
|
|
3273
3722
|
isHidden,
|
|
3274
3723
|
timestamp,
|
|
@@ -3277,12 +3726,12 @@ class StreamRecordingState {
|
|
|
3277
3726
|
processor.updateTabVisibility(isHidden, timestamp);
|
|
3278
3727
|
};
|
|
3279
3728
|
this.blurHandler = () => {
|
|
3280
|
-
const timestamp =
|
|
3729
|
+
const timestamp = this.dependencies.getCurrentTimestamp();
|
|
3281
3730
|
logger.debug("[StreamRecordingState] Window blur", { timestamp });
|
|
3282
3731
|
processor.updateTabVisibility(true, timestamp);
|
|
3283
3732
|
};
|
|
3284
3733
|
this.focusHandler = () => {
|
|
3285
|
-
const timestamp =
|
|
3734
|
+
const timestamp = this.dependencies.getCurrentTimestamp();
|
|
3286
3735
|
logger.debug("[StreamRecordingState] Window focus", { timestamp });
|
|
3287
3736
|
processor.updateTabVisibility(false, timestamp);
|
|
3288
3737
|
};
|
|
@@ -3291,7 +3740,7 @@ class StreamRecordingState {
|
|
|
3291
3740
|
window.addEventListener("focus", this.focusHandler);
|
|
3292
3741
|
const initialHidden = document.visibilityState === "hidden";
|
|
3293
3742
|
if (initialHidden) {
|
|
3294
|
-
const timestamp =
|
|
3743
|
+
const timestamp = this.dependencies.getCurrentTimestamp();
|
|
3295
3744
|
logger.debug("[StreamRecordingState] Initial state is hidden", {
|
|
3296
3745
|
timestamp
|
|
3297
3746
|
});
|
|
@@ -3429,7 +3878,7 @@ class CameraStreamManager {
|
|
|
3429
3878
|
// package.json
|
|
3430
3879
|
var package_default = {
|
|
3431
3880
|
name: "@vidtreo/recorder",
|
|
3432
|
-
version: "1.
|
|
3881
|
+
version: "1.3.0",
|
|
3433
3882
|
type: "module",
|
|
3434
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.",
|
|
3435
3884
|
main: "./dist/index.js",
|
|
@@ -4348,6 +4797,118 @@ function serializeBitrate(bitrate) {
|
|
|
4348
4797
|
return "high";
|
|
4349
4798
|
}
|
|
4350
4799
|
|
|
4800
|
+
// src/core/processor/mp4-container-guard.ts
|
|
4801
|
+
var MP4_BOX_HEADER_BYTES = 8;
|
|
4802
|
+
var MP4_BOX_TYPE_OFFSET_BYTES = 4;
|
|
4803
|
+
var MP4_EXTENDED_SIZE_MARKER = 1;
|
|
4804
|
+
var MP4_ZERO_SIZE_MARKER = 0;
|
|
4805
|
+
var MP4_EXTENDED_SIZE_BYTES = 8;
|
|
4806
|
+
var MP4_EXTENDED_BOX_HEADER_BYTES = MP4_BOX_HEADER_BYTES + MP4_EXTENDED_SIZE_BYTES;
|
|
4807
|
+
var TWO_POWER_32 = 4294967296;
|
|
4808
|
+
var MP4_FRAGMENT_BOX_TYPE_MOOF = "moof";
|
|
4809
|
+
var MP4_FRAGMENT_BOX_TYPE_MFRA = "mfra";
|
|
4810
|
+
var ERROR_RECORDING_INVALID_CONTAINER_LAYOUT = "recording.invalid-container-layout";
|
|
4811
|
+
function createInvalidMp4ContainerLayoutError(detectedBoxTypes) {
|
|
4812
|
+
const error = new Error(ERROR_RECORDING_INVALID_CONTAINER_LAYOUT);
|
|
4813
|
+
error.code = ERROR_RECORDING_INVALID_CONTAINER_LAYOUT;
|
|
4814
|
+
error.detectedBoxTypes = detectedBoxTypes;
|
|
4815
|
+
return error;
|
|
4816
|
+
}
|
|
4817
|
+
function toUint8Array(input) {
|
|
4818
|
+
if (input instanceof Uint8Array) {
|
|
4819
|
+
return input;
|
|
4820
|
+
}
|
|
4821
|
+
return new Uint8Array(input);
|
|
4822
|
+
}
|
|
4823
|
+
function readBoxType(view, offset) {
|
|
4824
|
+
const firstCharCode = view.getUint8(offset);
|
|
4825
|
+
const secondCharCode = view.getUint8(offset + 1);
|
|
4826
|
+
const thirdCharCode = view.getUint8(offset + 2);
|
|
4827
|
+
const fourthCharCode = view.getUint8(offset + 3);
|
|
4828
|
+
return String.fromCharCode(firstCharCode, secondCharCode, thirdCharCode, fourthCharCode);
|
|
4829
|
+
}
|
|
4830
|
+
function readLargeSize(view, offset) {
|
|
4831
|
+
const highBits = view.getUint32(offset, false);
|
|
4832
|
+
const lowBits = view.getUint32(offset + MP4_BOX_TYPE_OFFSET_BYTES, false);
|
|
4833
|
+
return highBits * TWO_POWER_32 + lowBits;
|
|
4834
|
+
}
|
|
4835
|
+
function getUniqueValues(values) {
|
|
4836
|
+
const uniqueValues = new Set;
|
|
4837
|
+
for (const value of values) {
|
|
4838
|
+
uniqueValues.add(value);
|
|
4839
|
+
}
|
|
4840
|
+
return [...uniqueValues];
|
|
4841
|
+
}
|
|
4842
|
+
function parseMp4TopLevelBoxes(input) {
|
|
4843
|
+
const bytes = toUint8Array(input);
|
|
4844
|
+
const totalBytes = bytes.byteLength;
|
|
4845
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
4846
|
+
const boxes = [];
|
|
4847
|
+
let currentOffset = 0;
|
|
4848
|
+
while (currentOffset < totalBytes) {
|
|
4849
|
+
const hasMinimumHeader = currentOffset + MP4_BOX_HEADER_BYTES <= totalBytes;
|
|
4850
|
+
if (!hasMinimumHeader) {
|
|
4851
|
+
const detectedBoxTypes = boxes.map((box) => box.type);
|
|
4852
|
+
throw createInvalidMp4ContainerLayoutError(detectedBoxTypes);
|
|
4853
|
+
}
|
|
4854
|
+
const declaredSize = view.getUint32(currentOffset, false);
|
|
4855
|
+
const typeOffset = currentOffset + MP4_BOX_TYPE_OFFSET_BYTES;
|
|
4856
|
+
const boxType = readBoxType(view, typeOffset);
|
|
4857
|
+
let boxSize = declaredSize;
|
|
4858
|
+
let headerSize = MP4_BOX_HEADER_BYTES;
|
|
4859
|
+
if (declaredSize === MP4_EXTENDED_SIZE_MARKER) {
|
|
4860
|
+
const largeSizeOffset = currentOffset + MP4_BOX_HEADER_BYTES;
|
|
4861
|
+
const hasExtendedHeader = largeSizeOffset + MP4_EXTENDED_SIZE_BYTES <= totalBytes;
|
|
4862
|
+
if (!hasExtendedHeader) {
|
|
4863
|
+
const detectedBoxTypes = boxes.map((box) => box.type);
|
|
4864
|
+
throw createInvalidMp4ContainerLayoutError(detectedBoxTypes);
|
|
4865
|
+
}
|
|
4866
|
+
boxSize = readLargeSize(view, largeSizeOffset);
|
|
4867
|
+
headerSize = MP4_EXTENDED_BOX_HEADER_BYTES;
|
|
4868
|
+
}
|
|
4869
|
+
if (declaredSize === MP4_ZERO_SIZE_MARKER) {
|
|
4870
|
+
boxSize = totalBytes - currentOffset;
|
|
4871
|
+
}
|
|
4872
|
+
const hasValidHeaderSize = boxSize >= headerSize;
|
|
4873
|
+
if (!hasValidHeaderSize) {
|
|
4874
|
+
const detectedBoxTypes = boxes.map((box) => box.type);
|
|
4875
|
+
throw createInvalidMp4ContainerLayoutError(detectedBoxTypes);
|
|
4876
|
+
}
|
|
4877
|
+
const nextOffset = currentOffset + boxSize;
|
|
4878
|
+
const hasValidBoxRange = nextOffset <= totalBytes;
|
|
4879
|
+
if (!hasValidBoxRange) {
|
|
4880
|
+
const detectedBoxTypes = boxes.map((box) => box.type);
|
|
4881
|
+
throw createInvalidMp4ContainerLayoutError(detectedBoxTypes);
|
|
4882
|
+
}
|
|
4883
|
+
boxes.push({
|
|
4884
|
+
type: boxType,
|
|
4885
|
+
size: boxSize,
|
|
4886
|
+
startOffset: currentOffset,
|
|
4887
|
+
endOffset: nextOffset
|
|
4888
|
+
});
|
|
4889
|
+
currentOffset = nextOffset;
|
|
4890
|
+
}
|
|
4891
|
+
return boxes;
|
|
4892
|
+
}
|
|
4893
|
+
function assertMp4ContainerIsNonFragmented(input) {
|
|
4894
|
+
const topLevelBoxes = parseMp4TopLevelBoxes(input);
|
|
4895
|
+
const detectedFragmentBoxes = topLevelBoxes.filter((box) => {
|
|
4896
|
+
if (box.type === MP4_FRAGMENT_BOX_TYPE_MOOF) {
|
|
4897
|
+
return true;
|
|
4898
|
+
}
|
|
4899
|
+
if (box.type === MP4_FRAGMENT_BOX_TYPE_MFRA) {
|
|
4900
|
+
return true;
|
|
4901
|
+
}
|
|
4902
|
+
return false;
|
|
4903
|
+
}).map((box) => box.type);
|
|
4904
|
+
const hasFragmentBoxes = detectedFragmentBoxes.length > 0;
|
|
4905
|
+
if (!hasFragmentBoxes) {
|
|
4906
|
+
return;
|
|
4907
|
+
}
|
|
4908
|
+
const uniqueDetectedBoxTypes = getUniqueValues(detectedFragmentBoxes);
|
|
4909
|
+
throw createInvalidMp4ContainerLayoutError(uniqueDetectedBoxTypes);
|
|
4910
|
+
}
|
|
4911
|
+
|
|
4351
4912
|
// src/core/utils/shared-object-url-store.ts
|
|
4352
4913
|
function createSharedObjectUrlStore(dependencies) {
|
|
4353
4914
|
let activeUrl = null;
|
|
@@ -13374,6 +13935,54 @@ class FrameCompositor {
|
|
|
13374
13935
|
}
|
|
13375
13936
|
}
|
|
13376
13937
|
|
|
13938
|
+
// src/core/processor/worker/stop-finalization.ts
|
|
13939
|
+
var STOP_PENDING_WRITES_TIMEOUT_MILLISECONDS = 500;
|
|
13940
|
+
var STOP_PENDING_WRITES_POLL_INTERVAL_MILLISECONDS = 10;
|
|
13941
|
+
var ERROR_STOP_PENDING_WRITES_TIMEOUT = "stop.pending-writes-timeout";
|
|
13942
|
+
function createDefaultNowMilliseconds() {
|
|
13943
|
+
return () => performance.now();
|
|
13944
|
+
}
|
|
13945
|
+
function createDefaultWaitMilliseconds() {
|
|
13946
|
+
return (milliseconds) => new Promise((resolve) => {
|
|
13947
|
+
globalThis.setTimeout(resolve, milliseconds);
|
|
13948
|
+
});
|
|
13949
|
+
}
|
|
13950
|
+
async function waitForPendingWritesToDrain(dependencies) {
|
|
13951
|
+
let getNowMilliseconds = dependencies.getNowMilliseconds;
|
|
13952
|
+
if (!getNowMilliseconds) {
|
|
13953
|
+
getNowMilliseconds = createDefaultNowMilliseconds();
|
|
13954
|
+
}
|
|
13955
|
+
let waitMilliseconds = dependencies.waitMilliseconds;
|
|
13956
|
+
if (!waitMilliseconds) {
|
|
13957
|
+
waitMilliseconds = createDefaultWaitMilliseconds();
|
|
13958
|
+
}
|
|
13959
|
+
let timeoutMilliseconds = dependencies.timeoutMilliseconds;
|
|
13960
|
+
if (timeoutMilliseconds === undefined) {
|
|
13961
|
+
timeoutMilliseconds = STOP_PENDING_WRITES_TIMEOUT_MILLISECONDS;
|
|
13962
|
+
}
|
|
13963
|
+
const startedAtMilliseconds = getNowMilliseconds();
|
|
13964
|
+
let pendingWriteCount = dependencies.getPendingWriteCount();
|
|
13965
|
+
while (pendingWriteCount > 0) {
|
|
13966
|
+
const elapsedMilliseconds = getNowMilliseconds() - startedAtMilliseconds;
|
|
13967
|
+
if (elapsedMilliseconds >= timeoutMilliseconds) {
|
|
13968
|
+
throw new Error(ERROR_STOP_PENDING_WRITES_TIMEOUT);
|
|
13969
|
+
}
|
|
13970
|
+
await waitMilliseconds(STOP_PENDING_WRITES_POLL_INTERVAL_MILLISECONDS);
|
|
13971
|
+
pendingWriteCount = dependencies.getPendingWriteCount();
|
|
13972
|
+
}
|
|
13973
|
+
}
|
|
13974
|
+
|
|
13975
|
+
// src/core/processor/worker/stop-transition.ts
|
|
13976
|
+
async function runStopTransition(dependencies) {
|
|
13977
|
+
await dependencies.finalizeStopSequence().then(() => dependencies.completeStop()).catch((error) => {
|
|
13978
|
+
return dependencies.recoverStopFailure().then(() => {
|
|
13979
|
+
throw error;
|
|
13980
|
+
});
|
|
13981
|
+
}).finally(() => {
|
|
13982
|
+
dependencies.clearStoppingFlag();
|
|
13983
|
+
});
|
|
13984
|
+
}
|
|
13985
|
+
|
|
13377
13986
|
// src/core/processor/worker/timestamp-manager.ts
|
|
13378
13987
|
var DEFAULT_FRAME_RATE = 30;
|
|
13379
13988
|
var DEFAULT_KEY_FRAME_INTERVAL_SECONDS = 5;
|
|
@@ -13584,13 +14193,14 @@ function clampValue(value, min, max) {
|
|
|
13584
14193
|
var WORKER_MESSAGE_TYPE_PROBE = "probe";
|
|
13585
14194
|
var WORKER_MESSAGE_TYPE_AUDIO_CHUNK = "audioChunk";
|
|
13586
14195
|
var WORKER_RESPONSE_TYPE_PROBE_RESULT = "probeResult";
|
|
13587
|
-
var WORKER_RESPONSE_TYPE_DEBUG_LOG = "debugLog";
|
|
13588
14196
|
var WORKER_AUDIO_SAMPLE_FORMAT_F32_PLANAR = "f32-planar";
|
|
13589
14197
|
|
|
13590
14198
|
// src/core/processor/worker/visibility-tracker.ts
|
|
13591
14199
|
var MILLISECONDS_PER_SECOND3 = 1000;
|
|
13592
14200
|
var OVERLAY_LOG_FRAME_INTERVAL = 90;
|
|
13593
14201
|
var RECORDER_WORKER_LOG_PREFIX3 = "[RecorderWorker]";
|
|
14202
|
+
var INITIAL_INTERVAL_INDEX = 0;
|
|
14203
|
+
var INITIAL_LAST_OVERLAY_TIMESTAMP = -1;
|
|
13594
14204
|
|
|
13595
14205
|
class VisibilityTracker {
|
|
13596
14206
|
hiddenIntervals = [];
|
|
@@ -13598,6 +14208,8 @@ class VisibilityTracker {
|
|
|
13598
14208
|
pendingVisibilityUpdates = [];
|
|
13599
14209
|
recordingStartTime = 0;
|
|
13600
14210
|
isScreenCapture = false;
|
|
14211
|
+
intervalCursor = INITIAL_INTERVAL_INDEX;
|
|
14212
|
+
lastOverlayTimestamp = INITIAL_LAST_OVERLAY_TIMESTAMP;
|
|
13601
14213
|
logger;
|
|
13602
14214
|
constructor(dependencies) {
|
|
13603
14215
|
this.logger = dependencies.logger;
|
|
@@ -13608,6 +14220,8 @@ class VisibilityTracker {
|
|
|
13608
14220
|
this.pendingVisibilityUpdates = [];
|
|
13609
14221
|
this.recordingStartTime = recordingStartTime;
|
|
13610
14222
|
this.isScreenCapture = isScreenCapture;
|
|
14223
|
+
this.intervalCursor = INITIAL_INTERVAL_INDEX;
|
|
14224
|
+
this.lastOverlayTimestamp = INITIAL_LAST_OVERLAY_TIMESTAMP;
|
|
13611
14225
|
}
|
|
13612
14226
|
setRecordingStartTime(recordingStartTime) {
|
|
13613
14227
|
this.recordingStartTime = recordingStartTime;
|
|
@@ -13618,32 +14232,52 @@ class VisibilityTracker {
|
|
|
13618
14232
|
getPendingUpdatesCount() {
|
|
13619
14233
|
return this.pendingVisibilityUpdates.length;
|
|
13620
14234
|
}
|
|
13621
|
-
shouldApplyOverlay(
|
|
13622
|
-
if (!parameters.overlayEnabled) {
|
|
13623
|
-
return false;
|
|
13624
|
-
}
|
|
14235
|
+
shouldApplyOverlay(timestamp, frameCount) {
|
|
13625
14236
|
if (this.isScreenCapture) {
|
|
13626
14237
|
return false;
|
|
13627
14238
|
}
|
|
13628
|
-
const
|
|
13629
|
-
const currentIntervalMatch = this.currentHiddenIntervalStart !== null && parameters.timestamp >= this.currentHiddenIntervalStart;
|
|
14239
|
+
const shouldApplyCurrentInterval = this.currentHiddenIntervalStart !== null && timestamp >= this.currentHiddenIntervalStart;
|
|
13630
14240
|
let shouldApply = false;
|
|
13631
|
-
if (
|
|
14241
|
+
if (shouldApplyCurrentInterval) {
|
|
13632
14242
|
shouldApply = true;
|
|
13633
14243
|
}
|
|
13634
|
-
if (
|
|
13635
|
-
shouldApply =
|
|
14244
|
+
if (!shouldApplyCurrentInterval) {
|
|
14245
|
+
shouldApply = this.shouldApplyCompletedIntervalOverlay(timestamp);
|
|
13636
14246
|
}
|
|
13637
|
-
if (
|
|
14247
|
+
if (frameCount % OVERLAY_LOG_FRAME_INTERVAL === 0) {
|
|
13638
14248
|
this.logger.debug(\`\${RECORDER_WORKER_LOG_PREFIX3} Overlay check\`, {
|
|
13639
|
-
timestamp
|
|
14249
|
+
timestamp,
|
|
13640
14250
|
shouldApply,
|
|
13641
|
-
frameCount
|
|
13642
|
-
intervalsCount: this.hiddenIntervals.length
|
|
14251
|
+
frameCount,
|
|
14252
|
+
intervalsCount: this.hiddenIntervals.length,
|
|
14253
|
+
intervalCursor: this.intervalCursor
|
|
13643
14254
|
});
|
|
13644
14255
|
}
|
|
13645
14256
|
return shouldApply;
|
|
13646
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
|
+
}
|
|
13647
14281
|
handleUpdateVisibility(isHidden, timestamp, hasBaseVideoTimestamp, pausedDuration) {
|
|
13648
14282
|
if (!hasBaseVideoTimestamp) {
|
|
13649
14283
|
this.pendingVisibilityUpdates = [
|
|
@@ -13707,6 +14341,11 @@ var ERROR_AUDIO_CHANNELS_INVALID = "Audio channels must be greater than zero";
|
|
|
13707
14341
|
var ERROR_AUDIO_FRAMES_INVALID = "Audio frames must be greater than zero";
|
|
13708
14342
|
var STEREO_CHANNEL_COUNT = 2;
|
|
13709
14343
|
var AUDIO_SAMPLE_AVERAGE_SCALE = 0.5;
|
|
14344
|
+
var STOP_PENDING_WRITES_TIMEOUT_MILLISECONDS2 = 500;
|
|
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";
|
|
13710
14349
|
|
|
13711
14350
|
class RecorderWorker {
|
|
13712
14351
|
output = null;
|
|
@@ -13729,6 +14368,7 @@ class RecorderWorker {
|
|
|
13729
14368
|
totalSize = 0;
|
|
13730
14369
|
expectedAudioChannels = null;
|
|
13731
14370
|
expectedAudioSampleRate = null;
|
|
14371
|
+
pendingWriteCount = 0;
|
|
13732
14372
|
constructor() {
|
|
13733
14373
|
this.bufferTracker = new BufferTracker({
|
|
13734
14374
|
getBufferSize: () => this.totalSize,
|
|
@@ -13759,14 +14399,6 @@ class RecorderWorker {
|
|
|
13759
14399
|
},
|
|
13760
14400
|
getNowMilliseconds: () => performance.now()
|
|
13761
14401
|
});
|
|
13762
|
-
const sendDebugLog = (message, payload) => {
|
|
13763
|
-
const response = {
|
|
13764
|
-
type: WORKER_RESPONSE_TYPE_DEBUG_LOG,
|
|
13765
|
-
message,
|
|
13766
|
-
payload
|
|
13767
|
-
};
|
|
13768
|
-
self.postMessage(response);
|
|
13769
|
-
};
|
|
13770
14402
|
this.frameCompositor = new FrameCompositor({
|
|
13771
14403
|
logger: {
|
|
13772
14404
|
debug: (message, data) => logger.debug(message, data),
|
|
@@ -13775,7 +14407,9 @@ class RecorderWorker {
|
|
|
13775
14407
|
},
|
|
13776
14408
|
fetchResource: (input, init) => fetch(input, init),
|
|
13777
14409
|
createImageBitmap: (image) => createImageBitmap(image),
|
|
13778
|
-
sendDebugLog
|
|
14410
|
+
sendDebugLog: (_message, _payload) => {
|
|
14411
|
+
return;
|
|
14412
|
+
}
|
|
13779
14413
|
});
|
|
13780
14414
|
self.addEventListener("message", this.handleMessage);
|
|
13781
14415
|
}
|
|
@@ -13909,6 +14543,7 @@ class RecorderWorker {
|
|
|
13909
14543
|
this.audioState.reset();
|
|
13910
14544
|
this.expectedAudioChannels = null;
|
|
13911
14545
|
this.expectedAudioSampleRate = null;
|
|
14546
|
+
this.pendingWriteCount = 0;
|
|
13912
14547
|
this.videoProcessingActive = false;
|
|
13913
14548
|
this.frameCompositor.reset();
|
|
13914
14549
|
this.recordingStartTime = 0;
|
|
@@ -13939,13 +14574,11 @@ class RecorderWorker {
|
|
|
13939
14574
|
}
|
|
13940
14575
|
createOutput() {
|
|
13941
14576
|
const writable = new WritableStream({
|
|
13942
|
-
write: (chunk) =>
|
|
13943
|
-
this.sendChunk(chunk.data, chunk.position);
|
|
13944
|
-
}
|
|
14577
|
+
write: (chunk) => this.handleOutputChunkWrite(chunk)
|
|
13945
14578
|
});
|
|
13946
14579
|
this.output = new Output({
|
|
13947
14580
|
format: new Mp4OutputFormat({
|
|
13948
|
-
fastStart:
|
|
14581
|
+
fastStart: MP4_FAST_START_DISABLED
|
|
13949
14582
|
}),
|
|
13950
14583
|
target: new StreamTarget(writable, {
|
|
13951
14584
|
chunked: true,
|
|
@@ -13953,6 +14586,24 @@ class RecorderWorker {
|
|
|
13953
14586
|
})
|
|
13954
14587
|
});
|
|
13955
14588
|
}
|
|
14589
|
+
decrementPendingWriteCount() {
|
|
14590
|
+
this.pendingWriteCount -= 1;
|
|
14591
|
+
if (this.pendingWriteCount < 0) {
|
|
14592
|
+
this.pendingWriteCount = 0;
|
|
14593
|
+
}
|
|
14594
|
+
}
|
|
14595
|
+
handleOutputChunkWrite(chunk) {
|
|
14596
|
+
this.pendingWriteCount += 1;
|
|
14597
|
+
const writeOperation = Promise.resolve().then(() => {
|
|
14598
|
+
this.sendChunk(chunk.data, chunk.position);
|
|
14599
|
+
});
|
|
14600
|
+
return writeOperation.then(() => {
|
|
14601
|
+
this.decrementPendingWriteCount();
|
|
14602
|
+
}, (error) => {
|
|
14603
|
+
this.decrementPendingWriteCount();
|
|
14604
|
+
throw error;
|
|
14605
|
+
});
|
|
14606
|
+
}
|
|
13956
14607
|
createVideoSource(config) {
|
|
13957
14608
|
const fps = this.timestampManager.getFrameRate();
|
|
13958
14609
|
const keyFrameIntervalSeconds = config.keyFrameInterval;
|
|
@@ -13963,9 +14614,9 @@ class RecorderWorker {
|
|
|
13963
14614
|
sizeChangeBehavior: "contain",
|
|
13964
14615
|
alpha: "discard",
|
|
13965
14616
|
bitrateMode: "variable",
|
|
13966
|
-
latencyMode:
|
|
13967
|
-
contentHint:
|
|
13968
|
-
hardwareAcceleration:
|
|
14617
|
+
latencyMode: VIDEO_LATENCY_MODE_REALTIME,
|
|
14618
|
+
contentHint: VIDEO_CONTENT_HINT_MOTION,
|
|
14619
|
+
hardwareAcceleration: VIDEO_HARDWARE_ACCELERATION_PREFERENCE,
|
|
13969
14620
|
keyFrameInterval: keyFrameIntervalSeconds,
|
|
13970
14621
|
bitrate: this.deserializeBitrate(config.bitrate)
|
|
13971
14622
|
};
|
|
@@ -14146,18 +14797,14 @@ class RecorderWorker {
|
|
|
14146
14797
|
},
|
|
14147
14798
|
isScreenCapture: this.isScreenCapture
|
|
14148
14799
|
});
|
|
14149
|
-
|
|
14150
|
-
|
|
14151
|
-
|
|
14152
|
-
|
|
14153
|
-
|
|
14154
|
-
timestamp: frameTimestamp,
|
|
14155
|
-
overlayEnabled,
|
|
14156
|
-
frameCount: this.timestampManager.getFrameCount()
|
|
14157
|
-
});
|
|
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
|
+
}
|
|
14158
14805
|
const compositionResult = this.frameCompositor.composeFrame({
|
|
14159
14806
|
videoFrame,
|
|
14160
|
-
overlayConfig
|
|
14807
|
+
overlayConfig,
|
|
14161
14808
|
shouldApplyOverlay,
|
|
14162
14809
|
config
|
|
14163
14810
|
});
|
|
@@ -14442,19 +15089,42 @@ class RecorderWorker {
|
|
|
14442
15089
|
}
|
|
14443
15090
|
this.sendStateChange("recording");
|
|
14444
15091
|
}
|
|
14445
|
-
|
|
15092
|
+
handleStop() {
|
|
14446
15093
|
if (this.isStopping) {
|
|
14447
15094
|
logger.debug("[RecorderWorker] handleStop ignored (stopping/finalized)");
|
|
14448
|
-
return;
|
|
15095
|
+
return Promise.resolve();
|
|
14449
15096
|
}
|
|
14450
15097
|
if (this.isFinalized) {
|
|
14451
15098
|
logger.debug("[RecorderWorker] handleStop ignored (stopping/finalized)");
|
|
14452
|
-
return;
|
|
15099
|
+
return Promise.resolve();
|
|
14453
15100
|
}
|
|
14454
15101
|
this.isStopping = true;
|
|
14455
15102
|
this.isFinalized = true;
|
|
14456
15103
|
this.videoProcessingActive = false;
|
|
14457
15104
|
this.audioState.setProcessingActive(false);
|
|
15105
|
+
return runStopTransition({
|
|
15106
|
+
finalizeStopSequence: () => this.finalizeStopSequence(),
|
|
15107
|
+
completeStop: () => this.completeStop(),
|
|
15108
|
+
recoverStopFailure: () => {
|
|
15109
|
+
if (this.isFinalized) {
|
|
15110
|
+
this.resetStopStateAfterFailure();
|
|
15111
|
+
}
|
|
15112
|
+
return this.cleanup().catch((cleanupError) => {
|
|
15113
|
+
logger.error("[RecorderWorker] Stop failure cleanup failed", {
|
|
15114
|
+
error: extractErrorMessage(cleanupError)
|
|
15115
|
+
});
|
|
15116
|
+
});
|
|
15117
|
+
},
|
|
15118
|
+
clearStoppingFlag: () => {
|
|
15119
|
+
this.isStopping = false;
|
|
15120
|
+
}
|
|
15121
|
+
});
|
|
15122
|
+
}
|
|
15123
|
+
async completeStop() {
|
|
15124
|
+
await this.cleanup();
|
|
15125
|
+
this.sendStateChange("stopped");
|
|
15126
|
+
}
|
|
15127
|
+
async finalizeStopSequence() {
|
|
14458
15128
|
if (this.videoProcessor) {
|
|
14459
15129
|
await this.videoProcessor.cancel();
|
|
14460
15130
|
this.videoProcessor = null;
|
|
@@ -14464,13 +15134,17 @@ class RecorderWorker {
|
|
|
14464
15134
|
this.audioProcessor = null;
|
|
14465
15135
|
}
|
|
14466
15136
|
if (this.output) {
|
|
14467
|
-
await this.output.finalize()
|
|
14468
|
-
logger.warn("[RecorderWorker] finalize failed (ignored, already finalized?)", error);
|
|
14469
|
-
});
|
|
15137
|
+
await this.output.finalize();
|
|
14470
15138
|
}
|
|
14471
|
-
await
|
|
14472
|
-
|
|
14473
|
-
|
|
15139
|
+
await waitForPendingWritesToDrain({
|
|
15140
|
+
getPendingWriteCount: () => this.pendingWriteCount,
|
|
15141
|
+
timeoutMilliseconds: STOP_PENDING_WRITES_TIMEOUT_MILLISECONDS2
|
|
15142
|
+
});
|
|
15143
|
+
}
|
|
15144
|
+
resetStopStateAfterFailure() {
|
|
15145
|
+
this.isFinalized = false;
|
|
15146
|
+
this.videoProcessingActive = false;
|
|
15147
|
+
this.audioState.setProcessingActive(false);
|
|
14474
15148
|
}
|
|
14475
15149
|
handleToggleMute() {
|
|
14476
15150
|
this.audioState.toggleMuted();
|
|
@@ -14586,6 +15260,7 @@ class RecorderWorker {
|
|
|
14586
15260
|
this.isScreenCapture = false;
|
|
14587
15261
|
this.expectedAudioChannels = null;
|
|
14588
15262
|
this.expectedAudioSampleRate = null;
|
|
15263
|
+
this.pendingWriteCount = 0;
|
|
14589
15264
|
this.visibilityTracker.reset(this.recordingStartTime, this.isScreenCapture);
|
|
14590
15265
|
}
|
|
14591
15266
|
setExpectedAudioFormat(sampleRate, numberOfChannels) {
|
|
@@ -14706,8 +15381,50 @@ var KEY_FRAME_INTERVAL_SECONDS = 5;
|
|
|
14706
15381
|
var DEFAULT_SWITCH_SOURCE_FPS = 30;
|
|
14707
15382
|
var WORKER_PROBE_TIMEOUT_MILLISECONDS = 2000;
|
|
14708
15383
|
var FINALIZE_TIMEOUT_MILLISECONDS = 30000;
|
|
14709
|
-
var
|
|
15384
|
+
var MILLISECONDS_PER_SECOND3 = 1000;
|
|
14710
15385
|
var DEFAULT_RECORDING_FORMAT = "mp4";
|
|
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
|
+
}
|
|
14711
15428
|
|
|
14712
15429
|
class WorkerProcessor {
|
|
14713
15430
|
worker = null;
|
|
@@ -14728,6 +15445,7 @@ class WorkerProcessor {
|
|
|
14728
15445
|
workerProbeManager;
|
|
14729
15446
|
canUseMainThreadVideoProcessorFn;
|
|
14730
15447
|
createVideoStreamFromTrackFn;
|
|
15448
|
+
isLinuxPlatformFn;
|
|
14731
15449
|
constructor(dependencies = {}) {
|
|
14732
15450
|
let createWorkerFn = (workerUrl) => new Worker(workerUrl, { type: "classic" });
|
|
14733
15451
|
if (dependencies.createWorker) {
|
|
@@ -14747,8 +15465,13 @@ class WorkerProcessor {
|
|
|
14747
15465
|
if (dependencies.createVideoStreamFromTrack) {
|
|
14748
15466
|
createVideoStreamFromTrackFn = dependencies.createVideoStreamFromTrack;
|
|
14749
15467
|
}
|
|
15468
|
+
let isLinuxPlatformFn = () => isLinuxPlatform();
|
|
15469
|
+
if (dependencies.isLinuxPlatform) {
|
|
15470
|
+
isLinuxPlatformFn = dependencies.isLinuxPlatform;
|
|
15471
|
+
}
|
|
14750
15472
|
this.canUseMainThreadVideoProcessorFn = canUseMainThreadVideoProcessorFn;
|
|
14751
15473
|
this.createVideoStreamFromTrackFn = createVideoStreamFromTrackFn;
|
|
15474
|
+
this.isLinuxPlatformFn = isLinuxPlatformFn;
|
|
14752
15475
|
const hasWorkerFactory = !!dependencies.createWorker;
|
|
14753
15476
|
this.workerProbeManager = new WorkerProbeManager({
|
|
14754
15477
|
setTimeout: window.setTimeout.bind(window),
|
|
@@ -14875,8 +15598,12 @@ class WorkerProcessor {
|
|
|
14875
15598
|
this.resetProcessingState(overlayConfig);
|
|
14876
15599
|
this.stopAudioWorklet();
|
|
14877
15600
|
const format = this.resolveRecordingFormat(config);
|
|
14878
|
-
const
|
|
14879
|
-
|
|
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);
|
|
14880
15607
|
const isScreenCapture = isScreenCaptureStream(stream);
|
|
14881
15608
|
logger.debug("[WorkerProcessor] Starting processing", {
|
|
14882
15609
|
isScreenCapture,
|
|
@@ -14884,7 +15611,7 @@ class WorkerProcessor {
|
|
|
14884
15611
|
codec,
|
|
14885
15612
|
bitrate: config.bitrate
|
|
14886
15613
|
});
|
|
14887
|
-
const workerConfig = this.buildWorkerTranscodeConfig(config, audioCodec, codec, format);
|
|
15614
|
+
const workerConfig = this.buildWorkerTranscodeConfig(config, audioCodec, audioBitrate, codec, format);
|
|
14888
15615
|
const videoTracks = stream.getVideoTracks();
|
|
14889
15616
|
const audioTracks = stream.getAudioTracks();
|
|
14890
15617
|
logger.debug("[WorkerProcessor] Preparing to start processing", {
|
|
@@ -14960,28 +15687,88 @@ class WorkerProcessor {
|
|
|
14960
15687
|
}
|
|
14961
15688
|
return format;
|
|
14962
15689
|
}
|
|
14963
|
-
|
|
14964
|
-
|
|
14965
|
-
|
|
14966
|
-
audioCodec = await detectBestAudioCodec(config.audioBitrate);
|
|
15690
|
+
resolveAudioBitrate(config, format) {
|
|
15691
|
+
if (config.audioBitrate !== undefined) {
|
|
15692
|
+
return config.audioBitrate;
|
|
14967
15693
|
}
|
|
14968
|
-
return
|
|
15694
|
+
return getPresetAudioBitrateForFormat(format);
|
|
14969
15695
|
}
|
|
14970
|
-
async
|
|
14971
|
-
|
|
14972
|
-
|
|
14973
|
-
|
|
14974
|
-
|
|
14975
|
-
|
|
15696
|
+
async resolveAudioCodec(config, format, policy, audioBitrate) {
|
|
15697
|
+
return await resolveAudioCodecFromPolicy({
|
|
15698
|
+
format,
|
|
15699
|
+
overrideCodec: config.audioCodec,
|
|
15700
|
+
policy,
|
|
15701
|
+
bitrate: audioBitrate
|
|
15702
|
+
});
|
|
15703
|
+
}
|
|
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
|
+
});
|
|
14976
15713
|
}
|
|
14977
|
-
|
|
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) {
|
|
14978
15765
|
return {
|
|
14979
15766
|
width: config.width,
|
|
14980
15767
|
height: config.height,
|
|
14981
15768
|
fps: config.fps,
|
|
14982
15769
|
bitrate: serializeBitrate(config.bitrate),
|
|
14983
15770
|
audioCodec,
|
|
14984
|
-
audioBitrate
|
|
15771
|
+
audioBitrate,
|
|
14985
15772
|
codec,
|
|
14986
15773
|
keyFrameInterval: KEY_FRAME_INTERVAL_SECONDS,
|
|
14987
15774
|
format,
|
|
@@ -14990,6 +15777,7 @@ class WorkerProcessor {
|
|
|
14990
15777
|
}
|
|
14991
15778
|
async prepareAudioPipeline(audioTrack, workerProbeResult) {
|
|
14992
15779
|
if (!audioTrack) {
|
|
15780
|
+
logger.debug("[WorkerProcessor] Audio pipeline disabled (no track)");
|
|
14993
15781
|
return {
|
|
14994
15782
|
audioConfig: null,
|
|
14995
15783
|
audioStream: null,
|
|
@@ -15000,6 +15788,10 @@ class WorkerProcessor {
|
|
|
15000
15788
|
if (canUseMainThreadAudioPipeline) {
|
|
15001
15789
|
const audioStream = this.createAudioStreamFromTrack(audioTrack);
|
|
15002
15790
|
if (audioStream) {
|
|
15791
|
+
logger.debug("[WorkerProcessor] Audio pipeline selected", {
|
|
15792
|
+
path: "main-thread-audio-stream",
|
|
15793
|
+
hasAudioDataInWorker: workerProbeResult.hasAudioData
|
|
15794
|
+
});
|
|
15003
15795
|
return {
|
|
15004
15796
|
audioConfig: null,
|
|
15005
15797
|
audioStream,
|
|
@@ -15009,6 +15801,11 @@ class WorkerProcessor {
|
|
|
15009
15801
|
}
|
|
15010
15802
|
const audioConfig = await this.prepareAudioConfig(audioTrack);
|
|
15011
15803
|
if (audioConfig) {
|
|
15804
|
+
logger.debug("[WorkerProcessor] Audio pipeline selected", {
|
|
15805
|
+
path: "audio-worklet-chunks",
|
|
15806
|
+
sampleRate: audioConfig.sampleRate,
|
|
15807
|
+
numberOfChannels: audioConfig.numberOfChannels
|
|
15808
|
+
});
|
|
15012
15809
|
return {
|
|
15013
15810
|
audioConfig,
|
|
15014
15811
|
audioStream: null,
|
|
@@ -15143,42 +15940,88 @@ class WorkerProcessor {
|
|
|
15143
15940
|
if (!this.isWorkerActive()) {
|
|
15144
15941
|
throw new Error("Processing not active");
|
|
15145
15942
|
}
|
|
15943
|
+
const finalizeStartedAtMilliseconds = performance.now();
|
|
15146
15944
|
return new Promise((resolve, reject) => {
|
|
15147
|
-
const
|
|
15148
|
-
|
|
15149
|
-
|
|
15150
|
-
|
|
15151
|
-
|
|
15945
|
+
const worker = this.worker;
|
|
15946
|
+
if (!worker) {
|
|
15947
|
+
reject(new Error("Worker not initialized"));
|
|
15948
|
+
return;
|
|
15949
|
+
}
|
|
15950
|
+
let timeoutId = null;
|
|
15951
|
+
const clearFinalizeTimeout = () => {
|
|
15952
|
+
if (timeoutId === null) {
|
|
15152
15953
|
return;
|
|
15153
15954
|
}
|
|
15154
|
-
|
|
15955
|
+
clearTimeout(timeoutId);
|
|
15956
|
+
timeoutId = null;
|
|
15957
|
+
};
|
|
15958
|
+
const removeWorkerListener = () => {
|
|
15959
|
+
worker.removeEventListener("message", messageHandler);
|
|
15960
|
+
};
|
|
15961
|
+
let hasSettled = false;
|
|
15962
|
+
const settleOnce = () => {
|
|
15963
|
+
if (hasSettled) {
|
|
15964
|
+
return false;
|
|
15965
|
+
}
|
|
15966
|
+
hasSettled = true;
|
|
15967
|
+
clearFinalizeTimeout();
|
|
15968
|
+
removeWorkerListener();
|
|
15969
|
+
return true;
|
|
15155
15970
|
};
|
|
15156
15971
|
const messageHandler = (event) => {
|
|
15972
|
+
if (hasSettled) {
|
|
15973
|
+
return;
|
|
15974
|
+
}
|
|
15157
15975
|
const response = event.data;
|
|
15158
15976
|
const isStopped = response.type === "stateChange" && response.state === "stopped";
|
|
15159
15977
|
if (isStopped) {
|
|
15160
|
-
|
|
15161
|
-
|
|
15162
|
-
|
|
15163
|
-
|
|
15164
|
-
|
|
15978
|
+
const canSettle2 = settleOnce();
|
|
15979
|
+
if (!canSettle2) {
|
|
15980
|
+
return;
|
|
15981
|
+
}
|
|
15982
|
+
this.resetFinalizeRuntimeState();
|
|
15983
|
+
Promise.resolve().then(() => this.createBlobFromChunks()).then((streamProcessorResult) => {
|
|
15984
|
+
resolve(streamProcessorResult);
|
|
15985
|
+
}, (error) => {
|
|
15986
|
+
this.rejectFinalizeBlobCreationError(reject, error, finalizeStartedAtMilliseconds);
|
|
15987
|
+
});
|
|
15165
15988
|
return;
|
|
15166
15989
|
}
|
|
15167
15990
|
const isError = response.type === "error";
|
|
15168
|
-
if (isError) {
|
|
15169
|
-
|
|
15170
|
-
clearTimeout(timeout);
|
|
15171
|
-
this.stopAudioWorklet();
|
|
15172
|
-
reject(new Error(response.error));
|
|
15991
|
+
if (!isError) {
|
|
15992
|
+
return;
|
|
15173
15993
|
}
|
|
15994
|
+
const canSettle = settleOnce();
|
|
15995
|
+
if (!canSettle) {
|
|
15996
|
+
return;
|
|
15997
|
+
}
|
|
15998
|
+
this.resetFinalizeRuntimeState();
|
|
15999
|
+
logger.error("[WorkerProcessor] Finalize failed", {
|
|
16000
|
+
elapsedSeconds: (performance.now() - finalizeStartedAtMilliseconds) / MILLISECONDS_PER_SECOND3,
|
|
16001
|
+
error: response.error
|
|
16002
|
+
});
|
|
16003
|
+
reject(new Error(response.error));
|
|
15174
16004
|
};
|
|
15175
|
-
|
|
15176
|
-
|
|
15177
|
-
|
|
15178
|
-
|
|
15179
|
-
|
|
16005
|
+
timeoutId = setTimeout(() => {
|
|
16006
|
+
const canSettle = settleOnce();
|
|
16007
|
+
if (!canSettle) {
|
|
16008
|
+
return;
|
|
16009
|
+
}
|
|
16010
|
+
logger.error("[WorkerProcessor] Finalize timeout reached", {
|
|
16011
|
+
elapsedSeconds: (performance.now() - finalizeStartedAtMilliseconds) / MILLISECONDS_PER_SECOND3
|
|
16012
|
+
});
|
|
16013
|
+
this.resetFinalizeRuntimeState();
|
|
16014
|
+
reject(new Error("Finalize timeout"));
|
|
16015
|
+
}, FINALIZE_TIMEOUT_MILLISECONDS);
|
|
16016
|
+
worker.addEventListener("message", messageHandler);
|
|
16017
|
+
const message = { type: "stop" };
|
|
16018
|
+
worker.postMessage(message);
|
|
15180
16019
|
});
|
|
15181
16020
|
}
|
|
16021
|
+
resetFinalizeRuntimeState() {
|
|
16022
|
+
this.isActive = false;
|
|
16023
|
+
this.stopAudioWorklet();
|
|
16024
|
+
}
|
|
15182
16025
|
createBlobFromChunks() {
|
|
15183
16026
|
const sortedChunks = [...this.chunks].sort((a, b) => a.position - b.position);
|
|
15184
16027
|
const buffer = new ArrayBuffer(this.totalSize);
|
|
@@ -15186,12 +16029,24 @@ class WorkerProcessor {
|
|
|
15186
16029
|
for (const chunk of sortedChunks) {
|
|
15187
16030
|
view.set(chunk.data, chunk.position);
|
|
15188
16031
|
}
|
|
16032
|
+
assertMp4ContainerIsNonFragmented(buffer);
|
|
15189
16033
|
const blob = new Blob([buffer], { type: "video/mp4" });
|
|
15190
16034
|
return {
|
|
15191
16035
|
blob,
|
|
15192
16036
|
totalSize: this.totalSize
|
|
15193
16037
|
};
|
|
15194
16038
|
}
|
|
16039
|
+
rejectFinalizeBlobCreationError(reject, error, finalizeStartedAtMilliseconds) {
|
|
16040
|
+
logger.error("[WorkerProcessor] Finalize failed while creating blob", {
|
|
16041
|
+
elapsedSeconds: (performance.now() - finalizeStartedAtMilliseconds) / MILLISECONDS_PER_SECOND3,
|
|
16042
|
+
error: extractErrorMessage(error)
|
|
16043
|
+
});
|
|
16044
|
+
if (error instanceof Error) {
|
|
16045
|
+
reject(error);
|
|
16046
|
+
return;
|
|
16047
|
+
}
|
|
16048
|
+
reject(new Error(extractErrorMessage(error)));
|
|
16049
|
+
}
|
|
15195
16050
|
cancel() {
|
|
15196
16051
|
if (this.worker && this.isActive) {
|
|
15197
16052
|
const message = { type: "stop" };
|
|
@@ -15221,7 +16076,7 @@ class WorkerProcessor {
|
|
|
15221
16076
|
logger.debug("[WorkerProcessor] Sending visibility update to worker", {
|
|
15222
16077
|
isHidden,
|
|
15223
16078
|
timestamp,
|
|
15224
|
-
timestampSeconds: timestamp /
|
|
16079
|
+
timestampSeconds: timestamp / MILLISECONDS_PER_SECOND3
|
|
15225
16080
|
});
|
|
15226
16081
|
const message = {
|
|
15227
16082
|
type: "updateVisibility",
|
|
@@ -15525,7 +16380,7 @@ class StreamProcessor {
|
|
|
15525
16380
|
|
|
15526
16381
|
// src/core/recording/recording-manager.ts
|
|
15527
16382
|
var DEFAULT_COUNTDOWN_DURATION = 5000;
|
|
15528
|
-
var
|
|
16383
|
+
var MILLISECONDS_PER_SECOND4 = 1000;
|
|
15529
16384
|
var COUNTDOWN_UPDATE_INTERVAL = 100;
|
|
15530
16385
|
var RECORDING_TIMER_INTERVAL = 1000;
|
|
15531
16386
|
var RECORDING_STATE_RECORDING = "recording";
|
|
@@ -15592,6 +16447,9 @@ class RecordingManager {
|
|
|
15592
16447
|
getOriginalCameraStream() {
|
|
15593
16448
|
return this.originalCameraStream;
|
|
15594
16449
|
}
|
|
16450
|
+
prewarmStreamProcessor() {
|
|
16451
|
+
this.getOrCreateStreamProcessor();
|
|
16452
|
+
}
|
|
15595
16453
|
async startRecording() {
|
|
15596
16454
|
try {
|
|
15597
16455
|
this.callbacks.onClearUploadStatus();
|
|
@@ -15608,7 +16466,7 @@ class RecordingManager {
|
|
|
15608
16466
|
}
|
|
15609
16467
|
startCountdown() {
|
|
15610
16468
|
this.recordingState = RECORDING_STATE_COUNTDOWN;
|
|
15611
|
-
this.countdownRemaining = Math.ceil(this.countdownDuration /
|
|
16469
|
+
this.countdownRemaining = Math.ceil(this.countdownDuration / MILLISECONDS_PER_SECOND4);
|
|
15612
16470
|
this.countdownStartTime = Date.now();
|
|
15613
16471
|
this.callbacks.onCountdownUpdate(this.recordingState, this.countdownRemaining);
|
|
15614
16472
|
this.countdownIntervalId = window.setInterval(() => {
|
|
@@ -15616,7 +16474,7 @@ class RecordingManager {
|
|
|
15616
16474
|
return;
|
|
15617
16475
|
}
|
|
15618
16476
|
const elapsed = Date.now() - this.countdownStartTime;
|
|
15619
|
-
const remaining = Math.max(0, Math.ceil((this.countdownDuration - elapsed) /
|
|
16477
|
+
const remaining = Math.max(0, Math.ceil((this.countdownDuration - elapsed) / MILLISECONDS_PER_SECOND4));
|
|
15620
16478
|
this.countdownRemaining = remaining;
|
|
15621
16479
|
this.callbacks.onCountdownUpdate(this.recordingState, this.countdownRemaining);
|
|
15622
16480
|
}, COUNTDOWN_UPDATE_INTERVAL);
|
|
@@ -15627,8 +16485,6 @@ class RecordingManager {
|
|
|
15627
16485
|
async doStartRecording() {
|
|
15628
16486
|
logger.debug("[RecordingManager] doStartRecording called");
|
|
15629
16487
|
this.cancelCountdown();
|
|
15630
|
-
this.recordingState = RECORDING_STATE_RECORDING;
|
|
15631
|
-
this.callbacks.onStateChange(this.recordingState);
|
|
15632
16488
|
this.resetRecordingState();
|
|
15633
16489
|
const currentStream = this.streamManager.getStream();
|
|
15634
16490
|
logger.debug("[RecordingManager] Current stream:", {
|
|
@@ -15644,24 +16500,30 @@ class RecordingManager {
|
|
|
15644
16500
|
return;
|
|
15645
16501
|
}
|
|
15646
16502
|
this.originalCameraStream = currentStream;
|
|
15647
|
-
logger.debug("[RecordingManager]
|
|
15648
|
-
|
|
15649
|
-
logger.debug("[RecordingManager] StreamProcessor
|
|
15650
|
-
|
|
15651
|
-
|
|
15652
|
-
|
|
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);
|
|
15653
16515
|
this.recordingState = RECORDING_STATE_IDLE;
|
|
15654
16516
|
this.callbacks.onStateChange(this.recordingState);
|
|
15655
16517
|
return;
|
|
15656
16518
|
}
|
|
15657
|
-
if (!
|
|
16519
|
+
if (!recordingConfig) {
|
|
15658
16520
|
this.handleError(new Error("Failed to get recording config"));
|
|
15659
16521
|
this.recordingState = RECORDING_STATE_IDLE;
|
|
15660
16522
|
this.callbacks.onStateChange(this.recordingState);
|
|
15661
16523
|
return;
|
|
15662
16524
|
}
|
|
15663
16525
|
logger.debug("[RecordingManager] Starting recording with stream manager");
|
|
15664
|
-
const recordingError = await this.streamManager.startRecording(
|
|
16526
|
+
const recordingError = await this.streamManager.startRecording(streamProcessor, recordingConfig, this.enableTabVisibilityOverlay, this.tabVisibilityOverlayText).then(() => {
|
|
15665
16527
|
logger.info("[RecordingManager] Recording started successfully");
|
|
15666
16528
|
return null;
|
|
15667
16529
|
}).catch((error) => {
|
|
@@ -15674,6 +16536,8 @@ class RecordingManager {
|
|
|
15674
16536
|
this.callbacks.onStateChange(this.recordingState);
|
|
15675
16537
|
return;
|
|
15676
16538
|
}
|
|
16539
|
+
this.recordingState = RECORDING_STATE_RECORDING;
|
|
16540
|
+
this.callbacks.onStateChange(this.recordingState);
|
|
15677
16541
|
this.startRecordingTimer();
|
|
15678
16542
|
this.recordingStartTime = Date.now();
|
|
15679
16543
|
if (this.maxRecordingTime && this.maxRecordingTime > 0) {
|
|
@@ -15752,6 +16616,12 @@ class RecordingManager {
|
|
|
15752
16616
|
this.recordingIntervalId = null;
|
|
15753
16617
|
this.clearTimer(this.maxTimeTimer, clearTimeout);
|
|
15754
16618
|
this.maxTimeTimer = null;
|
|
16619
|
+
if (this.streamProcessor) {
|
|
16620
|
+
this.streamProcessor.cancel().catch(() => {
|
|
16621
|
+
return;
|
|
16622
|
+
});
|
|
16623
|
+
this.streamProcessor = null;
|
|
16624
|
+
}
|
|
15755
16625
|
}
|
|
15756
16626
|
resetRecordingState() {
|
|
15757
16627
|
this.isPaused = false;
|
|
@@ -15766,6 +16636,14 @@ class RecordingManager {
|
|
|
15766
16636
|
this.pauseStartTime = null;
|
|
15767
16637
|
this.totalPausedTime = 0;
|
|
15768
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
|
+
}
|
|
15769
16647
|
updatePausedDuration() {
|
|
15770
16648
|
if (this.pauseStartTime === null) {
|
|
15771
16649
|
throw new Error("Pause start time not set");
|
|
@@ -15896,6 +16774,7 @@ class UploadMetadataManager {
|
|
|
15896
16774
|
|
|
15897
16775
|
// src/core/recording/recorder-controller.ts
|
|
15898
16776
|
var LOGGER_PREFIX = "[RecorderController]";
|
|
16777
|
+
var RECORDING_WARMUP_DELAY_MILLISECONDS = 0;
|
|
15899
16778
|
|
|
15900
16779
|
class RecorderController {
|
|
15901
16780
|
streamManager;
|
|
@@ -15913,8 +16792,10 @@ class RecorderController {
|
|
|
15913
16792
|
uploadQueueManager = null;
|
|
15914
16793
|
isInitialized = false;
|
|
15915
16794
|
isDemo = false;
|
|
16795
|
+
isDestroyed = false;
|
|
15916
16796
|
enableTabVisibilityOverlay = false;
|
|
15917
16797
|
tabVisibilityOverlayText;
|
|
16798
|
+
recordingWarmupTimeoutId = null;
|
|
15918
16799
|
constructor(callbacks = {}) {
|
|
15919
16800
|
this.callbacks = callbacks;
|
|
15920
16801
|
this.streamManager = new CameraStreamManager;
|
|
@@ -15930,7 +16811,7 @@ class RecorderController {
|
|
|
15930
16811
|
this.uploadMetadataManager = new UploadMetadataManager;
|
|
15931
16812
|
const recordingCallbacks = createRecordingCallbacks(callbacks, {
|
|
15932
16813
|
stopAudioTracking: () => this.audioLevelAnalyzer.stopTracking(),
|
|
15933
|
-
getConfig: () => this.configManager.
|
|
16814
|
+
getConfig: () => Promise.resolve(this.configManager.getConfigForRecording())
|
|
15934
16815
|
});
|
|
15935
16816
|
this.recordingManager = new RecordingManager(this.streamManager, recordingCallbacks);
|
|
15936
16817
|
const sourceSwitchCallbacks = createSourceSwitchCallbacks(callbacks, {
|
|
@@ -15995,6 +16876,7 @@ class RecorderController {
|
|
|
15995
16876
|
this.applyRecordingConfig(config);
|
|
15996
16877
|
await this.initializeStorage();
|
|
15997
16878
|
this.isInitialized = true;
|
|
16879
|
+
this.scheduleRecordingWarmup();
|
|
15998
16880
|
}
|
|
15999
16881
|
});
|
|
16000
16882
|
}
|
|
@@ -16006,6 +16888,8 @@ class RecorderController {
|
|
|
16006
16888
|
action: async () => {
|
|
16007
16889
|
logger.debug(`${LOGGER_PREFIX} startStream called`);
|
|
16008
16890
|
await this.streamManager.startStream();
|
|
16891
|
+
this.ignorePromiseRejection(this.ensureConfigReady());
|
|
16892
|
+
this.recordingManager.prewarmStreamProcessor();
|
|
16009
16893
|
logger.debug(`${LOGGER_PREFIX} startStream completed`);
|
|
16010
16894
|
},
|
|
16011
16895
|
properties: {
|
|
@@ -16023,13 +16907,13 @@ class RecorderController {
|
|
|
16023
16907
|
return this.streamManager.switchAudioDevice(deviceId);
|
|
16024
16908
|
}
|
|
16025
16909
|
async startRecording() {
|
|
16026
|
-
await this.ensureConfigReady();
|
|
16027
16910
|
const sourceType = this.getCurrentSourceType();
|
|
16028
16911
|
await this.telemetryManager.executeAction({
|
|
16029
16912
|
requestedEvent: "recording.start.requested",
|
|
16030
16913
|
succeededEvent: "recording.start.succeeded",
|
|
16031
16914
|
failedEvent: "recording.start.failed",
|
|
16032
16915
|
action: async () => {
|
|
16916
|
+
await this.ensureConfigReady();
|
|
16033
16917
|
await this.recordingManager.startRecording();
|
|
16034
16918
|
},
|
|
16035
16919
|
properties: {
|
|
@@ -16153,13 +17037,25 @@ class RecorderController {
|
|
|
16153
17037
|
isConfigReady() {
|
|
16154
17038
|
return this.configManager.isConfigReady();
|
|
16155
17039
|
}
|
|
16156
|
-
|
|
17040
|
+
ensureConfigReady() {
|
|
16157
17041
|
if (this.isDemo) {
|
|
16158
|
-
return;
|
|
17042
|
+
return Promise.resolve();
|
|
16159
17043
|
}
|
|
16160
|
-
|
|
17044
|
+
if (this.configManager.isConfigReady()) {
|
|
17045
|
+
return Promise.resolve();
|
|
17046
|
+
}
|
|
17047
|
+
return this.configManager.fetchConfig().then(() => {
|
|
17048
|
+
return;
|
|
17049
|
+
}).catch(() => {
|
|
17050
|
+
return;
|
|
17051
|
+
});
|
|
16161
17052
|
}
|
|
16162
17053
|
cleanup() {
|
|
17054
|
+
this.isDestroyed = true;
|
|
17055
|
+
if (this.recordingWarmupTimeoutId !== null) {
|
|
17056
|
+
clearTimeout(this.recordingWarmupTimeoutId);
|
|
17057
|
+
this.recordingWarmupTimeoutId = null;
|
|
17058
|
+
}
|
|
16163
17059
|
this.storageManager.destroy();
|
|
16164
17060
|
this.recordingManager.cleanup();
|
|
16165
17061
|
this.audioLevelAnalyzer.stopTracking();
|
|
@@ -16279,6 +17175,30 @@ class RecorderController {
|
|
|
16279
17175
|
});
|
|
16280
17176
|
throw error;
|
|
16281
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
|
+
}
|
|
16282
17202
|
}
|
|
16283
17203
|
// src/core/storage/quota-manager.ts
|
|
16284
17204
|
var PERCENTAGE_MULTIPLIER = 100;
|
|
@@ -16704,6 +17624,10 @@ export {
|
|
|
16704
17624
|
mapPresetToConfig,
|
|
16705
17625
|
logger,
|
|
16706
17626
|
isMobileDevice2 as isMobileDevice,
|
|
17627
|
+
getPresetVideoBitrateForFormat,
|
|
17628
|
+
getPresetTotalBitrate,
|
|
17629
|
+
getPresetAudioBitrateForFormat,
|
|
17630
|
+
getFormatCompatibilityPolicy,
|
|
16707
17631
|
getDefaultConfigForFormat,
|
|
16708
17632
|
getDefaultAudioCodecForFormat,
|
|
16709
17633
|
getBrowserName,
|
|
@@ -16716,6 +17640,8 @@ export {
|
|
|
16716
17640
|
extractLastFrame,
|
|
16717
17641
|
extractErrorMessage,
|
|
16718
17642
|
checkRecorderSupport,
|
|
17643
|
+
calculateVideoBitrate,
|
|
17644
|
+
calculateTotalBitrateFromMbPerMinute,
|
|
16719
17645
|
calculateBarColor,
|
|
16720
17646
|
VidtreoRecorder,
|
|
16721
17647
|
VideoUploadService,
|
|
@@ -16726,8 +17652,10 @@ export {
|
|
|
16726
17652
|
RecordingManager,
|
|
16727
17653
|
RecorderController,
|
|
16728
17654
|
QuotaManager,
|
|
17655
|
+
PRESET_SIZE_LIMIT_MB_PER_MINUTE,
|
|
16729
17656
|
NativeCameraHandler,
|
|
16730
17657
|
FORMAT_DEFAULT_CODECS,
|
|
17658
|
+
FORMAT_COMPATIBILITY_POLICY,
|
|
16731
17659
|
DeviceManager,
|
|
16732
17660
|
DEFAULT_TRANSCODE_CONFIG,
|
|
16733
17661
|
DEFAULT_STREAM_CONFIG,
|