@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.
Files changed (4) hide show
  1. package/README.md +5 -0
  2. package/dist/index.d.ts +1746 -1578
  3. package/dist/index.js +1228 -300
  4. 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 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
+ }
249
286
  };
250
- function calculateTotalBitrate(sizeMbPerMinute) {
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.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,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
- 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_HEVC = "hevc";
304
- var VIDEO_CODEC_AVC = "avc";
305
- var AUDIO_CODEC_AAC = "aac";
306
- var AUDIO_CODEC_OPUS = "opus";
307
- var ERROR_NO_SUPPORTED_AUDIO_CODEC = "No supported audio codec found for recording";
308
- var AUDIO_CODEC_CANDIDATES = [
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 (bitrate !== undefined) {
327
- checkOptions = { ...checkOptions, bitrate };
386
+ if (device.type === "tablet") {
387
+ return true;
328
388
  }
329
- return checkOptions;
389
+ return false;
330
390
  }
331
- function buildAudioCodecCheckOptions(bitrate) {
332
- let checkOptions = {};
333
- if (bitrate !== undefined) {
334
- 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 "";
335
401
  }
336
- return checkOptions;
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 createVideoCodecSupportError(error) {
348
- return { kind: "error", error };
404
+ function hasPlatformKeyword(value, keyword) {
405
+ return value.includes(keyword);
349
406
  }
350
- function createAudioCodecLookupResult(codec) {
351
- return { kind: "result", codec };
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
- const { canEncodeVideo } = mediabunnyModuleResult.module;
362
- if (typeof canEncodeVideo !== "function") {
363
- return VIDEO_CODEC_AVC;
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
- const checkOptions = buildVideoCodecCheckOptions(width, height, bitrate);
366
- const videoCodecSupportResult = await canEncodeVideo(VIDEO_CODEC_HEVC, checkOptions).then((supported) => createVideoCodecSupportResult(supported)).catch((error) => createVideoCodecSupportError(error));
367
- if (videoCodecSupportResult.kind === "error") {
368
- return VIDEO_CODEC_AVC;
417
+ return false;
418
+ }
419
+ function resolveNavigatorProvider(navigatorProvider) {
420
+ if (navigatorProvider) {
421
+ return navigatorProvider;
369
422
  }
370
- if (videoCodecSupportResult.supported) {
371
- return VIDEO_CODEC_HEVC;
423
+ if (typeof navigator === "undefined") {
424
+ return null;
372
425
  }
373
- return VIDEO_CODEC_AVC;
426
+ const globalNavigator = navigator;
427
+ return {
428
+ platform: globalNavigator.platform,
429
+ userAgent: globalNavigator.userAgent,
430
+ userAgentData: globalNavigator.userAgentData
431
+ };
374
432
  }
375
- async function detectBestAudioCodec(bitrate, dependencies) {
376
- const mediabunnyModuleResult = await resolveMediabunnyModule(dependencies).then((module) => createMediabunnyModuleResult(module)).catch((error) => createMediabunnyModuleError(error));
377
- if (mediabunnyModuleResult.kind === "error") {
378
- return AUDIO_CODEC_AAC;
433
+ function isLinuxPlatform(navigatorProvider) {
434
+ const resolvedNavigatorProvider = resolveNavigatorProvider(navigatorProvider);
435
+ if (resolvedNavigatorProvider === null) {
436
+ return false;
379
437
  }
380
- const { getFirstEncodableAudioCodec } = mediabunnyModuleResult.module;
381
- if (typeof getFirstEncodableAudioCodec !== "function") {
382
- return AUDIO_CODEC_AAC;
438
+ const userAgent = normalizeValue(resolvedNavigatorProvider.userAgent);
439
+ if (isNonBrowserRuntime(userAgent)) {
440
+ return false;
383
441
  }
384
- const checkOptions = buildAudioCodecCheckOptions(bitrate);
385
- const audioCodecLookupResult = await getFirstEncodableAudioCodec(AUDIO_CODEC_CANDIDATES, checkOptions).then((codec) => createAudioCodecLookupResult(codec)).catch((error) => createAudioCodecLookupError(error));
386
- if (audioCodecLookupResult.kind === "error") {
387
- const errorMessage = extractErrorMessage(audioCodecLookupResult.error);
388
- if (errorMessage === ERROR_NO_SUPPORTED_AUDIO_CODEC) {
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 (audioCodecLookupResult.codec === null) {
394
- throw new Error(ERROR_NO_SUPPORTED_AUDIO_CODEC);
448
+ if (platform.includes(PLATFORM_KEYWORD_LINUX)) {
449
+ return true;
395
450
  }
396
- return audioCodecLookupResult.codec;
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 (device.type === "tablet") {
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
- async function mapPresetToConfig(options) {
461
+ function mapPresetToConfig(options) {
415
462
  const { preset, outputFormat, watermark, isMobile } = options;
416
- if (!(preset in PRESET_VIDEO_BITRATE_MAP)) {
417
- 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;
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
- const format = outputFormat || "mp4";
423
- 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
+ });
424
486
  const config = {
425
487
  format,
426
488
  width,
427
489
  height,
428
- bitrate: PRESET_VIDEO_BITRATE_MAP[preset],
429
- audioCodec,
430
- audioBitrate: DEFAULT_AUDIO_BITRATE
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.configService.fetchConfig().then((config) => {
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: config.format || DEFAULT_TRANSCODE_CONFIG.format
1043
+ format: resolvedFormat
821
1044
  };
822
- if (!finalConfig.audioCodec) {
823
- finalConfig.audioCodec = await detectBestAudioCodec(finalConfig.audioBitrate);
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: config.format || DEFAULT_TRANSCODE_CONFIG.format
1103
+ format: resolvedFormat
860
1104
  };
861
- if (!finalConfig.audioCodec) {
862
- finalConfig.audioCodec = await detectBestAudioCodec(finalConfig.audioBitrate);
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 = 1;
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 = indexedDB.open(DB_NAME, DB_VERSION);
1716
+ const request = this.createOpenRequest(databaseVersion);
1362
1717
  request.onerror = () => {
1363
- if (request.error) {
1364
- reject(request.error);
1365
- } else {
1366
- reject(new Error("Failed to open database"));
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
- this.db = request.result;
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
- if (!db.objectStoreNames.contains(STORE_NAME)) {
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
- constructor() {
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 = Date.now();
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 = Date.now() - this.pauseStartTime;
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 = Date.now();
3383
+ this.currentIntervalStart = this.getCurrentTimestamp();
2979
3384
  }
2980
3385
  endCurrentIntervalIfActive() {
2981
3386
  if (this.currentIntervalStart === null) {
2982
3387
  return;
2983
3388
  }
2984
- const endTime = Date.now();
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) / 1000;
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 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
+ };
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
- constructor(streamManager) {
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: mediaStream?.getAudioTracks().length || 0
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
- const overlayConfig = enableTabVisibilityOverlay && tabVisibilityOverlayText ? {
3082
- enabled: true,
3083
- text: tabVisibilityOverlayText,
3084
- recordingStartTime: this.recordingStartTime
3085
- } : 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
+ }
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 = Date.now();
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 = Date.now() - this.pauseStartTime;
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 = (Date.now() - this.recordingStartTime - this.totalPausedTime) / MILLISECONDS_PER_SECOND;
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 = performance.now();
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 = performance.now();
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 = performance.now();
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 = performance.now();
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 = performance.now();
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.2.0",
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(parameters) {
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 completedIntervalMatch = this.hiddenIntervals.some((interval) => parameters.timestamp >= interval.start && parameters.timestamp <= interval.end);
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 (completedIntervalMatch) {
14241
+ if (shouldApplyCurrentInterval) {
13632
14242
  shouldApply = true;
13633
14243
  }
13634
- if (currentIntervalMatch) {
13635
- shouldApply = true;
14244
+ if (!shouldApplyCurrentInterval) {
14245
+ shouldApply = this.shouldApplyCompletedIntervalOverlay(timestamp);
13636
14246
  }
13637
- if (parameters.frameCount % OVERLAY_LOG_FRAME_INTERVAL === 0) {
14247
+ if (frameCount % OVERLAY_LOG_FRAME_INTERVAL === 0) {
13638
14248
  this.logger.debug(\`\${RECORDER_WORKER_LOG_PREFIX3} Overlay check\`, {
13639
- timestamp: parameters.timestamp,
14249
+ timestamp,
13640
14250
  shouldApply,
13641
- frameCount: parameters.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: "fragmented"
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: "quality",
13967
- contentHint: "detail",
13968
- hardwareAcceleration: "no-preference",
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
- let overlayEnabled = false;
14150
- if (this.overlayConfig?.enabled) {
14151
- overlayEnabled = true;
14152
- }
14153
- const shouldApplyOverlay = this.visibilityTracker.shouldApplyOverlay({
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: this.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
- async handleStop() {
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().catch((error) => {
14468
- logger.warn("[RecorderWorker] finalize failed (ignored, already finalized?)", error);
14469
- });
15137
+ await this.output.finalize();
14470
15138
  }
14471
- await this.cleanup();
14472
- this.sendStateChange("stopped");
14473
- this.isStopping = false;
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 MILLISECONDS_PER_SECOND2 = 1000;
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 audioCodec = await this.resolveAudioCodec(config);
14879
- const codec = await this.resolveVideoCodec(config);
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
- async resolveAudioCodec(config) {
14964
- let audioCodec = config.audioCodec;
14965
- if (!audioCodec) {
14966
- audioCodec = await detectBestAudioCodec(config.audioBitrate);
15690
+ resolveAudioBitrate(config, format) {
15691
+ if (config.audioBitrate !== undefined) {
15692
+ return config.audioBitrate;
14967
15693
  }
14968
- return audioCodec;
15694
+ return getPresetAudioBitrateForFormat(format);
14969
15695
  }
14970
- async resolveVideoCodec(config) {
14971
- let codec = config.codec;
14972
- if (!codec) {
14973
- codec = await detectBestCodec(config.width, config.height, config.bitrate);
14974
- }
14975
- return codec;
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
- buildWorkerTranscodeConfig(config, audioCodec, codec, format) {
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: config.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 timeout = setTimeout(() => {
15148
- reject(new Error("Finalize timeout"));
15149
- }, FINALIZE_TIMEOUT_MILLISECONDS);
15150
- const removeWorkerListener = () => {
15151
- if (!this.worker) {
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
- this.worker.removeEventListener("message", messageHandler);
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
- removeWorkerListener();
15161
- clearTimeout(timeout);
15162
- this.isActive = false;
15163
- this.stopAudioWorklet();
15164
- resolve(this.createBlobFromChunks());
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
- removeWorkerListener();
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
- if (this.worker) {
15176
- this.worker.addEventListener("message", messageHandler);
15177
- const message = { type: "stop" };
15178
- this.worker.postMessage(message);
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 / MILLISECONDS_PER_SECOND2
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 MILLISECONDS_PER_SECOND3 = 1000;
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 / MILLISECONDS_PER_SECOND3);
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) / MILLISECONDS_PER_SECOND3));
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] Creating stream processor");
15648
- this.streamProcessor = new StreamProcessor;
15649
- logger.debug("[RecordingManager] StreamProcessor created:", !!this.streamProcessor);
15650
- const configResult = await this.callbacks.onGetConfig().then((config) => ({ config, error: null })).catch((error) => ({ config: null, error }));
15651
- if (configResult.error) {
15652
- 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);
15653
16515
  this.recordingState = RECORDING_STATE_IDLE;
15654
16516
  this.callbacks.onStateChange(this.recordingState);
15655
16517
  return;
15656
16518
  }
15657
- if (!configResult.config) {
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(this.streamProcessor, configResult.config, this.enableTabVisibilityOverlay, this.tabVisibilityOverlayText).then(() => {
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.getConfig()
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
- async ensureConfigReady() {
17040
+ ensureConfigReady() {
16157
17041
  if (this.isDemo) {
16158
- return;
17042
+ return Promise.resolve();
16159
17043
  }
16160
- 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
+ });
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,