@vidtreo/recorder 1.2.1 → 1.3.0

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