@vidtreo/recorder 1.8.1 → 1.9.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 (3) hide show
  1. package/dist/index.d.ts +252 -13
  2. package/dist/index.js +2987 -354
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -139,6 +139,460 @@ class AudioLevelAnalyzer {
139
139
  return isMutedFromCallback || hasDisabledTracks;
140
140
  }
141
141
  }
142
+ // src/core/audio-recording/audio-file-transcoder.ts
143
+ import {
144
+ ALL_FORMATS,
145
+ AudioSampleSink,
146
+ BlobSource,
147
+ BufferTarget,
148
+ Conversion,
149
+ canEncodeAudio,
150
+ Input,
151
+ Mp4OutputFormat,
152
+ Output,
153
+ WebMOutputFormat
154
+ } from "mediabunny";
155
+
156
+ // src/core/recording/recording-filename.ts
157
+ var MAX_TITLE_LENGTH = 120;
158
+ var CONTROL_CHARACTERS = /\p{Cc}/gu;
159
+ var FORBIDDEN_CHARACTERS = /[/\\*?"<>|]/g;
160
+ var WHITESPACE = /\s+/g;
161
+ var LEADING_DOTS = /^[.\s]+/;
162
+ function isHighSurrogate(code) {
163
+ return code >= 55296 && code <= 56319;
164
+ }
165
+ function truncateTitle(title) {
166
+ if (title.length <= MAX_TITLE_LENGTH) {
167
+ return title;
168
+ }
169
+ let truncated = title.slice(0, MAX_TITLE_LENGTH);
170
+ if (isHighSurrogate(truncated.charCodeAt(truncated.length - 1))) {
171
+ truncated = truncated.slice(0, -1);
172
+ }
173
+ return truncated.trimEnd();
174
+ }
175
+ function sanitizeRecordingTitle(title) {
176
+ const cleaned = title.replace(CONTROL_CHARACTERS, " ").replace(FORBIDDEN_CHARACTERS, "").replace(WHITESPACE, " ").replace(LEADING_DOTS, "").trim();
177
+ return truncateTitle(cleaned);
178
+ }
179
+ function createRecordingFilename(title, extension) {
180
+ const sanitized = title === null ? "" : sanitizeRecordingTitle(title);
181
+ if (sanitized === "") {
182
+ return `recording-${Date.now()}.${extension}`;
183
+ }
184
+ return `${sanitized}.${extension}`;
185
+ }
186
+
187
+ // src/core/utils/platform-detection.ts
188
+ var PLATFORM_KEYWORD_LINUX = "linux";
189
+ var PLATFORM_KEYWORD_ANDROID = "android";
190
+ var RUNTIME_MARKER_BUN = "bun/";
191
+ var RUNTIME_MARKER_HAPPY_DOM = "happydom";
192
+ var RUNTIME_MARKER_NODE = "node.js";
193
+ function normalizeValue(value) {
194
+ if (value === undefined) {
195
+ return "";
196
+ }
197
+ return value.toLowerCase();
198
+ }
199
+ function hasPlatformKeyword(value, keyword) {
200
+ return value.includes(keyword);
201
+ }
202
+ function isNonBrowserRuntime(userAgent) {
203
+ if (userAgent.includes(RUNTIME_MARKER_BUN)) {
204
+ return true;
205
+ }
206
+ if (userAgent.includes(RUNTIME_MARKER_HAPPY_DOM)) {
207
+ return true;
208
+ }
209
+ if (userAgent.includes(RUNTIME_MARKER_NODE)) {
210
+ return true;
211
+ }
212
+ return false;
213
+ }
214
+ function resolveNavigatorProvider(navigatorProvider) {
215
+ if (navigatorProvider) {
216
+ return navigatorProvider;
217
+ }
218
+ if (typeof navigator === "undefined") {
219
+ return null;
220
+ }
221
+ const globalNavigator = navigator;
222
+ return {
223
+ platform: globalNavigator.platform,
224
+ userAgent: globalNavigator.userAgent,
225
+ userAgentData: globalNavigator.userAgentData
226
+ };
227
+ }
228
+ function isLinuxPlatform(navigatorProvider) {
229
+ const resolvedNavigatorProvider = resolveNavigatorProvider(navigatorProvider);
230
+ if (resolvedNavigatorProvider === null) {
231
+ return false;
232
+ }
233
+ const userAgent = normalizeValue(resolvedNavigatorProvider.userAgent);
234
+ if (isNonBrowserRuntime(userAgent)) {
235
+ return false;
236
+ }
237
+ const platform = normalizeValue(resolvedNavigatorProvider.platform);
238
+ const userAgentDataPlatform = normalizeValue(resolvedNavigatorProvider.userAgentData?.platform);
239
+ const isAndroidPlatform = hasPlatformKeyword(platform, PLATFORM_KEYWORD_ANDROID) || hasPlatformKeyword(userAgentDataPlatform, PLATFORM_KEYWORD_ANDROID) || hasPlatformKeyword(userAgent, PLATFORM_KEYWORD_ANDROID);
240
+ if (isAndroidPlatform) {
241
+ return false;
242
+ }
243
+ if (platform.includes(PLATFORM_KEYWORD_LINUX)) {
244
+ return true;
245
+ }
246
+ if (userAgentDataPlatform.includes(PLATFORM_KEYWORD_LINUX)) {
247
+ return true;
248
+ }
249
+ if (userAgent.includes(PLATFORM_KEYWORD_LINUX)) {
250
+ return true;
251
+ }
252
+ return false;
253
+ }
254
+
255
+ // src/core/audio-recording/audio-recording-error.ts
256
+ var ERROR_AUDIO_RECORDING_DISABLED = "recording.audio-disabled";
257
+ var ERROR_AUDIO_SOURCE_SWITCH_UNSUPPORTED = "recording.audio-source-switch-unsupported";
258
+ var ERROR_AUDIO_UNSUPPORTED_INPUT = "audio.unsupported-input";
259
+ var ERROR_AUDIO_NO_AUDIO_TRACK = "audio.no-audio-track";
260
+ var ERROR_AUDIO_CODEC_UNSUPPORTED = "audio.codec-unsupported";
261
+ function createAudioRecordingError(code, message) {
262
+ const error = new Error(`${message} [${code}]`);
263
+ error.name = "AudioRecordingError";
264
+ error.code = code;
265
+ return error;
266
+ }
267
+
268
+ // src/core/audio-recording/audio-output-format.ts
269
+ var AUDIO_OUTPUT_FORMATS = Object.freeze({
270
+ aac: {
271
+ format: "aac",
272
+ container: "mp4",
273
+ mimeType: "audio/mp4",
274
+ extension: "m4a"
275
+ },
276
+ opus: {
277
+ format: "opus",
278
+ container: "webm",
279
+ mimeType: "audio/webm",
280
+ extension: "webm"
281
+ }
282
+ });
283
+ function getAudioFormatCandidates(requestedFormat, isLinuxPlatform) {
284
+ if (isLinuxPlatform) {
285
+ return ["opus"];
286
+ }
287
+ if (requestedFormat === "opus") {
288
+ return ["opus", "aac"];
289
+ }
290
+ return ["aac", "opus"];
291
+ }
292
+ async function resolveAudioOutputFormat(requestedFormat, options) {
293
+ const candidates = getAudioFormatCandidates(requestedFormat, options.isLinuxPlatform);
294
+ for (const candidate of candidates) {
295
+ const isEncodable = await options.canEncode(candidate).catch(() => false);
296
+ if (isEncodable) {
297
+ return AUDIO_OUTPUT_FORMATS[candidate];
298
+ }
299
+ }
300
+ throw createAudioRecordingError(ERROR_AUDIO_CODEC_UNSUPPORTED, `This browser cannot encode audio as ${candidates.join(" or ")}`);
301
+ }
302
+
303
+ // src/core/audio-recording/audio-peaks.ts
304
+ var PEAKS_VERSION = 1;
305
+ var MAX_AUDIO_PEAK_POINTS = 1000;
306
+ var PEAK_WINDOW_SECONDS = 0.01;
307
+ var PEAK_PRECISION = 1000;
308
+ function roundPeak(value) {
309
+ return Math.round(Math.min(1, value) * PEAK_PRECISION) / PEAK_PRECISION;
310
+ }
311
+
312
+ class AudioPeaksAccumulator {
313
+ windowPeaks = [];
314
+ windowFrames = 0;
315
+ currentWindowPeak = 0;
316
+ currentWindowFrameCount = 0;
317
+ addPlanarSamples(data, frames, numberOfChannels, sampleRate) {
318
+ if (frames <= 0 || numberOfChannels <= 0 || sampleRate <= 0) {
319
+ return;
320
+ }
321
+ if (this.windowFrames === 0) {
322
+ this.windowFrames = Math.max(1, Math.round(sampleRate * PEAK_WINDOW_SECONDS));
323
+ }
324
+ for (let frame = 0;frame < frames; frame += 1) {
325
+ let amplitude = 0;
326
+ for (let channel = 0;channel < numberOfChannels; channel += 1) {
327
+ const sample = Math.abs(data[channel * frames + frame]);
328
+ if (sample > amplitude) {
329
+ amplitude = sample;
330
+ }
331
+ }
332
+ if (amplitude > this.currentWindowPeak) {
333
+ this.currentWindowPeak = amplitude;
334
+ }
335
+ this.currentWindowFrameCount += 1;
336
+ if (this.currentWindowFrameCount >= this.windowFrames) {
337
+ this.flushWindow();
338
+ }
339
+ }
340
+ }
341
+ toPeaks() {
342
+ if (this.currentWindowFrameCount > 0) {
343
+ this.flushWindow();
344
+ }
345
+ const windowCount = this.windowPeaks.length;
346
+ const length = Math.min(MAX_AUDIO_PEAK_POINTS, windowCount);
347
+ const data = [];
348
+ for (let point = 0;point < length; point += 1) {
349
+ const start = Math.floor(point * windowCount / length);
350
+ const end = Math.max(start + 1, Math.floor((point + 1) * windowCount / length));
351
+ let peak = 0;
352
+ for (let index = start;index < end; index += 1) {
353
+ if (this.windowPeaks[index] > peak) {
354
+ peak = this.windowPeaks[index];
355
+ }
356
+ }
357
+ data.push(roundPeak(peak));
358
+ }
359
+ return { version: PEAKS_VERSION, length, data };
360
+ }
361
+ flushWindow() {
362
+ this.windowPeaks.push(this.currentWindowPeak);
363
+ this.currentWindowPeak = 0;
364
+ this.currentWindowFrameCount = 0;
365
+ }
366
+ }
367
+
368
+ // src/core/audio-recording/audio-file-transcoder.ts
369
+ var FILE_EXTENSION_PATTERN = /\.[^.]+$/;
370
+ function openInput(blob) {
371
+ return new Input({ formats: ALL_FORMATS, source: new BlobSource(blob) });
372
+ }
373
+ function requirePrimaryAudioTrack(input) {
374
+ return input.getPrimaryAudioTrack().catch(() => {
375
+ throw createAudioRecordingError(ERROR_AUDIO_UNSUPPORTED_INPUT, "The file format is not supported");
376
+ }).then((track) => {
377
+ if (!track) {
378
+ throw createAudioRecordingError(ERROR_AUDIO_NO_AUDIO_TRACK, "The file has no audio track");
379
+ }
380
+ return track;
381
+ });
382
+ }
383
+ function addSampleToPeaks(accumulator, sample) {
384
+ const frames = sample.numberOfFrames;
385
+ const channels = sample.numberOfChannels;
386
+ const planar = new Float32Array(frames * channels);
387
+ for (let channel = 0;channel < channels; channel += 1) {
388
+ sample.copyTo(planar.subarray(channel * frames, (channel + 1) * frames), {
389
+ planeIndex: channel,
390
+ format: "f32-planar"
391
+ });
392
+ }
393
+ accumulator.addPlanarSamples(planar, frames, channels, sample.sampleRate);
394
+ }
395
+ function createOutput(outputFormat) {
396
+ const format = outputFormat.container === "mp4" ? new Mp4OutputFormat({ fastStart: "in-memory" }) : new WebMOutputFormat;
397
+ return new Output({ format, target: new BufferTarget });
398
+ }
399
+ async function readAudioMetadata(blob) {
400
+ const input = openInput(blob);
401
+ const track = await requirePrimaryAudioTrack(input);
402
+ const metadata = {
403
+ duration: await input.computeDuration(),
404
+ channels: await track.getNumberOfChannels(),
405
+ sampleRate: await track.getSampleRate()
406
+ };
407
+ input.dispose();
408
+ return metadata;
409
+ }
410
+ async function computeAudioPeaksFromBlob(blob) {
411
+ const input = openInput(blob);
412
+ const track = await requirePrimaryAudioTrack(input);
413
+ if (!await track.canDecode()) {
414
+ input.dispose();
415
+ return null;
416
+ }
417
+ const accumulator = new AudioPeaksAccumulator;
418
+ for await (const sample of new AudioSampleSink(track).samples()) {
419
+ addSampleToPeaks(accumulator, sample);
420
+ sample.close();
421
+ }
422
+ input.dispose();
423
+ return accumulator.toPeaks();
424
+ }
425
+ async function convertAudio(file, config, options) {
426
+ const input = openInput(file);
427
+ const primaryTrack = await requirePrimaryAudioTrack(input);
428
+ if (!await primaryTrack.canDecode()) {
429
+ input.dispose();
430
+ throw createAudioRecordingError(ERROR_AUDIO_UNSUPPORTED_INPUT, `This browser cannot decode ${primaryTrack.codec ?? "this"} audio`);
431
+ }
432
+ const outputFormat = await resolveAudioOutputFormat(config.format, {
433
+ isLinuxPlatform: isLinuxPlatform(),
434
+ canEncode: (format) => canEncodeAudio(format, {
435
+ numberOfChannels: config.channels,
436
+ bitrate: config.bitrate
437
+ })
438
+ });
439
+ const output = createOutput(outputFormat);
440
+ const accumulator = new AudioPeaksAccumulator;
441
+ const audioOptions = {
442
+ codec: outputFormat.format,
443
+ bitrate: config.bitrate,
444
+ numberOfChannels: config.channels,
445
+ forceTranscode: options.shouldForceTranscode
446
+ };
447
+ if (options.shouldForceTranscode) {
448
+ audioOptions.process = (sample) => {
449
+ addSampleToPeaks(accumulator, sample);
450
+ return sample;
451
+ };
452
+ }
453
+ const conversion = await Conversion.init({
454
+ input,
455
+ output,
456
+ video: { discard: true },
457
+ audio: (track) => track === primaryTrack ? audioOptions : { discard: true },
458
+ showWarnings: false
459
+ });
460
+ if (!conversion.isValid) {
461
+ input.dispose();
462
+ throw createAudioRecordingError(ERROR_AUDIO_UNSUPPORTED_INPUT, "The audio track cannot be converted in this browser");
463
+ }
464
+ if (options.onProgress) {
465
+ const onProgress = options.onProgress;
466
+ conversion.onProgress = (progress) => onProgress(progress);
467
+ }
468
+ options.signal?.addEventListener("abort", () => {
469
+ conversion.cancel().catch(() => {
470
+ return;
471
+ });
472
+ }, { once: true });
473
+ await conversion.execute().finally(() => input.dispose());
474
+ const buffer = output.target.buffer;
475
+ if (!buffer) {
476
+ throw new Error("Audio conversion produced no output");
477
+ }
478
+ const blob = new Blob([buffer], { type: outputFormat.mimeType });
479
+ const peaks = options.shouldForceTranscode ? accumulator.toPeaks() : await computeAudioPeaksFromBlob(blob);
480
+ return { blob, outputFormat, bitrate: config.bitrate, peaks };
481
+ }
482
+ async function createAudioRecording(encoded) {
483
+ const metadata = await readAudioMetadata(encoded.blob);
484
+ return {
485
+ blob: encoded.blob,
486
+ mimeType: encoded.outputFormat.mimeType,
487
+ extension: encoded.outputFormat.extension,
488
+ duration: metadata.duration,
489
+ peaks: encoded.peaks,
490
+ format: encoded.outputFormat.format,
491
+ bitrate: encoded.bitrate,
492
+ channels: metadata.channels,
493
+ sampleRate: metadata.sampleRate
494
+ };
495
+ }
496
+ async function convertRecordedAudio(blob, config, options = {}) {
497
+ const encoded = await convertAudio(blob, config, {
498
+ ...options,
499
+ shouldForceTranscode: false
500
+ });
501
+ return await createAudioRecording(encoded);
502
+ }
503
+ function resolveSourceTitle(file) {
504
+ if (!(file instanceof File) || file.name.trim() === "") {
505
+ return null;
506
+ }
507
+ return file.name.replace(FILE_EXTENSION_PATTERN, "");
508
+ }
509
+ async function transcodeAudioFile(file, config, options = {}) {
510
+ const encoded = await convertAudio(file, config, {
511
+ ...options,
512
+ shouldForceTranscode: true
513
+ });
514
+ const recording = await createAudioRecording(encoded);
515
+ if (!recording.peaks) {
516
+ throw new Error("Audio peaks could not be computed");
517
+ }
518
+ return {
519
+ blob: recording.blob,
520
+ mimeType: recording.mimeType,
521
+ filename: createRecordingFilename(resolveSourceTitle(file), recording.extension),
522
+ duration: recording.duration,
523
+ peaks: recording.peaks,
524
+ format: recording.format,
525
+ bitrate: recording.bitrate,
526
+ channels: recording.channels,
527
+ sampleRate: recording.sampleRate
528
+ };
529
+ }
530
+ // src/core/audio-recording/audio-recording-config.ts
531
+ var AUDIO_BITRATES = [
532
+ 64000,
533
+ 96000,
534
+ 128000,
535
+ 192000
536
+ ];
537
+ var DISABLED_AUDIO_RECORDING_CONFIG = Object.freeze({
538
+ enabled: false,
539
+ format: "aac",
540
+ bitrate: 128000,
541
+ channels: 1,
542
+ echoCancellation: true,
543
+ noiseSuppression: true,
544
+ autoGain: true,
545
+ normalize: false
546
+ });
547
+ function isRecord(value) {
548
+ return typeof value === "object" && value !== null && !Array.isArray(value);
549
+ }
550
+ function parseFormat(value) {
551
+ if (value === "opus") {
552
+ return "opus";
553
+ }
554
+ return DISABLED_AUDIO_RECORDING_CONFIG.format;
555
+ }
556
+ function parseBitrate(value) {
557
+ const bitrate = AUDIO_BITRATES.find((candidate) => candidate === value);
558
+ if (bitrate === undefined) {
559
+ return DISABLED_AUDIO_RECORDING_CONFIG.bitrate;
560
+ }
561
+ return bitrate;
562
+ }
563
+ function parseChannels(value) {
564
+ if (value === 2) {
565
+ return 2;
566
+ }
567
+ return DISABLED_AUDIO_RECORDING_CONFIG.channels;
568
+ }
569
+ function parseBoolean(value, defaultValue) {
570
+ if (typeof value === "boolean") {
571
+ return value;
572
+ }
573
+ return defaultValue;
574
+ }
575
+ function parseAudioRecordingConfig(value) {
576
+ if (!(isRecord(value) && value.enabled === true)) {
577
+ return { ...DISABLED_AUDIO_RECORDING_CONFIG };
578
+ }
579
+ return {
580
+ enabled: true,
581
+ format: parseFormat(value.format),
582
+ bitrate: parseBitrate(value.bitrate),
583
+ channels: parseChannels(value.channels),
584
+ echoCancellation: parseBoolean(value.echoCancellation, DISABLED_AUDIO_RECORDING_CONFIG.echoCancellation),
585
+ noiseSuppression: parseBoolean(value.noiseSuppression, DISABLED_AUDIO_RECORDING_CONFIG.noiseSuppression),
586
+ autoGain: parseBoolean(value.autoGain, DISABLED_AUDIO_RECORDING_CONFIG.autoGain),
587
+ normalize: parseBoolean(value.normalize, DISABLED_AUDIO_RECORDING_CONFIG.normalize)
588
+ };
589
+ }
590
+ function requireEnabledAudioRecordingConfig(config) {
591
+ if (!config?.enabled) {
592
+ throw createAudioRecordingError(ERROR_AUDIO_RECORDING_DISABLED, "Audio recording is not enabled for this environment");
593
+ }
594
+ return config;
595
+ }
142
596
  // src/core/browser-guard/browser-guard.ts
143
597
  import { UAParser } from "ua-parser-js";
144
598
  var UNSUPPORTED_BROWSER_NAMES = new Set([
@@ -425,96 +879,28 @@ function isMobileDevice() {
425
879
  return false;
426
880
  }
427
881
 
428
- // src/core/utils/platform-detection.ts
429
- var PLATFORM_KEYWORD_LINUX = "linux";
430
- var PLATFORM_KEYWORD_ANDROID = "android";
431
- var RUNTIME_MARKER_BUN = "bun/";
432
- var RUNTIME_MARKER_HAPPY_DOM = "happydom";
433
- var RUNTIME_MARKER_NODE = "node.js";
434
- function normalizeValue(value) {
435
- if (value === undefined) {
436
- return "";
882
+ // src/core/config/preset-mapper.ts
883
+ function mapPresetToConfig(options) {
884
+ const { preset, outputFormat, watermark, isMobile } = options;
885
+ if (!(preset in RESOLUTION_MAP)) {
886
+ return Promise.reject(new Error(`Invalid preset: ${preset}`));
437
887
  }
438
- return value.toLowerCase();
439
- }
440
- function hasPlatformKeyword(value, keyword) {
441
- return value.includes(keyword);
442
- }
443
- function isNonBrowserRuntime(userAgent) {
444
- if (userAgent.includes(RUNTIME_MARKER_BUN)) {
445
- return true;
888
+ let isMobileDeviceDetected = isMobile;
889
+ if (isMobileDeviceDetected === undefined) {
890
+ isMobileDeviceDetected = isMobileDevice();
446
891
  }
447
- if (userAgent.includes(RUNTIME_MARKER_HAPPY_DOM)) {
448
- return true;
892
+ let resolutionMap = RESOLUTION_MAP;
893
+ if (isMobileDeviceDetected) {
894
+ resolutionMap = MOBILE_RESOLUTION_MAP;
449
895
  }
450
- if (userAgent.includes(RUNTIME_MARKER_NODE)) {
451
- return true;
896
+ const { width, height } = resolutionMap[preset];
897
+ let format = outputFormat;
898
+ if (format === undefined) {
899
+ format = "mp4";
452
900
  }
453
- return false;
454
- }
455
- function resolveNavigatorProvider(navigatorProvider) {
456
- if (navigatorProvider) {
457
- return navigatorProvider;
458
- }
459
- if (typeof navigator === "undefined") {
460
- return null;
461
- }
462
- const globalNavigator = navigator;
463
- return {
464
- platform: globalNavigator.platform,
465
- userAgent: globalNavigator.userAgent,
466
- userAgentData: globalNavigator.userAgentData
467
- };
468
- }
469
- function isLinuxPlatform(navigatorProvider) {
470
- const resolvedNavigatorProvider = resolveNavigatorProvider(navigatorProvider);
471
- if (resolvedNavigatorProvider === null) {
472
- return false;
473
- }
474
- const userAgent = normalizeValue(resolvedNavigatorProvider.userAgent);
475
- if (isNonBrowserRuntime(userAgent)) {
476
- return false;
477
- }
478
- const platform = normalizeValue(resolvedNavigatorProvider.platform);
479
- const userAgentDataPlatform = normalizeValue(resolvedNavigatorProvider.userAgentData?.platform);
480
- const isAndroidPlatform = hasPlatformKeyword(platform, PLATFORM_KEYWORD_ANDROID) || hasPlatformKeyword(userAgentDataPlatform, PLATFORM_KEYWORD_ANDROID) || hasPlatformKeyword(userAgent, PLATFORM_KEYWORD_ANDROID);
481
- if (isAndroidPlatform) {
482
- return false;
483
- }
484
- if (platform.includes(PLATFORM_KEYWORD_LINUX)) {
485
- return true;
486
- }
487
- if (userAgentDataPlatform.includes(PLATFORM_KEYWORD_LINUX)) {
488
- return true;
489
- }
490
- if (userAgent.includes(PLATFORM_KEYWORD_LINUX)) {
491
- return true;
492
- }
493
- return false;
494
- }
495
-
496
- // src/core/config/preset-mapper.ts
497
- function mapPresetToConfig(options) {
498
- const { preset, outputFormat, watermark, isMobile } = options;
499
- if (!(preset in RESOLUTION_MAP)) {
500
- return Promise.reject(new Error(`Invalid preset: ${preset}`));
501
- }
502
- let isMobileDeviceDetected = isMobile;
503
- if (isMobileDeviceDetected === undefined) {
504
- isMobileDeviceDetected = isMobileDevice();
505
- }
506
- let resolutionMap = RESOLUTION_MAP;
507
- if (isMobileDeviceDetected) {
508
- resolutionMap = MOBILE_RESOLUTION_MAP;
509
- }
510
- const { width, height } = resolutionMap[preset];
511
- let format = outputFormat;
512
- if (format === undefined) {
513
- format = "mp4";
514
- }
515
- let isLinuxPlatformDetected = options.isLinuxPlatform;
516
- if (isLinuxPlatformDetected === undefined) {
517
- isLinuxPlatformDetected = isLinuxPlatform();
901
+ let isLinuxPlatformDetected = options.isLinuxPlatform;
902
+ if (isLinuxPlatformDetected === undefined) {
903
+ isLinuxPlatformDetected = isLinuxPlatform();
518
904
  }
519
905
  const policy = getFormatCompatibilityPolicy(format, {
520
906
  isLinuxPlatform: isLinuxPlatformDetected
@@ -645,6 +1031,9 @@ class ConfigService {
645
1031
  preset: data.presetEncoding,
646
1032
  outputFormat: data.outputFormat,
647
1033
  watermark: data.watermark
1034
+ }).then((config) => {
1035
+ config.audioRecording = parseAudioRecordingConfig(data.audio);
1036
+ return config;
648
1037
  });
649
1038
  });
650
1039
  });
@@ -927,7 +1316,11 @@ function createPermissionRecoveryData(errorCode, browserInfo, language = "en") {
927
1316
  }
928
1317
 
929
1318
  // src/core/device/permission-flow-orchestrator.ts
930
- var GRANTED_PERMISSION_COUNT = 2;
1319
+ var VIDEO_PERMISSION_NAMES = [
1320
+ "camera",
1321
+ "microphone"
1322
+ ];
1323
+ var AUDIO_PERMISSION_NAMES = ["microphone"];
931
1324
  var DEFAULT_PERMISSION_STATUS = {
932
1325
  camera: "unknown",
933
1326
  microphone: "unknown"
@@ -948,7 +1341,7 @@ function stopStreamTracks(stream) {
948
1341
  track.stop();
949
1342
  }
950
1343
  }
951
- async function requestPermissionProbe() {
1344
+ async function requestPermissionProbe(mediaType = "video") {
952
1345
  const mediaDevices = globalThis.navigator?.mediaDevices;
953
1346
  if (!mediaDevices) {
954
1347
  return;
@@ -957,6 +1350,14 @@ async function requestPermissionProbe() {
957
1350
  if (!canGetUserMedia) {
958
1351
  return;
959
1352
  }
1353
+ if (mediaType === "audio") {
1354
+ const audioStream = await mediaDevices.getUserMedia({
1355
+ audio: true,
1356
+ video: false
1357
+ });
1358
+ stopStreamTracks(audioStream);
1359
+ return;
1360
+ }
960
1361
  const stream = await mediaDevices.getUserMedia({ audio: true, video: true });
961
1362
  stopStreamTracks(stream);
962
1363
  await mediaDevices.getUserMedia({
@@ -967,45 +1368,14 @@ async function requestPermissionProbe() {
967
1368
  return;
968
1369
  }).catch(noOperation2);
969
1370
  }
970
- function countGrantedPermissions(permissions) {
971
- let grantedCount = 0;
972
- if (permissions.camera === "granted") {
973
- grantedCount += 1;
974
- }
975
- if (permissions.microphone === "granted") {
976
- grantedCount += 1;
977
- }
978
- return grantedCount;
979
- }
980
- function hasUnknownPermissionState(permissions) {
981
- if (permissions.camera === "unknown") {
982
- return true;
983
- }
984
- if (permissions.microphone === "unknown") {
985
- return true;
986
- }
987
- return false;
988
- }
989
- function hasPromptPermissionState(permissions) {
990
- if (permissions.camera === "prompt") {
991
- return true;
992
- }
993
- if (permissions.microphone === "prompt") {
994
- return true;
995
- }
996
- return false;
1371
+ function hasPermissionState(permissions, permissionNames, state) {
1372
+ return permissionNames.some((name) => permissions[name] === state);
997
1373
  }
998
- function hasHardPermissionDenial(permissions) {
999
- if (permissions.camera === "denied") {
1000
- return true;
1001
- }
1002
- if (permissions.microphone === "denied") {
1003
- return true;
1004
- }
1005
- return false;
1374
+ function areAllPermissionsGranted(permissions, permissionNames) {
1375
+ return permissionNames.every((name) => permissions[name] === "granted");
1006
1376
  }
1007
- function getPermissionDeniedIssueCode(permissions) {
1008
- const hasCameraDenied = permissions.camera === "denied";
1377
+ function getPermissionDeniedIssueCode(permissions, permissionNames) {
1378
+ const hasCameraDenied = permissionNames.includes("camera") && permissions.camera === "denied";
1009
1379
  const hasMicrophoneDenied = permissions.microphone === "denied";
1010
1380
  if (hasCameraDenied && hasMicrophoneDenied) {
1011
1381
  return "permission-denied-both";
@@ -1036,6 +1406,8 @@ class PermissionFlowOrchestrator {
1036
1406
  dependencies;
1037
1407
  callbacks;
1038
1408
  language;
1409
+ mediaType;
1410
+ permissionNames;
1039
1411
  currentState;
1040
1412
  cleanupCameraListener = noOperation2;
1041
1413
  cleanupMicrophoneListener = noOperation2;
@@ -1053,6 +1425,12 @@ class PermissionFlowOrchestrator {
1053
1425
  language = "es";
1054
1426
  }
1055
1427
  this.language = language;
1428
+ let mediaType = "video";
1429
+ if (options.mediaType === "audio") {
1430
+ mediaType = "audio";
1431
+ }
1432
+ this.mediaType = mediaType;
1433
+ this.permissionNames = mediaType === "audio" ? AUDIO_PERMISSION_NAMES : VIDEO_PERMISSION_NAMES;
1056
1434
  this.currentState = {
1057
1435
  step: "idle",
1058
1436
  permissions: DEFAULT_PERMISSION_STATUS,
@@ -1107,9 +1485,9 @@ class PermissionFlowOrchestrator {
1107
1485
  return this.currentState;
1108
1486
  }
1109
1487
  requestPermissionAndRefresh() {
1110
- return this.dependencies.requestPermissionProbe().then(() => {
1488
+ return this.dependencies.requestPermissionProbe(this.mediaType).then(() => {
1111
1489
  const grantedPermissions = {
1112
- camera: "granted",
1490
+ camera: this.mediaType === "audio" ? this.currentState.permissions.camera : "granted",
1113
1491
  microphone: "granted"
1114
1492
  };
1115
1493
  this.currentState = this.createStateFromPermissions(grantedPermissions);
@@ -1169,8 +1547,7 @@ class PermissionFlowOrchestrator {
1169
1547
  if (!isSecureContext) {
1170
1548
  return this.createInsecureContextState();
1171
1549
  }
1172
- const grantedCount = countGrantedPermissions(permissions);
1173
- if (grantedCount === GRANTED_PERMISSION_COUNT) {
1550
+ if (areAllPermissionsGranted(permissions, this.permissionNames)) {
1174
1551
  return {
1175
1552
  step: "ready",
1176
1553
  permissions,
@@ -1181,8 +1558,8 @@ class PermissionFlowOrchestrator {
1181
1558
  shouldProbeUnknown: false
1182
1559
  };
1183
1560
  }
1184
- if (hasHardPermissionDenial(permissions)) {
1185
- const issueCode = getPermissionDeniedIssueCode(permissions);
1561
+ if (hasPermissionState(permissions, this.permissionNames, "denied")) {
1562
+ const issueCode = getPermissionDeniedIssueCode(permissions, this.permissionNames);
1186
1563
  const recoveryData = this.dependencies.createPermissionRecoveryData(issueCode, this.dependencies.getBrowserInfo(), this.language);
1187
1564
  return {
1188
1565
  step: "blocked",
@@ -1196,8 +1573,8 @@ class PermissionFlowOrchestrator {
1196
1573
  shouldProbeUnknown: false
1197
1574
  };
1198
1575
  }
1199
- const shouldProbeUnknown = hasUnknownPermissionState(permissions);
1200
- const hasPromptState = hasPromptPermissionState(permissions);
1576
+ const shouldProbeUnknown = hasPermissionState(permissions, this.permissionNames, "unknown");
1577
+ const hasPromptState = hasPermissionState(permissions, this.permissionNames, "prompt");
1201
1578
  let canRetry = false;
1202
1579
  if (shouldProbeUnknown) {
1203
1580
  canRetry = true;
@@ -1216,7 +1593,7 @@ class PermissionFlowOrchestrator {
1216
1593
  };
1217
1594
  }
1218
1595
  transitionToBlockedFromFailedProbe() {
1219
- const issueCode = "permission-denied-both";
1596
+ const issueCode = this.mediaType === "audio" ? "permission-denied-microphone" : "permission-denied-both";
1220
1597
  const recoveryData = this.dependencies.createPermissionRecoveryData(issueCode, this.dependencies.getBrowserInfo(), this.language);
1221
1598
  this.currentState = {
1222
1599
  step: "blocked",
@@ -1273,11 +1650,11 @@ class PermissionFlowOrchestrator {
1273
1650
  }
1274
1651
  }
1275
1652
  // src/core/utils/video-utils.ts
1276
- import { BlobSource, Input, MP4 } from "mediabunny";
1653
+ import { BlobSource as BlobSource2, Input as Input2, MP4 } from "mediabunny";
1277
1654
  async function extractVideoDuration(file) {
1278
1655
  try {
1279
- const source = new BlobSource(file);
1280
- const input = new Input({
1656
+ const source = new BlobSource2(file);
1657
+ const input = new Input2({
1281
1658
  formats: [MP4],
1282
1659
  source
1283
1660
  });
@@ -1656,6 +2033,48 @@ function decideRecordingRoute(options) {
1656
2033
  }
1657
2034
  };
1658
2035
  }
2036
+ var UNSUPPORTED_AUDIO_GUIDANCE_MESSAGE = "Audio recording is not supported in this browser context. Use a secure connection and a current Chrome, Edge, Firefox or Safari browser.";
2037
+ function detectAudioRecordingCapabilities() {
2038
+ const hasWindow = typeof window !== "undefined";
2039
+ return {
2040
+ isSecureContext: hasWindow && window.isSecureContext === true,
2041
+ hasAudioEncoder: typeof AudioEncoder !== "undefined",
2042
+ hasAudioWorklet: typeof AudioWorkletNode !== "undefined" && hasWindow && typeof window.AudioContext !== "undefined",
2043
+ hasMediaRecorder: typeof MediaRecorder !== "undefined"
2044
+ };
2045
+ }
2046
+ function decideAudioRecordingRoute(capabilities) {
2047
+ if (!capabilities.isSecureContext) {
2048
+ return {
2049
+ route: "unsupported-with-guidance",
2050
+ reasonCodes: ["insecure-context"],
2051
+ guidance: {
2052
+ canRetry: true,
2053
+ message: UNSUPPORTED_AUDIO_GUIDANCE_MESSAGE
2054
+ }
2055
+ };
2056
+ }
2057
+ if (capabilities.hasAudioEncoder && capabilities.hasAudioWorklet) {
2058
+ return {
2059
+ route: "local-webcodecs",
2060
+ reasonCodes: ["webcodecs-supported", "target-codec-supported"]
2061
+ };
2062
+ }
2063
+ if (capabilities.hasMediaRecorder) {
2064
+ return {
2065
+ route: "safe-capture-post-recording-transcode",
2066
+ reasonCodes: ["webcodecs-unavailable", "mediarecorder-supported"]
2067
+ };
2068
+ }
2069
+ return {
2070
+ route: "unsupported-with-guidance",
2071
+ reasonCodes: ["webcodecs-unavailable", "mediarecorder-unavailable"],
2072
+ guidance: {
2073
+ canRetry: true,
2074
+ message: UNSUPPORTED_AUDIO_GUIDANCE_MESSAGE
2075
+ }
2076
+ };
2077
+ }
1659
2078
 
1660
2079
  // src/core/storage/video-storage.ts
1661
2080
  var DB_NAME = "vidtreo-recorder";
@@ -1675,6 +2094,12 @@ var ERROR_BLOB_READ_FAILED = "Failed to prepare upload data for storage. The rec
1675
2094
  var PROBE_RECORD_ID = "__probe__";
1676
2095
  var PROBE_DATA = new ArrayBuffer(1);
1677
2096
  var ERROR_PROBE_WRITE_FAILED = "Storage write probe failed. Browser may be in private browsing mode or IndexedDB writes are restricted.";
2097
+ function resolveMediaType(mediaType) {
2098
+ if (mediaType === "audio") {
2099
+ return "audio";
2100
+ }
2101
+ return "video";
2102
+ }
1678
2103
 
1679
2104
  class VideoStorageService {
1680
2105
  db = null;
@@ -1821,6 +2246,7 @@ class VideoStorageService {
1821
2246
  const now = Date.now();
1822
2247
  const pendingUpload = {
1823
2248
  ...upload,
2249
+ mediaType: resolveMediaType(upload.mediaType),
1824
2250
  id,
1825
2251
  status: "pending",
1826
2252
  retryCount: 0,
@@ -1935,14 +2361,16 @@ class VideoStorageService {
1935
2361
  return uploads.reduce((total, upload) => total + upload.blob.size, 0);
1936
2362
  }
1937
2363
  hydrateUpload(upload) {
2364
+ const mediaType = resolveMediaType(upload.mediaType);
1938
2365
  if ("blobData" in upload && upload.blobData) {
1939
2366
  const { blobData, blobType, ...rest } = upload;
1940
2367
  return {
1941
2368
  ...rest,
2369
+ mediaType,
1942
2370
  blob: new Blob([blobData], { type: blobType })
1943
2371
  };
1944
2372
  }
1945
- return upload;
2373
+ return { ...upload, mediaType };
1946
2374
  }
1947
2375
  async createStoredUpdates(updates) {
1948
2376
  const storedUpdates = { ...updates };
@@ -2007,17 +2435,17 @@ class VideoStorageService {
2007
2435
 
2008
2436
  // src/core/transcode/video-transcoder.ts
2009
2437
  import {
2010
- ALL_FORMATS,
2011
- BlobSource as BlobSource2,
2012
- BufferTarget,
2013
- Conversion,
2438
+ ALL_FORMATS as ALL_FORMATS2,
2439
+ BlobSource as BlobSource3,
2440
+ BufferTarget as BufferTarget2,
2441
+ Conversion as Conversion2,
2014
2442
  FilePathSource,
2015
- Input as Input2,
2443
+ Input as Input3,
2016
2444
  MkvOutputFormat,
2017
2445
  MovOutputFormat,
2018
- Mp4OutputFormat,
2019
- Output,
2020
- WebMOutputFormat
2446
+ Mp4OutputFormat as Mp4OutputFormat2,
2447
+ Output as Output2,
2448
+ WebMOutputFormat as WebMOutputFormat2
2021
2449
  } from "mediabunny";
2022
2450
 
2023
2451
  // src/core/processor/codec-policy-resolver.ts
@@ -2263,16 +2691,16 @@ function createSource(input) {
2263
2691
  return new FilePathSource(input);
2264
2692
  }
2265
2693
  if (input instanceof Blob) {
2266
- return new BlobSource2(input);
2694
+ return new BlobSource3(input);
2267
2695
  }
2268
2696
  throw new Error("Invalid input type. Expected Blob, File, or file path string.");
2269
2697
  }
2270
2698
  function createOutputFormat(format) {
2271
2699
  switch (format) {
2272
2700
  case "mp4":
2273
- return new Mp4OutputFormat;
2701
+ return new Mp4OutputFormat2;
2274
2702
  case "webm":
2275
- return new WebMOutputFormat;
2703
+ return new WebMOutputFormat2;
2276
2704
  case "mkv":
2277
2705
  return new MkvOutputFormat;
2278
2706
  case "mov":
@@ -2362,16 +2790,16 @@ async function transcodeVideo(input, config = {}, onProgress, maximumDurationSec
2362
2790
  height: finalConfig.height,
2363
2791
  bitrate: finalConfig.bitrate
2364
2792
  });
2365
- const mediabunnyInput = new Input2({
2366
- formats: ALL_FORMATS,
2793
+ const mediabunnyInput = new Input3({
2794
+ formats: ALL_FORMATS2,
2367
2795
  source
2368
2796
  });
2369
2797
  const outputFormat = createOutputFormat(finalConfig.format);
2370
- const output = new Output({
2798
+ const output = new Output2({
2371
2799
  format: outputFormat,
2372
- target: new BufferTarget
2800
+ target: new BufferTarget2
2373
2801
  });
2374
- const conversion = await Conversion.init({
2802
+ const conversion = await Conversion2.init({
2375
2803
  input: mediabunnyInput,
2376
2804
  output,
2377
2805
  trim: await resolveTrim(mediabunnyInput, maximumDurationSeconds),
@@ -2437,17 +2865,17 @@ async function transcodeVideoForNativeCamera(file, config = {}, onProgress) {
2437
2865
  height: finalConfig.height,
2438
2866
  bitrate: finalConfig.bitrate
2439
2867
  });
2440
- const source = new BlobSource2(file);
2441
- const mediabunnyInput = new Input2({
2442
- formats: ALL_FORMATS,
2868
+ const source = new BlobSource3(file);
2869
+ const mediabunnyInput = new Input3({
2870
+ formats: ALL_FORMATS2,
2443
2871
  source
2444
2872
  });
2445
2873
  const outputFormat = createOutputFormat(finalConfig.format);
2446
- const output = new Output({
2874
+ const output = new Output2({
2447
2875
  format: outputFormat,
2448
- target: new BufferTarget
2876
+ target: new BufferTarget2
2449
2877
  });
2450
- const conversion = await Conversion.init({
2878
+ const conversion = await Conversion2.init({
2451
2879
  input: mediabunnyInput,
2452
2880
  output,
2453
2881
  video: {
@@ -2480,12 +2908,17 @@ async function transcodeVideoForNativeCamera(file, config = {}, onProgress) {
2480
2908
  }
2481
2909
 
2482
2910
  // src/core/upload/recording-upload-result.ts
2483
- function createRecordingUploadResult(result) {
2484
- return {
2911
+ function createRecordingUploadResult(result, mediaType = "video") {
2912
+ const recordingUploadResult = {
2485
2913
  ...result,
2486
2914
  recordingId: result.id,
2487
2915
  uploadUrl: result.uploadUrl ?? ""
2488
2916
  };
2917
+ if (mediaType === "audio") {
2918
+ recordingUploadResult.mediaType = "audio";
2919
+ recordingUploadResult.audioId = result.id;
2920
+ }
2921
+ return recordingUploadResult;
2489
2922
  }
2490
2923
 
2491
2924
  // src/core/upload/upload-error.ts
@@ -2724,6 +3157,8 @@ class UploadQueueManager {
2724
3157
  duration: upload.duration,
2725
3158
  metadata: upload.metadata,
2726
3159
  userMetadata: upload.userMetadata,
3160
+ mediaType: upload.mediaType,
3161
+ audio: upload.audio,
2727
3162
  isResume: this.isResumedUpload(upload),
2728
3163
  onProgress: (progress) => {
2729
3164
  this.reportUploadProgress(upload.id, progress);
@@ -2740,7 +3175,7 @@ class UploadQueueManager {
2740
3175
  } else {
2741
3176
  throw new Error("Upload credentials are unavailable for recovery");
2742
3177
  }
2743
- const result = createRecordingUploadResult(await this.uploadService.uploadVideo(upload.blob, uploadOptions));
3178
+ const result = createRecordingUploadResult(await this.uploadService.uploadVideo(upload.blob, uploadOptions), upload.mediaType);
2744
3179
  this.callbacks.onUploadAttemptSucceeded?.(upload.id, {
2745
3180
  attempt,
2746
3181
  maxAttempts: MAX_RETRIES2,
@@ -3596,6 +4031,7 @@ function revokeProbeWorkerUrl(workerUrl) {
3596
4031
  }
3597
4032
 
3598
4033
  // src/core/processor/worker/types.ts
4034
+ var WORKER_MESSAGE_TYPE_START_AUDIO = "startAudio";
3599
4035
  var WORKER_MESSAGE_TYPE_PROBE = "probe";
3600
4036
  var WORKER_MESSAGE_TYPE_WARMUP = "warmup";
3601
4037
  var WORKER_MESSAGE_TYPE_AUDIO_CHUNK = "audioChunk";
@@ -4035,6 +4471,56 @@ class StorageManager {
4035
4471
  }
4036
4472
  }
4037
4473
 
4474
+ // src/core/stream/constraints-builder.ts
4475
+ function buildVideoConstraints(cameraDeviceId, dependencies) {
4476
+ let constraints = {};
4477
+ if (dependencies.originalCameraConstraints) {
4478
+ const { deviceId: _deviceId, ...originalConstraintsWithoutDeviceId } = dependencies.originalCameraConstraints;
4479
+ constraints = {
4480
+ ...constraints,
4481
+ ...originalConstraintsWithoutDeviceId
4482
+ };
4483
+ }
4484
+ if (cameraDeviceId) {
4485
+ constraints = { ...constraints, deviceId: { exact: cameraDeviceId } };
4486
+ }
4487
+ const selectedDeviceId = dependencies.getSelectedCameraDeviceId();
4488
+ let hasDeviceId = false;
4489
+ if (cameraDeviceId) {
4490
+ hasDeviceId = true;
4491
+ }
4492
+ if (constraints.deviceId) {
4493
+ hasDeviceId = true;
4494
+ }
4495
+ if (!hasDeviceId && selectedDeviceId) {
4496
+ constraints = { ...constraints, deviceId: { exact: selectedDeviceId } };
4497
+ }
4498
+ return constraints;
4499
+ }
4500
+ function buildAudioConstraints(micDeviceId) {
4501
+ const constraints = {
4502
+ echoCancellation: true,
4503
+ noiseSuppression: true,
4504
+ autoGainControl: true
4505
+ };
4506
+ if (micDeviceId) {
4507
+ return { ...constraints, deviceId: { exact: micDeviceId } };
4508
+ }
4509
+ return constraints;
4510
+ }
4511
+ function buildAudioRecordingConstraints(config, micDeviceId) {
4512
+ const constraints = {
4513
+ echoCancellation: config.echoCancellation,
4514
+ noiseSuppression: config.noiseSuppression,
4515
+ autoGainControl: config.autoGain,
4516
+ channelCount: { ideal: config.channels }
4517
+ };
4518
+ if (micDeviceId) {
4519
+ return { ...constraints, deviceId: { exact: micDeviceId } };
4520
+ }
4521
+ return constraints;
4522
+ }
4523
+
4038
4524
  // src/core/utils/logger.ts
4039
4525
  function isDebugEnabled() {
4040
4526
  const globalAny = globalThis;
@@ -4196,44 +4682,6 @@ function requireMediaDevices() {
4196
4682
  return navigator.mediaDevices;
4197
4683
  }
4198
4684
 
4199
- // src/core/stream/constraints-builder.ts
4200
- function buildVideoConstraints(cameraDeviceId, dependencies) {
4201
- let constraints = {};
4202
- if (dependencies.originalCameraConstraints) {
4203
- const { deviceId: _deviceId, ...originalConstraintsWithoutDeviceId } = dependencies.originalCameraConstraints;
4204
- constraints = {
4205
- ...constraints,
4206
- ...originalConstraintsWithoutDeviceId
4207
- };
4208
- }
4209
- if (cameraDeviceId) {
4210
- constraints = { ...constraints, deviceId: { exact: cameraDeviceId } };
4211
- }
4212
- const selectedDeviceId = dependencies.getSelectedCameraDeviceId();
4213
- let hasDeviceId = false;
4214
- if (cameraDeviceId) {
4215
- hasDeviceId = true;
4216
- }
4217
- if (constraints.deviceId) {
4218
- hasDeviceId = true;
4219
- }
4220
- if (!hasDeviceId && selectedDeviceId) {
4221
- constraints = { ...constraints, deviceId: { exact: selectedDeviceId } };
4222
- }
4223
- return constraints;
4224
- }
4225
- function buildAudioConstraints(micDeviceId) {
4226
- const constraints = {
4227
- echoCancellation: true,
4228
- noiseSuppression: true,
4229
- autoGainControl: true
4230
- };
4231
- if (micDeviceId) {
4232
- return { ...constraints, deviceId: { exact: micDeviceId } };
4233
- }
4234
- return constraints;
4235
- }
4236
-
4237
4685
  // src/core/stream/stream-utils.ts
4238
4686
  var TRACK_READY_STATE_LIVE = "live";
4239
4687
  function stopLiveTracks(tracks) {
@@ -5735,6 +6183,9 @@ class SourceSwitchManager {
5735
6183
  if (sourceType === this.currentSourceType) {
5736
6184
  return;
5737
6185
  }
6186
+ if (sourceType === "audio" || this.currentSourceType === "audio") {
6187
+ throw createAudioRecordingError(ERROR_AUDIO_SOURCE_SWITCH_UNSUPPORTED, "Switching between audio and video sources is not supported");
6188
+ }
5738
6189
  await this.resolveSwitch(sourceType).catch((error) => {
5739
6190
  this.handleToggleError(error);
5740
6191
  });
@@ -5751,6 +6202,15 @@ class SourceSwitchManager {
5751
6202
  }
5752
6203
  return this.switchToScreen();
5753
6204
  }
6205
+ async startAudioOnly(audioConstraints) {
6206
+ this.cleanup();
6207
+ const stream = await this.streamManager.startAudioOnlyStream(audioConstraints);
6208
+ this.currentSourceType = "audio";
6209
+ if (this.callbacks.onSourceChange) {
6210
+ await this.callbacks.onSourceChange(this.currentSourceType);
6211
+ }
6212
+ return stream;
6213
+ }
5754
6214
  async startScreenOnly() {
5755
6215
  const displayStream = await this.requestDisplayStream();
5756
6216
  if (!displayStream) {
@@ -6526,6 +6986,29 @@ class StreamManager {
6526
6986
  this.emit("streamstart", { stream });
6527
6987
  return stream;
6528
6988
  }
6989
+ async startAudioOnlyStream(audioConstraints) {
6990
+ if (this.mediaStream) {
6991
+ this.stopStream();
6992
+ }
6993
+ this.setState("starting");
6994
+ this.setAudioStatus("pending");
6995
+ this.pendingAudioError = null;
6996
+ this.acquisitionGeneration++;
6997
+ const audioTrack = await this.acquireAudioTrackWithRetry(requireMediaDevices(), audioConstraints).catch((error) => {
6998
+ const audioError = error instanceof Error && "code" in error ? error : createAudioStreamError(error);
6999
+ this.pendingAudioError = audioError;
7000
+ this.setAudioStatus("failed");
7001
+ this.setState("error");
7002
+ this.emit("error", { error: audioError });
7003
+ throw audioError;
7004
+ });
7005
+ const stream = new MediaStream([audioTrack]);
7006
+ this.mediaStream = stream;
7007
+ this.setAudioStatus("acquired");
7008
+ this.setState("active");
7009
+ this.emit("streamstart", { stream });
7010
+ return stream;
7011
+ }
6529
7012
  async acquireVideoAndAudioStream() {
6530
7013
  const mediaDevices = requireMediaDevices();
6531
7014
  const videoConstraints = this.buildVideoConstraints(this.selectedVideoDeviceId);
@@ -6807,8 +7290,8 @@ class StreamManager {
6807
7290
  switchVideoDevice(deviceId) {
6808
7291
  return this.switchDeviceTrack(deviceId, "video", this.streamConfig.video);
6809
7292
  }
6810
- switchAudioDevice(deviceId) {
6811
- return this.switchDeviceTrack(deviceId, "audio", this.streamConfig.audio);
7293
+ switchAudioDevice(deviceId, audioConstraints = this.streamConfig.audio) {
7294
+ return this.switchDeviceTrack(deviceId, "audio", audioConstraints);
6812
7295
  }
6813
7296
  setMediaStream(stream) {
6814
7297
  this.mediaStream = stream;
@@ -6967,21 +7450,113 @@ class SystemAudioMixer {
6967
7450
  }
6968
7451
  }
6969
7452
 
6970
- // src/core/recording/recording-lifecycle-error.ts
6971
- var ERROR_RECORDING_STOP_NOT_READY = "recording.stop-not-ready";
6972
- var ERROR_RECORDING_FINALIZE_NOT_READY = "recording.finalize-not-ready";
6973
- var ERROR_RECORDING_FINALIZE_TIMEOUT = "recording.finalize-timeout";
6974
- var ERROR_RECORDING_UPLOAD_IN_PROGRESS = "recording.upload-in-progress";
6975
- function createRecordingLifecycleError(code, message, details) {
6976
- const error = new Error(`${message} [${code}]`);
6977
- error.code = code;
6978
- error.isRecoverable = true;
6979
- if (details !== undefined) {
6980
- error.details = details;
7453
+ // src/core/audio-recording/audio-normalizer.ts
7454
+ var COMPRESSOR_THRESHOLD_DB = -24;
7455
+ var COMPRESSOR_KNEE_DB = 12;
7456
+ var COMPRESSOR_RATIO = 4;
7457
+ var COMPRESSOR_ATTACK_SECONDS = 0.003;
7458
+ var COMPRESSOR_RELEASE_SECONDS = 0.25;
7459
+ var MAKEUP_GAIN = 2;
7460
+ var LIMITER_THRESHOLD_DB = -1;
7461
+ var LIMITER_KNEE_DB = 0;
7462
+ var LIMITER_RATIO = 20;
7463
+ var LIMITER_ATTACK_SECONDS = 0.001;
7464
+ var LIMITER_RELEASE_SECONDS = 0.05;
7465
+ var DEFAULT_DEPENDENCIES2 = {
7466
+ createAudioContext: () => {
7467
+ const AudioContextClass = getAudioContextClass();
7468
+ if (!AudioContextClass) {
7469
+ return null;
7470
+ }
7471
+ return new AudioContextClass;
6981
7472
  }
6982
- return error;
6983
- }
6984
- function normalizeRecordingLifecycleError(error) {
7473
+ };
7474
+
7475
+ class AudioNormalizer {
7476
+ audioContext = null;
7477
+ source = null;
7478
+ destination = null;
7479
+ dependencies;
7480
+ constructor(dependencies = {}) {
7481
+ this.dependencies = { ...DEFAULT_DEPENDENCIES2, ...dependencies };
7482
+ }
7483
+ start(track) {
7484
+ const audioContext = this.dependencies.createAudioContext();
7485
+ if (!audioContext) {
7486
+ return null;
7487
+ }
7488
+ const compressor = audioContext.createDynamicsCompressor();
7489
+ compressor.threshold.value = COMPRESSOR_THRESHOLD_DB;
7490
+ compressor.knee.value = COMPRESSOR_KNEE_DB;
7491
+ compressor.ratio.value = COMPRESSOR_RATIO;
7492
+ compressor.attack.value = COMPRESSOR_ATTACK_SECONDS;
7493
+ compressor.release.value = COMPRESSOR_RELEASE_SECONDS;
7494
+ const makeupGain = audioContext.createGain();
7495
+ makeupGain.gain.value = MAKEUP_GAIN;
7496
+ const limiter = audioContext.createDynamicsCompressor();
7497
+ limiter.threshold.value = LIMITER_THRESHOLD_DB;
7498
+ limiter.knee.value = LIMITER_KNEE_DB;
7499
+ limiter.ratio.value = LIMITER_RATIO;
7500
+ limiter.attack.value = LIMITER_ATTACK_SECONDS;
7501
+ limiter.release.value = LIMITER_RELEASE_SECONDS;
7502
+ const source = audioContext.createMediaStreamSource(new MediaStream([track]));
7503
+ const destination = audioContext.createMediaStreamDestination();
7504
+ source.connect(compressor);
7505
+ compressor.connect(makeupGain);
7506
+ makeupGain.connect(limiter);
7507
+ limiter.connect(destination);
7508
+ this.audioContext = audioContext;
7509
+ this.source = source;
7510
+ this.destination = destination;
7511
+ audioContext.resume().catch((error) => {
7512
+ logger.warn("[AudioNormalizer] Failed to resume AudioContext", {
7513
+ error: extractErrorMessage(error)
7514
+ });
7515
+ });
7516
+ const outputTrack = destination.stream.getAudioTracks()[0];
7517
+ if (!outputTrack) {
7518
+ this.close();
7519
+ return null;
7520
+ }
7521
+ return outputTrack;
7522
+ }
7523
+ close() {
7524
+ this.source?.disconnect();
7525
+ this.source = null;
7526
+ if (this.destination) {
7527
+ for (const track of this.destination.stream.getAudioTracks()) {
7528
+ track.stop();
7529
+ }
7530
+ this.destination = null;
7531
+ }
7532
+ const audioContext = this.audioContext;
7533
+ this.audioContext = null;
7534
+ if (!audioContext) {
7535
+ return;
7536
+ }
7537
+ audioContext.close().catch((error) => {
7538
+ logger.warn("[AudioNormalizer] Failed to close AudioContext", {
7539
+ error: extractErrorMessage(error)
7540
+ });
7541
+ });
7542
+ }
7543
+ }
7544
+
7545
+ // src/core/recording/recording-lifecycle-error.ts
7546
+ var ERROR_RECORDING_STOP_NOT_READY = "recording.stop-not-ready";
7547
+ var ERROR_RECORDING_FINALIZE_NOT_READY = "recording.finalize-not-ready";
7548
+ var ERROR_RECORDING_FINALIZE_TIMEOUT = "recording.finalize-timeout";
7549
+ var ERROR_RECORDING_UPLOAD_IN_PROGRESS = "recording.upload-in-progress";
7550
+ function createRecordingLifecycleError(code, message, details) {
7551
+ const error = new Error(`${message} [${code}]`);
7552
+ error.code = code;
7553
+ error.isRecoverable = true;
7554
+ if (details !== undefined) {
7555
+ error.details = details;
7556
+ }
7557
+ return error;
7558
+ }
7559
+ function normalizeRecordingLifecycleError(error) {
6985
7560
  if (!(error instanceof Error)) {
6986
7561
  return error;
6987
7562
  }
@@ -7201,7 +7776,8 @@ function createUnsupportedRouteError(routeDecision) {
7201
7776
  var DEFAULT_STREAM_RECORDING_STATE_DEPENDENCIES = {
7202
7777
  checkRecorderSupport,
7203
7778
  getCurrentTimestamp: () => performance.now(),
7204
- createAudioMixer: () => new SystemAudioMixer
7779
+ createAudioMixer: () => new SystemAudioMixer,
7780
+ createAudioNormalizer: () => new AudioNormalizer
7205
7781
  };
7206
7782
  function isLiveTrack(track) {
7207
7783
  return !!track && track.readyState === "live";
@@ -7236,6 +7812,8 @@ class StreamRecordingState {
7236
7812
  isSystemAudioCaptureEnabled = false;
7237
7813
  systemAudioTrack = null;
7238
7814
  audioMixer = null;
7815
+ audioNormalizer = null;
7816
+ isAudioOnlyRecording = false;
7239
7817
  streamManager;
7240
7818
  dependencies;
7241
7819
  constructor(streamManager, dependencies) {
@@ -7257,7 +7835,8 @@ class StreamRecordingState {
7257
7835
  this.dependencies = {
7258
7836
  checkRecorderSupport: resolvedCheckRecorderSupportDependency,
7259
7837
  getCurrentTimestamp: resolvedGetCurrentTimestampDependency,
7260
- createAudioMixer: dependencies?.createAudioMixer ?? DEFAULT_STREAM_RECORDING_STATE_DEPENDENCIES.createAudioMixer
7838
+ createAudioMixer: dependencies?.createAudioMixer ?? DEFAULT_STREAM_RECORDING_STATE_DEPENDENCIES.createAudioMixer,
7839
+ createAudioNormalizer: dependencies?.createAudioNormalizer ?? DEFAULT_STREAM_RECORDING_STATE_DEPENDENCIES.createAudioNormalizer
7261
7840
  };
7262
7841
  }
7263
7842
  setPreResolvedSupportReport(report) {
@@ -7328,7 +7907,8 @@ class StreamRecordingState {
7328
7907
  if (this.activeRouteDecision.route === "unsupported-with-guidance") {
7329
7908
  throw createUnsupportedRouteError(this.activeRouteDecision);
7330
7909
  }
7331
- if (this.activeRouteDecision.route === "local-webcodecs") {
7910
+ this.isAudioOnlyRecording = config.mediaType === "audio";
7911
+ if (this.activeRouteDecision.route === "local-webcodecs" && !this.isAudioOnlyRecording) {
7332
7912
  const requiresWatermark = config.watermark !== undefined;
7333
7913
  let supportReport;
7334
7914
  if (this.preResolvedSupportReport?.isSupported) {
@@ -7380,13 +7960,14 @@ class StreamRecordingState {
7380
7960
  overlayConfig
7381
7961
  });
7382
7962
  if (this.activeRouteDecision.route === "safe-capture-post-recording-transcode") {
7383
- await this.startSafeCaptureRecording(mediaStream, config);
7963
+ await this.startSafeCaptureRecording(this.buildNormalizedStream(mediaStream, config), config);
7384
7964
  logger.info("[StreamRecordingState] MediaRecorder capture started for post-recording transcode");
7385
7965
  } else {
7386
7966
  logger.debug("[StreamRecordingState] Starting processing");
7387
- const recordingStream = this.buildRecordingStream(mediaStream, processor);
7967
+ const recordingStream = this.isAudioOnlyRecording ? this.buildNormalizedStream(mediaStream, config) : this.buildRecordingStream(mediaStream, processor);
7388
7968
  await processor.startProcessing(recordingStream, config, overlayConfig).catch((error) => {
7389
7969
  this.releaseAudioMixer();
7970
+ this.releaseAudioNormalizer();
7390
7971
  throw error;
7391
7972
  });
7392
7973
  logger.info("[StreamRecordingState] Processing started and worker ready");
@@ -7444,9 +8025,13 @@ class StreamRecordingState {
7444
8025
  expectedDurationSeconds: safeCaptureResult.expectedDurationSeconds,
7445
8026
  mediaRecorderDiagnostics: safeCaptureResult.diagnostics
7446
8027
  };
8028
+ this.releaseAudioNormalizer();
7447
8029
  } else {
7448
8030
  logger.debug("[StreamRecordingState] Finalizing stream processor");
7449
- result = await this.streamProcessor.finalize().finally(() => this.releaseAudioMixer());
8031
+ result = await this.streamProcessor.finalize().finally(() => {
8032
+ this.releaseAudioMixer();
8033
+ this.releaseAudioNormalizer();
8034
+ });
7450
8035
  logger.info("[StreamRecordingState] Stream processor finalized", {
7451
8036
  blobSize: result.blob.size,
7452
8037
  hasBlob: !!result.blob
@@ -7457,6 +8042,7 @@ class StreamRecordingState {
7457
8042
  blob: result.blob,
7458
8043
  mimeType: result.blob.type || "video/mp4"
7459
8044
  });
8045
+ this.isAudioOnlyRecording = false;
7460
8046
  return {
7461
8047
  blob: result.blob,
7462
8048
  captureFrameRate: result.captureFrameRate,
@@ -7464,7 +8050,8 @@ class StreamRecordingState {
7464
8050
  tabVisibilityIntervals,
7465
8051
  recordingStats: result.recordingStats,
7466
8052
  encoderAcceleration: result.encoderAcceleration,
7467
- mediaRecorderDiagnostics: result.mediaRecorderDiagnostics
8053
+ mediaRecorderDiagnostics: result.mediaRecorderDiagnostics,
8054
+ audio: result.audio
7468
8055
  };
7469
8056
  }
7470
8057
  pauseRecording() {
@@ -7564,6 +8151,28 @@ class StreamRecordingState {
7564
8151
  });
7565
8152
  return new MediaStream([...mediaStream.getVideoTracks(), mixedTrack]);
7566
8153
  }
8154
+ buildNormalizedStream(mediaStream, config) {
8155
+ const microphone = mediaStream.getAudioTracks()[0];
8156
+ const shouldNormalize = this.isAudioOnlyRecording && config.audioRecording?.normalize === true;
8157
+ if (!(shouldNormalize && isLiveTrack(microphone))) {
8158
+ return mediaStream;
8159
+ }
8160
+ const normalizer = this.dependencies.createAudioNormalizer();
8161
+ const normalizedTrack = normalizer.start(microphone);
8162
+ if (!normalizedTrack) {
8163
+ logger.warn("[StreamRecordingState] Audio normalization unavailable, recording raw microphone");
8164
+ return mediaStream;
8165
+ }
8166
+ this.audioNormalizer = normalizer;
8167
+ return new MediaStream([normalizedTrack]);
8168
+ }
8169
+ releaseAudioNormalizer() {
8170
+ if (!this.audioNormalizer) {
8171
+ return;
8172
+ }
8173
+ this.audioNormalizer.close();
8174
+ this.audioNormalizer = null;
8175
+ }
7567
8176
  releaseAudioMixer() {
7568
8177
  if (!this.audioMixer) {
7569
8178
  return;
@@ -7831,6 +8440,15 @@ class StreamRecordingState {
7831
8440
  }
7832
8441
  return MediaRecorder.isTypeSupported(mimeType);
7833
8442
  };
8443
+ if (this.isAudioOnlyRecording) {
8444
+ return [
8445
+ "audio/webm;codecs=opus",
8446
+ "audio/mp4;codecs=mp4a.40.2",
8447
+ "audio/mp4",
8448
+ "audio/webm",
8449
+ "audio/ogg;codecs=opus"
8450
+ ].filter(canUseMimeType);
8451
+ }
7834
8452
  const webmCandidateMimeTypes = [
7835
8453
  "video/webm;codecs=vp8,opus",
7836
8454
  "video/webm;codecs=vp9,opus",
@@ -8024,6 +8642,8 @@ class StreamRecordingState {
8024
8642
  this.clearMediaRecorderErrorTimeout();
8025
8643
  this.activeRouteDecision = DEFAULT_ROUTE_DECISION;
8026
8644
  this.releaseAudioMixer();
8645
+ this.releaseAudioNormalizer();
8646
+ this.isAudioOnlyRecording = false;
8027
8647
  if (this.streamProcessor) {
8028
8648
  this.streamProcessor.destroy();
8029
8649
  this.streamProcessor = null;
@@ -8091,14 +8711,17 @@ class CameraStreamManager {
8091
8711
  async startStreamWithVideoTrack(videoTrack) {
8092
8712
  return await this.streamManager.startStreamWithVideoTrack(videoTrack);
8093
8713
  }
8714
+ async startAudioOnlyStream(audioConstraints) {
8715
+ return await this.streamManager.startAudioOnlyStream(audioConstraints);
8716
+ }
8094
8717
  stopStream() {
8095
8718
  this.streamManager.stopStream();
8096
8719
  }
8097
8720
  switchVideoDevice(deviceId) {
8098
8721
  return this.streamManager.switchVideoDevice(deviceId);
8099
8722
  }
8100
- async switchAudioDevice(deviceId) {
8101
- const stream = await this.streamManager.switchAudioDevice(deviceId);
8723
+ async switchAudioDevice(deviceId, audioConstraints) {
8724
+ const stream = await this.streamManager.switchAudioDevice(deviceId, audioConstraints);
8102
8725
  this.recordingState.updateMixedMicrophone(stream);
8103
8726
  return stream;
8104
8727
  }
@@ -8426,7 +9049,7 @@ function resolveDeviceError(input) {
8426
9049
  // package.json
8427
9050
  var package_default = {
8428
9051
  name: "@vidtreo/recorder",
8429
- version: "1.8.1",
9052
+ version: "1.9.0",
8430
9053
  type: "module",
8431
9054
  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.",
8432
9055
  main: "./dist/index.js",
@@ -9995,6 +10618,8 @@ function easeToward(start, ceiling, elapsed, easingMs) {
9995
10618
 
9996
10619
  // src/core/upload/upload-service.ts
9997
10620
  var API_DIRECT_UPLOAD_PATH = "/api/v1/videos/uploads";
10621
+ var API_AUDIO_DIRECT_UPLOAD_PATH = "/api/v1/audios/uploads";
10622
+ var API_AUDIO_PATH = "/api/v1/audios";
9998
10623
  var TRAILING_SLASHES = /\/+$/;
9999
10624
  var MIME_TYPE_PARAMETER_SEPARATOR = ";";
10000
10625
  var MAX_TRANSFER_PROGRESS = 0.99;
@@ -10035,6 +10660,18 @@ var VIDEO_TRANSFER_STATUSES = new Set([
10035
10660
  "transferred",
10036
10661
  "failed"
10037
10662
  ]);
10663
+ var UPLOAD_RESOURCES = {
10664
+ video: {
10665
+ basePath: API_DIRECT_UPLOAD_PATH,
10666
+ entityKey: "video",
10667
+ idKey: "videoId"
10668
+ },
10669
+ audio: {
10670
+ basePath: API_AUDIO_DIRECT_UPLOAD_PATH,
10671
+ entityKey: "audio",
10672
+ idKey: "audioId"
10673
+ }
10674
+ };
10038
10675
  var UPLOAD_CANCELLED_CODE = "upload.cancelled";
10039
10676
  function createCancelledError() {
10040
10677
  return Object.assign(new Error("Upload was cancelled"), {
@@ -10075,6 +10712,9 @@ class VideoUploadService {
10075
10712
  if (options.duration !== undefined && (!Number.isFinite(options.duration) || options.duration <= 0)) {
10076
10713
  throw new Error("Duration must be a finite positive number");
10077
10714
  }
10715
+ if (options.mediaType === "audio" && options.audio === undefined) {
10716
+ throw new Error("Audio details are required for audio uploads");
10717
+ }
10078
10718
  const filename = options.filename;
10079
10719
  const idempotencyKey = this.resolveIdempotencyKey(options.idempotencyKey);
10080
10720
  const mimeType = this.resolveUploadMimeType(blob, options.mimeType);
@@ -10092,7 +10732,7 @@ class VideoUploadService {
10092
10732
  return this.hashInitialParts(blob, hasher).then((checksums) => this.initiateUpload(blob, options, filename, idempotencyKey, mimeType, duration, checksums).then((initiation) => ({ initiation, checksums }))).then(({ initiation, checksums }) => {
10093
10733
  if (initiation.upload === null) {
10094
10734
  this.startFinalizing(reporter, blob.size);
10095
- return this.waitForTerminalUpload(options, initiation.video).then((result) => {
10735
+ return this.waitForTerminalUpload(options, initiation.video).then((result) => this.uploadPeaksForResult(options, result)).then((result) => {
10096
10736
  this.reportCompletion(reporter, blob.size, blob.size);
10097
10737
  return result;
10098
10738
  });
@@ -10102,7 +10742,7 @@ class VideoUploadService {
10102
10742
  return this.transferToR2(blob, hasher, descriptor, options, reporter, checksums, initiation.authorizations).then((progress) => {
10103
10743
  assertNotCancelled(options.signal);
10104
10744
  this.startFinalizing(reporter, progress.totalBytes);
10105
- return this.confirmUpload(options, descriptor.videoId).then((result) => this.waitForTerminalUpload(options, result)).then((result) => {
10745
+ return this.confirmUpload(options, descriptor.mediaId).then((result) => this.waitForTerminalUpload(options, result)).then((result) => this.uploadPeaksForResult(options, result)).then((result) => {
10106
10746
  this.reportCompletion(reporter, progress.loadedBytes, progress.totalBytes);
10107
10747
  return result;
10108
10748
  });
@@ -10122,13 +10762,19 @@ class VideoUploadService {
10122
10762
  mimeType,
10123
10763
  duration
10124
10764
  };
10765
+ if (options.mediaType === "audio" && options.audio !== undefined) {
10766
+ request.format = options.audio.format;
10767
+ request.bitrate = options.audio.bitrate;
10768
+ request.channels = options.audio.channels;
10769
+ request.sampleRate = options.audio.sampleRate;
10770
+ }
10125
10771
  if (options.userMetadata !== undefined) {
10126
10772
  request.userMetadata = options.userMetadata;
10127
10773
  }
10128
10774
  if (checksums.size > 0) {
10129
10775
  request.parts = this.buildPartManifests(checksums, [...checksums.keys()]);
10130
10776
  }
10131
- return this.requestAuthenticatedMetadata(options, this.buildBackendUrl(options.backendUrl, API_DIRECT_UPLOAD_PATH), {
10777
+ return this.requestAuthenticatedMetadata(options, this.buildBackendUrl(options.backendUrl, this.resolveUploadResource(options).basePath), {
10132
10778
  method: "POST",
10133
10779
  credentials: "omit",
10134
10780
  headers: {
@@ -10136,7 +10782,7 @@ class VideoUploadService {
10136
10782
  "Idempotency-Key": idempotencyKey
10137
10783
  },
10138
10784
  body: JSON.stringify(request)
10139
- }, "Upload initiation").then((response) => this.parseInitiation(response, filename, blob.size, mimeType, checksums));
10785
+ }, "Upload initiation").then((response) => this.parseInitiation(response, this.resolveUploadResource(options), filename, blob.size, mimeType, checksums));
10140
10786
  }
10141
10787
  transferToR2(blob, hasher, descriptor, options, reporter, checksums, authorizations) {
10142
10788
  if (descriptor.partSize !== calculateDirectUploadPartSize(blob.size)) {
@@ -10305,7 +10951,7 @@ class VideoUploadService {
10305
10951
  for (const partNumber of partNumbers) {
10306
10952
  session.signingParts.add(partNumber);
10307
10953
  }
10308
- const request = session.signing.then(() => this.hashParts(session.hasher, session.blob, session.descriptor.partSize, partNumbers, session.checksums)).then((checksums) => this.authorizeParts(session.options, session.descriptor.videoId, this.buildPartManifests(checksums, partNumbers))).then((authorizations) => {
10954
+ const request = session.signing.then(() => this.hashParts(session.hasher, session.blob, session.descriptor.partSize, partNumbers, session.checksums)).then((checksums) => this.authorizeParts(session.options, session.descriptor.mediaId, this.buildPartManifests(checksums, partNumbers))).then((authorizations) => {
10309
10955
  for (const authorization of authorizations.values()) {
10310
10956
  session.authorizations.set(authorization.partNumber, authorization);
10311
10957
  }
@@ -10326,8 +10972,8 @@ class VideoUploadService {
10326
10972
  isAuthorizationFresh(authorization) {
10327
10973
  return Date.parse(authorization.expiresAt) - Date.now() > AUTHORIZATION_REFRESH_MARGIN_MS;
10328
10974
  }
10329
- authorizeParts(options, videoId, manifests) {
10330
- const path = `${API_DIRECT_UPLOAD_PATH}/${encodeURIComponent(videoId)}/parts`;
10975
+ authorizeParts(options, mediaId, manifests) {
10976
+ const path = `${this.resolveUploadResource(options).basePath}/${encodeURIComponent(mediaId)}/parts`;
10331
10977
  return this.requestAuthenticatedMetadata(options, this.buildBackendUrl(options.backendUrl, path), {
10332
10978
  method: "POST",
10333
10979
  credentials: "omit",
@@ -10474,15 +11120,15 @@ class VideoUploadService {
10474
11120
  const start = (partNumber - 1) * partSize;
10475
11121
  return blob.slice(start, Math.min(start + partSize, blob.size));
10476
11122
  }
10477
- confirmUpload(options, videoId) {
10478
- const path = `${API_DIRECT_UPLOAD_PATH}/${encodeURIComponent(videoId)}/confirm`;
11123
+ confirmUpload(options, mediaId) {
11124
+ const path = `${this.resolveUploadResource(options).basePath}/${encodeURIComponent(mediaId)}/confirm`;
10479
11125
  return this.requestAuthenticatedMetadata(options, this.buildBackendUrl(options.backendUrl, path), {
10480
11126
  method: "POST",
10481
11127
  credentials: "omit"
10482
11128
  }, "Upload confirmation").then((response) => this.parseVideoEnvelope(response));
10483
11129
  }
10484
- getUploadStatus(options, videoId) {
10485
- const path = `${API_DIRECT_UPLOAD_PATH}/${encodeURIComponent(videoId)}`;
11130
+ getUploadStatus(options, mediaId) {
11131
+ const path = `${this.resolveUploadResource(options).basePath}/${encodeURIComponent(mediaId)}`;
10486
11132
  return this.requestAuthenticatedMetadata(options, this.buildBackendUrl(options.backendUrl, path), {
10487
11133
  method: "GET",
10488
11134
  credentials: "omit"
@@ -10501,6 +11147,45 @@ class VideoUploadService {
10501
11147
  const nextDelay = Math.min(delay * STATUS_POLL_BACKOFF_FACTOR, STATUS_POLL_MAX_DELAY_MS);
10502
11148
  return this.waitForStatusPoll(delay).then(() => this.getUploadStatus(options, result.id)).then((nextResult) => this.waitForTerminalUpload(options, nextResult, deadline, nextDelay));
10503
11149
  }
11150
+ resolveUploadResource(options) {
11151
+ if (options.mediaType === "audio") {
11152
+ return UPLOAD_RESOURCES.audio;
11153
+ }
11154
+ return UPLOAD_RESOURCES.video;
11155
+ }
11156
+ uploadPeaksForResult(options, result) {
11157
+ const peaks = options.audio?.peaks;
11158
+ if (options.mediaType !== "audio" || !peaks) {
11159
+ return Promise.resolve(result);
11160
+ }
11161
+ return this.uploadAudioPeaks(options, result.id, peaks).then(() => result, (error) => {
11162
+ logger.warn("[VideoUploadService] Audio peaks upload failed", {
11163
+ audioId: result.id,
11164
+ error: extractErrorMessage(error)
11165
+ });
11166
+ return result;
11167
+ });
11168
+ }
11169
+ uploadAudioPeaks(options, audioId, peaks, hasRetriedAuthentication = false) {
11170
+ const path = `${API_AUDIO_PATH}/${encodeURIComponent(audioId)}/peaks`;
11171
+ return this.resolveAuthorization(options).then((authorization) => this.requestMetadataResponse(this.buildBackendUrl(options.backendUrl, path), {
11172
+ method: "PUT",
11173
+ credentials: "omit",
11174
+ headers: {
11175
+ Authorization: authorization,
11176
+ "Content-Type": "application/json"
11177
+ },
11178
+ body: JSON.stringify(peaks)
11179
+ }, "Audio peaks upload")).then((response) => {
11180
+ if (response.ok) {
11181
+ return;
11182
+ }
11183
+ if (response.status === 401 && options.getAccessToken !== undefined && !hasRetriedAuthentication) {
11184
+ return this.uploadAudioPeaks(options, audioId, peaks, true);
11185
+ }
11186
+ throw new Error(`Audio peaks upload failed: ${response.status} ${response.statusText}`);
11187
+ });
11188
+ }
10504
11189
  waitForStatusPoll(delay) {
10505
11190
  return new Promise((resolve) => {
10506
11191
  window.setTimeout(resolve, delay);
@@ -10587,12 +11272,12 @@ class VideoUploadService {
10587
11272
  throw new Error(`${operation} response is invalid JSON`);
10588
11273
  });
10589
11274
  }
10590
- parseInitiation(response, expectedFilename, expectedFileSize, expectedMimeType, requestedChecksums) {
11275
+ parseInitiation(response, resource, expectedFilename, expectedFileSize, expectedMimeType, requestedChecksums) {
10591
11276
  const data = response.data;
10592
11277
  if (!this.isRecord(data)) {
10593
11278
  throw new Error("Upload initiation response is missing data");
10594
11279
  }
10595
- const video = this.parseVideoResult(data.video, "Upload initiation");
11280
+ const video = this.parseVideoResult(data[resource.entityKey], "Upload initiation");
10596
11281
  if (video.filename !== expectedFilename || video.fileSize !== expectedFileSize || video.mimeType !== expectedMimeType) {
10597
11282
  throw new Error("Upload initiation video does not match request");
10598
11283
  }
@@ -10602,8 +11287,8 @@ class VideoUploadService {
10602
11287
  if (!this.isRecord(data.upload)) {
10603
11288
  throw new Error("Upload initiation response has invalid authorization");
10604
11289
  }
10605
- const upload = this.parseDescriptor(data.upload, expectedFileSize);
10606
- if (upload.videoId !== video.id) {
11290
+ const upload = this.parseDescriptor(data.upload, resource.idKey, expectedFileSize);
11291
+ if (upload.mediaId !== video.id) {
10607
11292
  throw new Error("Upload authorization video does not match initiation");
10608
11293
  }
10609
11294
  if (data.upload.parts === undefined) {
@@ -10653,8 +11338,9 @@ class VideoUploadService {
10653
11338
  isOptionalTimestamp(value) {
10654
11339
  return value === undefined || value === null || typeof value === "string" && Number.isFinite(Date.parse(value));
10655
11340
  }
10656
- parseDescriptor(value, expectedFileSize) {
10657
- if (typeof value.videoId !== "string" || value.videoId.length === 0 || typeof value.partSize !== "number" || !Number.isSafeInteger(value.partSize) || value.partSize <= 0 || typeof value.partCount !== "number" || !Number.isSafeInteger(value.partCount) || value.partCount <= 0 || value.partCount > 1e4 || !Array.isArray(value.uploadedParts)) {
11341
+ parseDescriptor(value, idKey, expectedFileSize) {
11342
+ const mediaId = value[idKey];
11343
+ if (typeof mediaId !== "string" || mediaId.length === 0 || typeof value.partSize !== "number" || !Number.isSafeInteger(value.partSize) || value.partSize <= 0 || typeof value.partCount !== "number" || !Number.isSafeInteger(value.partCount) || value.partCount <= 0 || value.partCount > 1e4 || !Array.isArray(value.uploadedParts)) {
10658
11344
  throw new Error("Upload initiation response has invalid authorization");
10659
11345
  }
10660
11346
  if (Math.ceil(expectedFileSize / value.partSize) !== value.partCount) {
@@ -10670,7 +11356,7 @@ class VideoUploadService {
10670
11356
  previousPart = partNumber;
10671
11357
  }
10672
11358
  return {
10673
- videoId: value.videoId,
11359
+ mediaId,
10674
11360
  partSize: value.partSize,
10675
11361
  partCount: value.partCount,
10676
11362
  uploadedParts
@@ -10964,36 +11650,8 @@ function createSourceSwitchCallbacks(callbacks, dependencies) {
10964
11650
  };
10965
11651
  }
10966
11652
 
10967
- // src/core/recording/recording-filename.ts
10968
- var MAX_TITLE_LENGTH = 120;
10969
- var CONTROL_CHARACTERS = /\p{Cc}/gu;
10970
- var FORBIDDEN_CHARACTERS = /[/\\*?"<>|]/g;
10971
- var WHITESPACE = /\s+/g;
10972
- var LEADING_DOTS = /^[.\s]+/;
10973
- function isHighSurrogate(code) {
10974
- return code >= 55296 && code <= 56319;
10975
- }
10976
- function truncateTitle(title) {
10977
- if (title.length <= MAX_TITLE_LENGTH) {
10978
- return title;
10979
- }
10980
- let truncated = title.slice(0, MAX_TITLE_LENGTH);
10981
- if (isHighSurrogate(truncated.charCodeAt(truncated.length - 1))) {
10982
- truncated = truncated.slice(0, -1);
10983
- }
10984
- return truncated.trimEnd();
10985
- }
10986
- function sanitizeRecordingTitle(title) {
10987
- const cleaned = title.replace(CONTROL_CHARACTERS, " ").replace(FORBIDDEN_CHARACTERS, "").replace(WHITESPACE, " ").replace(LEADING_DOTS, "").trim();
10988
- return truncateTitle(cleaned);
10989
- }
10990
- function createRecordingFilename(title, extension) {
10991
- const sanitized = title === null ? "" : sanitizeRecordingTitle(title);
10992
- if (sanitized === "") {
10993
- return `recording-${Date.now()}.${extension}`;
10994
- }
10995
- return `${sanitized}.${extension}`;
10996
- }
11653
+ // src/core/processor/worker-processor.ts
11654
+ import { canEncodeAudio as canEncodeAudio2 } from "mediabunny";
10997
11655
 
10998
11656
  // src/core/audio/audio-health-monitor.ts
10999
11657
  var DEFAULT_SILENCE_THRESHOLD = 0.001;
@@ -11027,6 +11685,7 @@ class AudioHealthMonitor {
11027
11685
  mutedDurationMs = 0;
11028
11686
  currentMuted = false;
11029
11687
  hasHealthySignal = false;
11688
+ pausedAtMs = null;
11030
11689
  constructor(options = {}) {
11031
11690
  this.silenceThreshold = this.resolveNumber(options.silenceThreshold, DEFAULT_SILENCE_THRESHOLD);
11032
11691
  this.lowSignalThreshold = this.resolveNumber(options.lowSignalThreshold, DEFAULT_LOW_SIGNAL_THRESHOLD);
@@ -11079,9 +11738,29 @@ class AudioHealthMonitor {
11079
11738
  return this.snapshot(input.timestampMs, input.isMuted);
11080
11739
  }
11081
11740
  inspect(timestampMs, isMuted) {
11082
- this.markStarted(timestampMs);
11083
- this.trackMuteState(timestampMs, isMuted);
11084
- return this.snapshot(timestampMs, isMuted);
11741
+ const effectiveTimestampMs = this.pausedAtMs ?? timestampMs;
11742
+ this.markStarted(effectiveTimestampMs);
11743
+ this.trackMuteState(effectiveTimestampMs, isMuted);
11744
+ return this.snapshot(effectiveTimestampMs, isMuted);
11745
+ }
11746
+ pause(timestampMs) {
11747
+ if (this.pausedAtMs !== null) {
11748
+ return;
11749
+ }
11750
+ this.pausedAtMs = timestampMs;
11751
+ }
11752
+ resume(timestampMs) {
11753
+ if (this.pausedAtMs === null) {
11754
+ return;
11755
+ }
11756
+ const pausedDurationMs = Math.max(0, timestampMs - this.pausedAtMs);
11757
+ this.pausedAtMs = null;
11758
+ this.startedAtMs = shiftTimestamp(this.startedAtMs, pausedDurationMs);
11759
+ this.lastChunkTimestampMs = shiftTimestamp(this.lastChunkTimestampMs, pausedDurationMs);
11760
+ this.silentStartedAtMs = shiftTimestamp(this.silentStartedAtMs, pausedDurationMs);
11761
+ this.lowSignalStartedAtMs = shiftTimestamp(this.lowSignalStartedAtMs, pausedDurationMs);
11762
+ this.mutedSpeechStartedAtMs = shiftTimestamp(this.mutedSpeechStartedAtMs, pausedDurationMs);
11763
+ this.mutedStartedAtMs = shiftTimestamp(this.mutedStartedAtMs, pausedDurationMs);
11085
11764
  }
11086
11765
  reset() {
11087
11766
  this.totalChunks = 0;
@@ -11100,6 +11779,7 @@ class AudioHealthMonitor {
11100
11779
  this.mutedDurationMs = 0;
11101
11780
  this.currentMuted = false;
11102
11781
  this.hasHealthySignal = false;
11782
+ this.pausedAtMs = null;
11103
11783
  }
11104
11784
  snapshot(timestampMs, isMuted) {
11105
11785
  const consecutiveSilentDurationMs = this.resolveSilentDuration(timestampMs);
@@ -11241,6 +11921,12 @@ class AudioHealthMonitor {
11241
11921
  return value;
11242
11922
  }
11243
11923
  }
11924
+ function shiftTimestamp(timestampMs, offsetMs) {
11925
+ if (timestampMs === null) {
11926
+ return null;
11927
+ }
11928
+ return timestampMs + offsetMs;
11929
+ }
11244
11930
 
11245
11931
  // src/core/processor/bitrate-utils.ts
11246
11932
  import {
@@ -11300,8 +11986,11 @@ const AUDIO_WORKLET_MESSAGE_TYPE_SET_PAUSED = "${AUDIO_WORKLET_MESSAGE_TYPE_SET_
11300
11986
  const AUDIO_WORKLET_MESSAGE_TYPE_SHUTDOWN = "${AUDIO_WORKLET_MESSAGE_TYPE_SHUTDOWN}";
11301
11987
 
11302
11988
  class VidtreoAudioWorkletProcessor extends AudioWorkletProcessor {
11303
- constructor() {
11989
+ constructor(options) {
11304
11990
  super();
11991
+ const processorOptions = options && options.processorOptions;
11992
+ this.channelCount =
11993
+ processorOptions && processorOptions.channelCount === 2 ? 2 : 1;
11305
11994
  this.isMuted = false;
11306
11995
  this.isPaused = false;
11307
11996
  this.processedFrames = 0;
@@ -11365,10 +12054,13 @@ class VidtreoAudioWorkletProcessor extends AudioWorkletProcessor {
11365
12054
  }
11366
12055
 
11367
12056
  const frames = firstChannel.length;
11368
- const data = new Float32Array(frames);
12057
+ const data = new Float32Array(frames * this.channelCount);
11369
12058
 
11370
- data.set(firstChannel);
11371
- const inputStats = this.calculateStats(data);
12059
+ for (let channel = 0; channel < this.channelCount; channel += 1) {
12060
+ const channelData = inputGroup[channel] || firstChannel;
12061
+ data.set(channelData, channel * frames);
12062
+ }
12063
+ const inputStats = this.calculateStats(firstChannel);
11372
12064
 
11373
12065
  if (this.isMuted) {
11374
12066
  data.fill(0);
@@ -11382,7 +12074,7 @@ class VidtreoAudioWorkletProcessor extends AudioWorkletProcessor {
11382
12074
  type: AUDIO_WORKLET_MESSAGE_TYPE_AUDIO_CHUNK,
11383
12075
  data,
11384
12076
  frames,
11385
- numberOfChannels: 1,
12077
+ numberOfChannels: this.channelCount,
11386
12078
  sampleRate,
11387
12079
  timestamp,
11388
12080
  inputPeak: inputStats.peak,
@@ -11431,7 +12123,7 @@ class AudioWorkletController {
11431
12123
  isPaused = false;
11432
12124
  isProcessing = false;
11433
12125
  hasAudioWorkletUrlLease = false;
11434
- async initialize(audioStream, onAudioChunk) {
12126
+ async initialize(audioStream, onAudioChunk, channelCount = RECORDING_AUDIO_CHANNELS) {
11435
12127
  if (!audioStream) {
11436
12128
  throw new Error("Audio stream is required");
11437
12129
  }
@@ -11459,7 +12151,8 @@ class AudioWorkletController {
11459
12151
  const audioWorkletNode = new AudioWorkletNode(audioContext, AUDIO_WORKLET_PROCESSOR_NAME, {
11460
12152
  numberOfInputs: AUDIO_WORKLET_INPUT_COUNT,
11461
12153
  numberOfOutputs: AUDIO_WORKLET_OUTPUT_COUNT,
11462
- outputChannelCount: [RECORDING_AUDIO_CHANNELS]
12154
+ outputChannelCount: [RECORDING_AUDIO_CHANNELS],
12155
+ processorOptions: { channelCount }
11463
12156
  });
11464
12157
  audioWorkletNode.port.onmessage = this.handleWorkletMessage.bind(this);
11465
12158
  this.audioWorkletNode = audioWorkletNode;
@@ -11468,7 +12161,7 @@ class AudioWorkletController {
11468
12161
  this.audioDestinationNode.gain.value = SILENT_GAIN_VALUE;
11469
12162
  const audioConfig = {
11470
12163
  sampleRate: audioContext.sampleRate,
11471
- numberOfChannels: RECORDING_AUDIO_CHANNELS,
12164
+ numberOfChannels: channelCount,
11472
12165
  format: WORKER_AUDIO_SAMPLE_FORMAT_F32_PLANAR
11473
12166
  };
11474
12167
  this.audioConfig = audioConfig;
@@ -11633,7 +12326,7 @@ class AudioWorkletManager {
11633
12326
  }
11634
12327
  this.createController = createController;
11635
12328
  }
11636
- async prepareAudioConfig(audioTrack) {
12329
+ async prepareAudioConfig(audioTrack, channelCount) {
11637
12330
  if (!audioTrack) {
11638
12331
  this.audioWorkletConfig = null;
11639
12332
  return null;
@@ -11642,7 +12335,7 @@ class AudioWorkletManager {
11642
12335
  this.audioWorkletController = this.createController();
11643
12336
  }
11644
12337
  const audioStream = new MediaStream([audioTrack]);
11645
- this.audioWorkletConfig = await this.audioWorkletController.initialize(audioStream, (chunk) => this.onChunk(chunk)).catch((error) => {
12338
+ this.audioWorkletConfig = await this.audioWorkletController.initialize(audioStream, (chunk) => this.onChunk(chunk), channelCount).catch((error) => {
11646
12339
  this.stop();
11647
12340
  throw error;
11648
12341
  });
@@ -12178,6 +12871,16 @@ var workerCode = `(() => {
12178
12871
  const transformedHeight = Math.abs(b) * width + Math.abs(d) * height;
12179
12872
  return multiplyMatrices(multiplyMatrices(translationMatrix(-width / 2, -height / 2), linear), translationMatrix(transformedWidth / 2, transformedHeight / 2));
12180
12873
  };
12874
+ var extractRotationFromMatrix = (matrix) => {
12875
+ const [a, b] = matrix;
12876
+ const radians = Math.atan2(b, matrixIsFlipped(matrix) ? -a : a);
12877
+ return normalizeRotation(roundToMultiple(radians * RAD_TO_DEG, 90));
12878
+ };
12879
+ var matrixIsFlipped = (matrix) => {
12880
+ const [a, b, , c, d] = matrix;
12881
+ const det = a * d - b * c;
12882
+ return det < 0;
12883
+ };
12181
12884
  var rotationMatrix = (rotationInDegrees) => {
12182
12885
  const theta = rotationInDegrees * DEG_TO_RAD;
12183
12886
  const cosTheta = Math.round(Math.cos(theta));
@@ -12259,6 +12962,16 @@ var workerCode = `(() => {
12259
12962
  const codeNum = readExpGolomb(bitstream);
12260
12963
  return (codeNum & 1) === 0 ? -(codeNum >> 1) : codeNum + 1 >> 1;
12261
12964
  };
12965
+ var writeBits = (bytes, start, end, value) => {
12966
+ for (let i = start;i < end; i++) {
12967
+ const byteIndex = Math.floor(i / 8);
12968
+ let byte = bytes[byteIndex];
12969
+ const bitIndex = 7 - (i & 7);
12970
+ byte &= ~(1 << bitIndex);
12971
+ byte |= (value & 1 << end - i - 1) >> end - i - 1 << bitIndex;
12972
+ bytes[byteIndex] = byte;
12973
+ }
12974
+ };
12262
12975
  var toUint8Array = (source) => {
12263
12976
  if (source.constructor === Uint8Array) {
12264
12977
  return source;
@@ -12648,6 +13361,43 @@ var workerCode = `(() => {
12648
13361
  yield { key, value };
12649
13362
  }
12650
13363
  };
13364
+ var imageMimeTypeToExtension = (mimeType) => {
13365
+ switch (mimeType.toLowerCase()) {
13366
+ case "image/jpeg":
13367
+ case "image/jpg":
13368
+ return ".jpg";
13369
+ case "image/png":
13370
+ return ".png";
13371
+ case "image/gif":
13372
+ return ".gif";
13373
+ case "image/webp":
13374
+ return ".webp";
13375
+ case "image/bmp":
13376
+ return ".bmp";
13377
+ case "image/svg+xml":
13378
+ return ".svg";
13379
+ case "image/tiff":
13380
+ return ".tiff";
13381
+ case "image/avif":
13382
+ return ".avif";
13383
+ case "image/x-icon":
13384
+ case "image/vnd.microsoft.icon":
13385
+ return ".ico";
13386
+ default:
13387
+ return null;
13388
+ }
13389
+ };
13390
+ var uint8ArraysAreEqual = (a, b) => {
13391
+ if (a.length !== b.length) {
13392
+ return false;
13393
+ }
13394
+ for (let i = 0;i < a.length; i++) {
13395
+ if (a[i] !== b[i]) {
13396
+ return false;
13397
+ }
13398
+ }
13399
+ return true;
13400
+ };
12651
13401
  var polyfillSymbolDispose = () => {
12652
13402
  Symbol.dispose ??= Symbol("Symbol.dispose");
12653
13403
  };
@@ -15415,6 +16165,27 @@ var workerCode = `(() => {
15415
16165
  }
15416
16166
  throw new TypeError(\`Unhandled codec '\${String(codec)}'.\`);
15417
16167
  };
16168
+ var generateVp9CodecConfigurationFromCodecString = (codecString) => {
16169
+ const parts = codecString.split(".");
16170
+ const profile = Number(parts[1]);
16171
+ const level = Number(parts[2]);
16172
+ const bitDepth = Number(parts[3]);
16173
+ const chromaSubsampling = parts[4] ? Number(parts[4]) : 1;
16174
+ return [
16175
+ 1,
16176
+ 1,
16177
+ profile,
16178
+ 2,
16179
+ 1,
16180
+ level,
16181
+ 3,
16182
+ 1,
16183
+ bitDepth,
16184
+ 4,
16185
+ 1,
16186
+ chromaSubsampling
16187
+ ];
16188
+ };
15418
16189
  var generateAv1CodecConfigurationFromCodecString = (codecString) => {
15419
16190
  const parts = codecString.split(".");
15420
16191
  const marker = 1;
@@ -15604,6 +16375,7 @@ var workerCode = `(() => {
15604
16375
  }
15605
16376
  throw new TypeError(\`Unhandled codec '\${codec}'.\`);
15606
16377
  };
16378
+ var OPUS_SAMPLE_RATE = 48000;
15607
16379
  var PCM_CODEC_REGEX = /^pcm-([usf])(\\d+)(be)?$/;
15608
16380
  var parsePcmCodec = (codec) => {
15609
16381
  assert(PCM_AUDIO_CODECS.includes(codec));
@@ -16081,6 +16853,443 @@ var workerCode = `(() => {
16081
16853
  var MIN_BOX_HEADER_SIZE = 8;
16082
16854
  var MAX_BOX_HEADER_SIZE = 16;
16083
16855
 
16856
+ // ../../node_modules/.bun/mediabunny@1.61.3/node_modules/mediabunny/dist/modules/src/matroska/ebml.js
16857
+ /*!
16858
+ * Copyright (c) 2026-present, Vanilagy and contributors
16859
+ *
16860
+ * This Source Code Form is subject to the terms of the Mozilla Public
16861
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
16862
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
16863
+ */
16864
+
16865
+ class EBMLFloat32 {
16866
+ constructor(value) {
16867
+ this.value = value;
16868
+ }
16869
+ }
16870
+
16871
+ class EBMLFloat64 {
16872
+ constructor(value) {
16873
+ this.value = value;
16874
+ }
16875
+ }
16876
+
16877
+ class EBMLSignedInt {
16878
+ constructor(value) {
16879
+ this.value = value;
16880
+ }
16881
+ }
16882
+
16883
+ class EBMLUnicodeString {
16884
+ constructor(value) {
16885
+ this.value = value;
16886
+ }
16887
+ }
16888
+ var EBMLId;
16889
+ (function(EBMLId) {
16890
+ EBMLId[EBMLId["EBML"] = 440786851] = "EBML";
16891
+ EBMLId[EBMLId["EBMLVersion"] = 17030] = "EBMLVersion";
16892
+ EBMLId[EBMLId["EBMLReadVersion"] = 17143] = "EBMLReadVersion";
16893
+ EBMLId[EBMLId["EBMLMaxIDLength"] = 17138] = "EBMLMaxIDLength";
16894
+ EBMLId[EBMLId["EBMLMaxSizeLength"] = 17139] = "EBMLMaxSizeLength";
16895
+ EBMLId[EBMLId["DocType"] = 17026] = "DocType";
16896
+ EBMLId[EBMLId["DocTypeVersion"] = 17031] = "DocTypeVersion";
16897
+ EBMLId[EBMLId["DocTypeReadVersion"] = 17029] = "DocTypeReadVersion";
16898
+ EBMLId[EBMLId["Void"] = 236] = "Void";
16899
+ EBMLId[EBMLId["Segment"] = 408125543] = "Segment";
16900
+ EBMLId[EBMLId["SeekHead"] = 290298740] = "SeekHead";
16901
+ EBMLId[EBMLId["Seek"] = 19899] = "Seek";
16902
+ EBMLId[EBMLId["SeekID"] = 21419] = "SeekID";
16903
+ EBMLId[EBMLId["SeekPosition"] = 21420] = "SeekPosition";
16904
+ EBMLId[EBMLId["Duration"] = 17545] = "Duration";
16905
+ EBMLId[EBMLId["Info"] = 357149030] = "Info";
16906
+ EBMLId[EBMLId["TimestampScale"] = 2807729] = "TimestampScale";
16907
+ EBMLId[EBMLId["MuxingApp"] = 19840] = "MuxingApp";
16908
+ EBMLId[EBMLId["WritingApp"] = 22337] = "WritingApp";
16909
+ EBMLId[EBMLId["Tracks"] = 374648427] = "Tracks";
16910
+ EBMLId[EBMLId["TrackEntry"] = 174] = "TrackEntry";
16911
+ EBMLId[EBMLId["TrackNumber"] = 215] = "TrackNumber";
16912
+ EBMLId[EBMLId["TrackUID"] = 29637] = "TrackUID";
16913
+ EBMLId[EBMLId["TrackType"] = 131] = "TrackType";
16914
+ EBMLId[EBMLId["FlagEnabled"] = 185] = "FlagEnabled";
16915
+ EBMLId[EBMLId["FlagDefault"] = 136] = "FlagDefault";
16916
+ EBMLId[EBMLId["FlagForced"] = 21930] = "FlagForced";
16917
+ EBMLId[EBMLId["FlagOriginal"] = 21934] = "FlagOriginal";
16918
+ EBMLId[EBMLId["FlagHearingImpaired"] = 21931] = "FlagHearingImpaired";
16919
+ EBMLId[EBMLId["FlagVisualImpaired"] = 21932] = "FlagVisualImpaired";
16920
+ EBMLId[EBMLId["FlagCommentary"] = 21935] = "FlagCommentary";
16921
+ EBMLId[EBMLId["FlagLacing"] = 156] = "FlagLacing";
16922
+ EBMLId[EBMLId["Name"] = 21358] = "Name";
16923
+ EBMLId[EBMLId["Language"] = 2274716] = "Language";
16924
+ EBMLId[EBMLId["LanguageBCP47"] = 2274717] = "LanguageBCP47";
16925
+ EBMLId[EBMLId["CodecID"] = 134] = "CodecID";
16926
+ EBMLId[EBMLId["CodecPrivate"] = 25506] = "CodecPrivate";
16927
+ EBMLId[EBMLId["CodecDelay"] = 22186] = "CodecDelay";
16928
+ EBMLId[EBMLId["SeekPreRoll"] = 22203] = "SeekPreRoll";
16929
+ EBMLId[EBMLId["DefaultDuration"] = 2352003] = "DefaultDuration";
16930
+ EBMLId[EBMLId["Video"] = 224] = "Video";
16931
+ EBMLId[EBMLId["PixelWidth"] = 176] = "PixelWidth";
16932
+ EBMLId[EBMLId["PixelHeight"] = 186] = "PixelHeight";
16933
+ EBMLId[EBMLId["DisplayWidth"] = 21680] = "DisplayWidth";
16934
+ EBMLId[EBMLId["DisplayHeight"] = 21690] = "DisplayHeight";
16935
+ EBMLId[EBMLId["DisplayUnit"] = 21682] = "DisplayUnit";
16936
+ EBMLId[EBMLId["AlphaMode"] = 21440] = "AlphaMode";
16937
+ EBMLId[EBMLId["Audio"] = 225] = "Audio";
16938
+ EBMLId[EBMLId["SamplingFrequency"] = 181] = "SamplingFrequency";
16939
+ EBMLId[EBMLId["Channels"] = 159] = "Channels";
16940
+ EBMLId[EBMLId["BitDepth"] = 25188] = "BitDepth";
16941
+ EBMLId[EBMLId["SimpleBlock"] = 163] = "SimpleBlock";
16942
+ EBMLId[EBMLId["BlockGroup"] = 160] = "BlockGroup";
16943
+ EBMLId[EBMLId["Block"] = 161] = "Block";
16944
+ EBMLId[EBMLId["BlockAdditions"] = 30113] = "BlockAdditions";
16945
+ EBMLId[EBMLId["BlockMore"] = 166] = "BlockMore";
16946
+ EBMLId[EBMLId["BlockAdditional"] = 165] = "BlockAdditional";
16947
+ EBMLId[EBMLId["BlockAddID"] = 238] = "BlockAddID";
16948
+ EBMLId[EBMLId["BlockDuration"] = 155] = "BlockDuration";
16949
+ EBMLId[EBMLId["ReferenceBlock"] = 251] = "ReferenceBlock";
16950
+ EBMLId[EBMLId["Cluster"] = 524531317] = "Cluster";
16951
+ EBMLId[EBMLId["Timestamp"] = 231] = "Timestamp";
16952
+ EBMLId[EBMLId["Cues"] = 475249515] = "Cues";
16953
+ EBMLId[EBMLId["CuePoint"] = 187] = "CuePoint";
16954
+ EBMLId[EBMLId["CueTime"] = 179] = "CueTime";
16955
+ EBMLId[EBMLId["CueTrackPositions"] = 183] = "CueTrackPositions";
16956
+ EBMLId[EBMLId["CueTrack"] = 247] = "CueTrack";
16957
+ EBMLId[EBMLId["CueClusterPosition"] = 241] = "CueClusterPosition";
16958
+ EBMLId[EBMLId["Colour"] = 21936] = "Colour";
16959
+ EBMLId[EBMLId["MatrixCoefficients"] = 21937] = "MatrixCoefficients";
16960
+ EBMLId[EBMLId["TransferCharacteristics"] = 21946] = "TransferCharacteristics";
16961
+ EBMLId[EBMLId["Primaries"] = 21947] = "Primaries";
16962
+ EBMLId[EBMLId["Range"] = 21945] = "Range";
16963
+ EBMLId[EBMLId["Projection"] = 30320] = "Projection";
16964
+ EBMLId[EBMLId["ProjectionType"] = 30321] = "ProjectionType";
16965
+ EBMLId[EBMLId["ProjectionPoseYaw"] = 30323] = "ProjectionPoseYaw";
16966
+ EBMLId[EBMLId["ProjectionPosePitch"] = 30324] = "ProjectionPosePitch";
16967
+ EBMLId[EBMLId["ProjectionPoseRoll"] = 30325] = "ProjectionPoseRoll";
16968
+ EBMLId[EBMLId["Attachments"] = 423732329] = "Attachments";
16969
+ EBMLId[EBMLId["AttachedFile"] = 24999] = "AttachedFile";
16970
+ EBMLId[EBMLId["FileDescription"] = 18046] = "FileDescription";
16971
+ EBMLId[EBMLId["FileName"] = 18030] = "FileName";
16972
+ EBMLId[EBMLId["FileMediaType"] = 18016] = "FileMediaType";
16973
+ EBMLId[EBMLId["FileData"] = 18012] = "FileData";
16974
+ EBMLId[EBMLId["FileUID"] = 18094] = "FileUID";
16975
+ EBMLId[EBMLId["Chapters"] = 272869232] = "Chapters";
16976
+ EBMLId[EBMLId["Tags"] = 307544935] = "Tags";
16977
+ EBMLId[EBMLId["Tag"] = 29555] = "Tag";
16978
+ EBMLId[EBMLId["Targets"] = 25536] = "Targets";
16979
+ EBMLId[EBMLId["TargetTypeValue"] = 26826] = "TargetTypeValue";
16980
+ EBMLId[EBMLId["TargetType"] = 25546] = "TargetType";
16981
+ EBMLId[EBMLId["TagTrackUID"] = 25541] = "TagTrackUID";
16982
+ EBMLId[EBMLId["TagEditionUID"] = 25545] = "TagEditionUID";
16983
+ EBMLId[EBMLId["TagChapterUID"] = 25540] = "TagChapterUID";
16984
+ EBMLId[EBMLId["TagAttachmentUID"] = 25542] = "TagAttachmentUID";
16985
+ EBMLId[EBMLId["SimpleTag"] = 26568] = "SimpleTag";
16986
+ EBMLId[EBMLId["TagName"] = 17827] = "TagName";
16987
+ EBMLId[EBMLId["TagLanguage"] = 17530] = "TagLanguage";
16988
+ EBMLId[EBMLId["TagString"] = 17543] = "TagString";
16989
+ EBMLId[EBMLId["TagBinary"] = 17541] = "TagBinary";
16990
+ EBMLId[EBMLId["ContentEncodings"] = 28032] = "ContentEncodings";
16991
+ EBMLId[EBMLId["ContentEncoding"] = 25152] = "ContentEncoding";
16992
+ EBMLId[EBMLId["ContentEncodingOrder"] = 20529] = "ContentEncodingOrder";
16993
+ EBMLId[EBMLId["ContentEncodingScope"] = 20530] = "ContentEncodingScope";
16994
+ EBMLId[EBMLId["ContentCompression"] = 20532] = "ContentCompression";
16995
+ EBMLId[EBMLId["ContentCompAlgo"] = 16980] = "ContentCompAlgo";
16996
+ EBMLId[EBMLId["ContentCompSettings"] = 16981] = "ContentCompSettings";
16997
+ EBMLId[EBMLId["ContentEncryption"] = 20533] = "ContentEncryption";
16998
+ })(EBMLId || (EBMLId = {}));
16999
+ var LEVEL_0_EBML_IDS = [
17000
+ EBMLId.EBML,
17001
+ EBMLId.Segment
17002
+ ];
17003
+ var LEVEL_1_EBML_IDS = [
17004
+ EBMLId.SeekHead,
17005
+ EBMLId.Info,
17006
+ EBMLId.Cluster,
17007
+ EBMLId.Tracks,
17008
+ EBMLId.Cues,
17009
+ EBMLId.Attachments,
17010
+ EBMLId.Chapters,
17011
+ EBMLId.Tags
17012
+ ];
17013
+ var LEVEL_0_AND_1_EBML_IDS = [
17014
+ ...LEVEL_0_EBML_IDS,
17015
+ ...LEVEL_1_EBML_IDS
17016
+ ];
17017
+ var measureUnsignedInt = (value) => {
17018
+ if (value < 1 << 8) {
17019
+ return 1;
17020
+ } else if (value < 1 << 16) {
17021
+ return 2;
17022
+ } else if (value < 1 << 24) {
17023
+ return 3;
17024
+ } else if (value < 2 ** 32) {
17025
+ return 4;
17026
+ } else if (value < 2 ** 40) {
17027
+ return 5;
17028
+ } else {
17029
+ return 6;
17030
+ }
17031
+ };
17032
+ var measureUnsignedBigInt = (value) => {
17033
+ if (value < 1n << 8n) {
17034
+ return 1;
17035
+ } else if (value < 1n << 16n) {
17036
+ return 2;
17037
+ } else if (value < 1n << 24n) {
17038
+ return 3;
17039
+ } else if (value < 1n << 32n) {
17040
+ return 4;
17041
+ } else if (value < 1n << 40n) {
17042
+ return 5;
17043
+ } else if (value < 1n << 48n) {
17044
+ return 6;
17045
+ } else if (value < 1n << 56n) {
17046
+ return 7;
17047
+ } else {
17048
+ return 8;
17049
+ }
17050
+ };
17051
+ var measureSignedInt = (value) => {
17052
+ if (value >= -(1 << 6) && value < 1 << 6) {
17053
+ return 1;
17054
+ } else if (value >= -(1 << 13) && value < 1 << 13) {
17055
+ return 2;
17056
+ } else if (value >= -(1 << 20) && value < 1 << 20) {
17057
+ return 3;
17058
+ } else if (value >= -(1 << 27) && value < 1 << 27) {
17059
+ return 4;
17060
+ } else if (value >= -(2 ** 34) && value < 2 ** 34) {
17061
+ return 5;
17062
+ } else {
17063
+ return 6;
17064
+ }
17065
+ };
17066
+ var measureVarInt = (value) => {
17067
+ if (value < (1 << 7) - 1) {
17068
+ return 1;
17069
+ } else if (value < (1 << 14) - 1) {
17070
+ return 2;
17071
+ } else if (value < (1 << 21) - 1) {
17072
+ return 3;
17073
+ } else if (value < (1 << 28) - 1) {
17074
+ return 4;
17075
+ } else if (value < 2 ** 35 - 1) {
17076
+ return 5;
17077
+ } else if (value < 2 ** 42 - 1) {
17078
+ return 6;
17079
+ } else {
17080
+ throw new Error("EBML varint size not supported " + value);
17081
+ }
17082
+ };
17083
+
17084
+ class EBMLWriter {
17085
+ constructor(writer) {
17086
+ this.writer = writer;
17087
+ this.helper = new Uint8Array(8);
17088
+ this.helperView = new DataView(this.helper.buffer);
17089
+ this.offsets = new WeakMap;
17090
+ this.dataOffsets = new WeakMap;
17091
+ }
17092
+ writeByte(value) {
17093
+ this.helperView.setUint8(0, value);
17094
+ this.writer.write(this.helper.subarray(0, 1));
17095
+ }
17096
+ writeFloat32(value) {
17097
+ this.helperView.setFloat32(0, value, false);
17098
+ this.writer.write(this.helper.subarray(0, 4));
17099
+ }
17100
+ writeFloat64(value) {
17101
+ this.helperView.setFloat64(0, value, false);
17102
+ this.writer.write(this.helper);
17103
+ }
17104
+ writeUnsignedInt(value, width = measureUnsignedInt(value)) {
17105
+ let pos = 0;
17106
+ switch (width) {
17107
+ case 6:
17108
+ this.helperView.setUint8(pos++, value / 2 ** 40 | 0);
17109
+ case 5:
17110
+ this.helperView.setUint8(pos++, value / 2 ** 32 | 0);
17111
+ case 4:
17112
+ this.helperView.setUint8(pos++, value >> 24);
17113
+ case 3:
17114
+ this.helperView.setUint8(pos++, value >> 16);
17115
+ case 2:
17116
+ this.helperView.setUint8(pos++, value >> 8);
17117
+ case 1:
17118
+ this.helperView.setUint8(pos++, value);
17119
+ break;
17120
+ default:
17121
+ throw new Error("Bad unsigned int size " + width);
17122
+ }
17123
+ this.writer.write(this.helper.subarray(0, pos));
17124
+ }
17125
+ writeUnsignedBigInt(value, width = measureUnsignedBigInt(value)) {
17126
+ let pos = 0;
17127
+ for (let i = width - 1;i >= 0; i--) {
17128
+ this.helperView.setUint8(pos++, Number(value >> BigInt(i * 8) & 0xffn));
17129
+ }
17130
+ this.writer.write(this.helper.subarray(0, pos));
17131
+ }
17132
+ writeSignedInt(value, width = measureSignedInt(value)) {
17133
+ if (value < 0) {
17134
+ value += 2 ** (width * 8);
17135
+ }
17136
+ this.writeUnsignedInt(value, width);
17137
+ }
17138
+ writeVarInt(value, width = measureVarInt(value)) {
17139
+ let pos = 0;
17140
+ switch (width) {
17141
+ case 1:
17142
+ this.helperView.setUint8(pos++, 1 << 7 | value);
17143
+ break;
17144
+ case 2:
17145
+ this.helperView.setUint8(pos++, 1 << 6 | value >> 8);
17146
+ this.helperView.setUint8(pos++, value);
17147
+ break;
17148
+ case 3:
17149
+ this.helperView.setUint8(pos++, 1 << 5 | value >> 16);
17150
+ this.helperView.setUint8(pos++, value >> 8);
17151
+ this.helperView.setUint8(pos++, value);
17152
+ break;
17153
+ case 4:
17154
+ this.helperView.setUint8(pos++, 1 << 4 | value >> 24);
17155
+ this.helperView.setUint8(pos++, value >> 16);
17156
+ this.helperView.setUint8(pos++, value >> 8);
17157
+ this.helperView.setUint8(pos++, value);
17158
+ break;
17159
+ case 5:
17160
+ this.helperView.setUint8(pos++, 1 << 3 | value / 2 ** 32 & 7);
17161
+ this.helperView.setUint8(pos++, value >> 24);
17162
+ this.helperView.setUint8(pos++, value >> 16);
17163
+ this.helperView.setUint8(pos++, value >> 8);
17164
+ this.helperView.setUint8(pos++, value);
17165
+ break;
17166
+ case 6:
17167
+ this.helperView.setUint8(pos++, 1 << 2 | value / 2 ** 40 & 3);
17168
+ this.helperView.setUint8(pos++, value / 2 ** 32 | 0);
17169
+ this.helperView.setUint8(pos++, value >> 24);
17170
+ this.helperView.setUint8(pos++, value >> 16);
17171
+ this.helperView.setUint8(pos++, value >> 8);
17172
+ this.helperView.setUint8(pos++, value);
17173
+ break;
17174
+ default:
17175
+ throw new Error("Bad EBML varint size " + width);
17176
+ }
17177
+ this.writer.write(this.helper.subarray(0, pos));
17178
+ }
17179
+ writeAsciiString(str) {
17180
+ this.writer.write(new Uint8Array(str.split("").map((x) => x.charCodeAt(0))));
17181
+ }
17182
+ writeEBML(data) {
17183
+ if (data === null)
17184
+ return;
17185
+ if (data instanceof Uint8Array) {
17186
+ this.writer.write(data);
17187
+ } else if (Array.isArray(data)) {
17188
+ for (const elem of data) {
17189
+ this.writeEBML(elem);
17190
+ }
17191
+ } else {
17192
+ this.offsets.set(data, this.writer.getPos());
17193
+ this.writeUnsignedInt(data.id);
17194
+ if (Array.isArray(data.data)) {
17195
+ const sizePos = this.writer.getPos();
17196
+ const sizeSize = data.size === -1 ? 1 : data.size ?? 4;
17197
+ if (data.size === -1) {
17198
+ this.writeByte(255);
17199
+ } else {
17200
+ this.writer.seek(this.writer.getPos() + sizeSize);
17201
+ }
17202
+ const startPos = this.writer.getPos();
17203
+ this.dataOffsets.set(data, startPos);
17204
+ this.writeEBML(data.data);
17205
+ if (data.size !== -1) {
17206
+ const size = this.writer.getPos() - startPos;
17207
+ const endPos = this.writer.getPos();
17208
+ this.writer.seek(sizePos);
17209
+ this.writeVarInt(size, sizeSize);
17210
+ this.writer.seek(endPos);
17211
+ }
17212
+ } else if (typeof data.data === "number") {
17213
+ const size = data.size ?? measureUnsignedInt(data.data);
17214
+ this.writeVarInt(size);
17215
+ this.writeUnsignedInt(data.data, size);
17216
+ } else if (typeof data.data === "bigint") {
17217
+ const size = data.size ?? measureUnsignedBigInt(data.data);
17218
+ this.writeVarInt(size);
17219
+ this.writeUnsignedBigInt(data.data, size);
17220
+ } else if (typeof data.data === "string") {
17221
+ this.writeVarInt(data.data.length);
17222
+ this.writeAsciiString(data.data);
17223
+ } else if (data.data instanceof Uint8Array) {
17224
+ this.writeVarInt(data.data.byteLength, data.size);
17225
+ this.writer.write(data.data);
17226
+ } else if (data.data instanceof EBMLFloat32) {
17227
+ this.writeVarInt(4);
17228
+ this.writeFloat32(data.data.value);
17229
+ } else if (data.data instanceof EBMLFloat64) {
17230
+ this.writeVarInt(8);
17231
+ this.writeFloat64(data.data.value);
17232
+ } else if (data.data instanceof EBMLSignedInt) {
17233
+ const size = data.size ?? measureSignedInt(data.data.value);
17234
+ this.writeVarInt(size);
17235
+ this.writeSignedInt(data.data.value, size);
17236
+ } else if (data.data instanceof EBMLUnicodeString) {
17237
+ const bytes = textEncoder.encode(data.data.value);
17238
+ this.writeVarInt(bytes.length);
17239
+ this.writer.write(bytes);
17240
+ } else {
17241
+ assertNever(data.data);
17242
+ }
17243
+ }
17244
+ }
17245
+ }
17246
+ var MAX_VAR_INT_SIZE = 8;
17247
+ var MAX_HEADER_SIZE = 2 * MAX_VAR_INT_SIZE;
17248
+ var CODEC_STRING_MAP = {
17249
+ avc: "V_MPEG4/ISO/AVC",
17250
+ hevc: "V_MPEGH/ISO/HEVC",
17251
+ vp8: "V_VP8",
17252
+ vp9: "V_VP9",
17253
+ av1: "V_AV1",
17254
+ prores: "V_PRORES",
17255
+ aac: "A_AAC",
17256
+ mp3: "A_MPEG/L3",
17257
+ opus: "A_OPUS",
17258
+ vorbis: "A_VORBIS",
17259
+ flac: "A_FLAC",
17260
+ ac3: "A_AC3",
17261
+ eac3: "A_EAC3",
17262
+ dts: "A_DTS",
17263
+ "pcm-u8": "A_PCM/INT/LIT",
17264
+ "pcm-s16": "A_PCM/INT/LIT",
17265
+ "pcm-s16be": "A_PCM/INT/BIG",
17266
+ "pcm-s24": "A_PCM/INT/LIT",
17267
+ "pcm-s24be": "A_PCM/INT/BIG",
17268
+ "pcm-s32": "A_PCM/INT/LIT",
17269
+ "pcm-s32be": "A_PCM/INT/BIG",
17270
+ "pcm-f32": "A_PCM/FLOAT/IEEE",
17271
+ "pcm-f64": "A_PCM/FLOAT/IEEE",
17272
+ webvtt: "S_TEXT/WEBVTT"
17273
+ };
17274
+
17275
+ // ../../node_modules/.bun/mediabunny@1.61.3/node_modules/mediabunny/dist/modules/src/matroska/matroska-misc.js
17276
+ /*!
17277
+ * Copyright (c) 2026-present, Vanilagy and contributors
17278
+ *
17279
+ * This Source Code Form is subject to the terms of the Mozilla Public
17280
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
17281
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
17282
+ */
17283
+ var buildMatroskaMimeType = (info) => {
17284
+ const base = info.hasVideo ? "video/" : info.hasAudio ? "audio/" : "application/";
17285
+ let string = base + (info.isWebM ? "webm" : "x-matroska");
17286
+ if (info.codecStrings.length > 0) {
17287
+ const uniqueCodecMimeTypes = [...new Set(info.codecStrings.filter(Boolean))];
17288
+ string += \`; codecs="\${uniqueCodecMimeTypes.join(", ")}"\`;
17289
+ }
17290
+ return string;
17291
+ };
17292
+
16084
17293
  // ../../node_modules/.bun/mediabunny@1.61.3/node_modules/mediabunny/dist/modules/src/adts/adts-reader.js
16085
17294
  /*!
16086
17295
  * Copyright (c) 2026-present, Vanilagy and contributors
@@ -18917,6 +20126,13 @@ var workerCode = `(() => {
18917
20126
  * file, You can obtain one at https://mozilla.org/MPL/2.0/.
18918
20127
  */
18919
20128
  var inlineTimestampRegex = /<(?:(\\d{2}):)?(\\d{2}):(\\d{2}).(\\d{3})>/g;
20129
+ var timestampRegex = /(?:(\\d{2}):)?(\\d{2}):(\\d{2}).(\\d{3})/;
20130
+ var parseSubtitleTimestamp = (string) => {
20131
+ const match = timestampRegex.exec(string);
20132
+ if (!match)
20133
+ throw new Error("Expected match.");
20134
+ return 60 * 60 * 1000 * Number(match[1] || "0") + 60 * 1000 * Number(match[2]) + 1000 * Number(match[3]) + Number(match[4]);
20135
+ };
18920
20136
  var formatSubtitleTimestamp = (timestamp) => {
18921
20137
  const hours = Math.floor(timestamp / (60 * 60 * 1000));
18922
20138
  const minutes = Math.floor(timestamp % (60 * 60 * 1000) / (60 * 1000));
@@ -21967,6 +23183,994 @@ var workerCode = `(() => {
21967
23183
  }
21968
23184
  }
21969
23185
 
23186
+ // ../../node_modules/.bun/mediabunny@1.61.3/node_modules/mediabunny/dist/modules/src/matroska/matroska-muxer.js
23187
+ /*!
23188
+ * Copyright (c) 2026-present, Vanilagy and contributors
23189
+ *
23190
+ * This Source Code Form is subject to the terms of the Mozilla Public
23191
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
23192
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
23193
+ */
23194
+ var MIN_CLUSTER_TIMESTAMP_MS = -(2 ** 15);
23195
+ var MAX_CLUSTER_TIMESTAMP_MS = 2 ** 15 - 1;
23196
+ var SEGMENT_SIZE_BYTES = 6;
23197
+ var CLUSTER_SIZE_BYTES = 5;
23198
+ var TRACK_TYPE_MAP = {
23199
+ video: 1,
23200
+ audio: 2,
23201
+ subtitle: 17
23202
+ };
23203
+
23204
+ class MatroskaMuxer extends Muxer {
23205
+ constructor(output, format) {
23206
+ super(output);
23207
+ this.trackDatas = [];
23208
+ this.allTracksKnown = promiseWithResolvers();
23209
+ this.segment = null;
23210
+ this.segmentInfo = null;
23211
+ this.seekHead = null;
23212
+ this.tracksElement = null;
23213
+ this.tagsElement = null;
23214
+ this.attachmentsElement = null;
23215
+ this.segmentDuration = null;
23216
+ this.cues = null;
23217
+ this.currentCluster = null;
23218
+ this.currentClusterStartMsTimestamp = null;
23219
+ this.currentClusterMaxMsTimestamp = null;
23220
+ this.trackDatasInCurrentCluster = new Map;
23221
+ this.startTimestamp = Infinity;
23222
+ this.endTimestamp = -Infinity;
23223
+ this.warnedAboutTooNegativeTimestamp = false;
23224
+ this.format = format;
23225
+ }
23226
+ async start() {
23227
+ const release = await this.mutex.acquire();
23228
+ this.writer = await this.output._getRootWriter(!!this.format._options.appendOnly);
23229
+ this.ebmlWriter = new EBMLWriter(this.writer);
23230
+ this.writeEBMLHeader();
23231
+ this.createSegmentInfo();
23232
+ this.createCues();
23233
+ await this.writer.flush();
23234
+ for (const track of this.output.tracks) {
23235
+ if (track.isVideoTrack() && track.metadata.decoderConfig) {
23236
+ this.getVideoTrackData(track, track.metadata.primingPacket ?? null, { decoderConfig: track.metadata.decoderConfig });
23237
+ } else if (track.isAudioTrack() && track.metadata.decoderConfig) {
23238
+ this.getAudioTrackData(track, track.metadata.primingPacket ?? null, { decoderConfig: track.metadata.decoderConfig });
23239
+ }
23240
+ }
23241
+ release();
23242
+ }
23243
+ writeEBMLHeader() {
23244
+ if (this.format._options.onEbmlHeader) {
23245
+ this.writer.startTrackingWrites();
23246
+ }
23247
+ const ebmlHeader = { id: EBMLId.EBML, data: [
23248
+ { id: EBMLId.EBMLVersion, data: 1 },
23249
+ { id: EBMLId.EBMLReadVersion, data: 1 },
23250
+ { id: EBMLId.EBMLMaxIDLength, data: 4 },
23251
+ { id: EBMLId.EBMLMaxSizeLength, data: 8 },
23252
+ { id: EBMLId.DocType, data: this.format instanceof WebMOutputFormat ? "webm" : "matroska" },
23253
+ { id: EBMLId.DocTypeVersion, data: 4 },
23254
+ { id: EBMLId.DocTypeReadVersion, data: 2 }
23255
+ ] };
23256
+ this.ebmlWriter.writeEBML(ebmlHeader);
23257
+ if (this.format._options.onEbmlHeader) {
23258
+ const { data, start } = this.writer.stopTrackingWrites();
23259
+ this.format._options.onEbmlHeader(data, start);
23260
+ }
23261
+ }
23262
+ maybeCreateSeekHead(writeOffsets) {
23263
+ if (this.format._options.appendOnly) {
23264
+ return;
23265
+ }
23266
+ const kaxCues = new Uint8Array([28, 83, 187, 107]);
23267
+ const kaxInfo = new Uint8Array([21, 73, 169, 102]);
23268
+ const kaxTracks = new Uint8Array([22, 84, 174, 107]);
23269
+ const kaxAttachments = new Uint8Array([25, 65, 164, 105]);
23270
+ const kaxTags = new Uint8Array([18, 84, 195, 103]);
23271
+ const seekHead = { id: EBMLId.SeekHead, data: [
23272
+ { id: EBMLId.Seek, data: [
23273
+ { id: EBMLId.SeekID, data: kaxInfo },
23274
+ {
23275
+ id: EBMLId.SeekPosition,
23276
+ size: 5,
23277
+ data: writeOffsets ? this.ebmlWriter.offsets.get(this.segmentInfo) - this.segmentDataOffset : 0
23278
+ }
23279
+ ] },
23280
+ { id: EBMLId.Seek, data: [
23281
+ { id: EBMLId.SeekID, data: kaxTracks },
23282
+ {
23283
+ id: EBMLId.SeekPosition,
23284
+ size: 5,
23285
+ data: writeOffsets ? this.ebmlWriter.offsets.get(this.tracksElement) - this.segmentDataOffset : 0
23286
+ }
23287
+ ] },
23288
+ this.attachmentsElement ? { id: EBMLId.Seek, data: [
23289
+ { id: EBMLId.SeekID, data: kaxAttachments },
23290
+ {
23291
+ id: EBMLId.SeekPosition,
23292
+ size: 5,
23293
+ data: writeOffsets ? this.ebmlWriter.offsets.get(this.attachmentsElement) - this.segmentDataOffset : 0
23294
+ }
23295
+ ] } : null,
23296
+ this.tagsElement ? { id: EBMLId.Seek, data: [
23297
+ { id: EBMLId.SeekID, data: kaxTags },
23298
+ {
23299
+ id: EBMLId.SeekPosition,
23300
+ size: 5,
23301
+ data: writeOffsets ? this.ebmlWriter.offsets.get(this.tagsElement) - this.segmentDataOffset : 0
23302
+ }
23303
+ ] } : null,
23304
+ { id: EBMLId.Seek, data: [
23305
+ { id: EBMLId.SeekID, data: kaxCues },
23306
+ {
23307
+ id: EBMLId.SeekPosition,
23308
+ size: 5,
23309
+ data: writeOffsets ? this.ebmlWriter.offsets.get(this.cues) - this.segmentDataOffset : 0
23310
+ }
23311
+ ] }
23312
+ ] };
23313
+ this.seekHead = seekHead;
23314
+ }
23315
+ createSegmentInfo() {
23316
+ const segmentDuration = { id: EBMLId.Duration, data: new EBMLFloat64(0) };
23317
+ this.segmentDuration = segmentDuration;
23318
+ const segmentInfo = { id: EBMLId.Info, data: [
23319
+ { id: EBMLId.TimestampScale, data: 1e6 },
23320
+ { id: EBMLId.MuxingApp, data: VERSIONED_NAME },
23321
+ { id: EBMLId.WritingApp, data: VERSIONED_NAME },
23322
+ !this.format._options.appendOnly ? segmentDuration : null
23323
+ ] };
23324
+ this.segmentInfo = segmentInfo;
23325
+ }
23326
+ createTracks() {
23327
+ const tracksElement = { id: EBMLId.Tracks, data: [] };
23328
+ this.tracksElement = tracksElement;
23329
+ for (const trackData of this.trackDatas) {
23330
+ let codecId = CODEC_STRING_MAP[trackData.track.source._codec];
23331
+ assert(codecId);
23332
+ if (trackData.type === "audio" && trackData.track.source._codec === "dts") {
23333
+ if (trackData.info.decoderConfig.codec === "dtse") {
23334
+ codecId = "A_DTS/EXPRESS";
23335
+ } else if (trackData.info.decoderConfig.codec === "dtsl") {
23336
+ codecId = "A_DTS/LOSSLESS";
23337
+ }
23338
+ }
23339
+ let seekPreRollNs = 0;
23340
+ if (trackData.type === "audio" && trackData.track.source._codec === "opus") {
23341
+ seekPreRollNs = 1e6 * 80;
23342
+ const description = trackData.info.decoderConfig.description;
23343
+ if (description) {
23344
+ const bytes = toUint8Array(description);
23345
+ const header = parseOpusIdentificationHeader(bytes);
23346
+ seekPreRollNs = Math.round(1e9 * (header.preSkip / OPUS_SAMPLE_RATE));
23347
+ }
23348
+ }
23349
+ tracksElement.data.push({ id: EBMLId.TrackEntry, data: [
23350
+ { id: EBMLId.TrackNumber, data: trackData.track.id },
23351
+ { id: EBMLId.TrackUID, data: trackData.track.id },
23352
+ { id: EBMLId.TrackType, data: TRACK_TYPE_MAP[trackData.type] },
23353
+ trackData.track.metadata.disposition?.default === false ? { id: EBMLId.FlagDefault, data: 0 } : null,
23354
+ trackData.track.metadata.disposition?.forced ? { id: EBMLId.FlagForced, data: 1 } : null,
23355
+ trackData.track.metadata.disposition?.hearingImpaired ? { id: EBMLId.FlagHearingImpaired, data: 1 } : null,
23356
+ trackData.track.metadata.disposition?.visuallyImpaired ? { id: EBMLId.FlagVisualImpaired, data: 1 } : null,
23357
+ trackData.track.metadata.disposition?.original ? { id: EBMLId.FlagOriginal, data: 1 } : null,
23358
+ trackData.track.metadata.disposition?.commentary ? { id: EBMLId.FlagCommentary, data: 1 } : null,
23359
+ { id: EBMLId.FlagLacing, data: 0 },
23360
+ { id: EBMLId.Language, data: trackData.track.metadata.languageCode ?? UNDETERMINED_LANGUAGE },
23361
+ { id: EBMLId.CodecID, data: codecId },
23362
+ trackData.codecPrivate ? { id: EBMLId.CodecPrivate, data: toUint8Array(trackData.codecPrivate) } : null,
23363
+ seekPreRollNs > 0 ? { id: EBMLId.SeekPreRoll, data: seekPreRollNs } : null,
23364
+ trackData.track.metadata.name !== undefined ? { id: EBMLId.Name, data: new EBMLUnicodeString(trackData.track.metadata.name) } : null,
23365
+ trackData.type === "video" ? this.videoSpecificTrackInfo(trackData) : null,
23366
+ trackData.type === "audio" ? this.audioSpecificTrackInfo(trackData) : null,
23367
+ trackData.type === "subtitle" ? this.subtitleSpecificTrackInfo(trackData) : null
23368
+ ] });
23369
+ }
23370
+ }
23371
+ videoSpecificTrackInfo(trackData) {
23372
+ const { frameRate, transformationMatrix } = trackData.track.metadata;
23373
+ const elements = [
23374
+ frameRate ? {
23375
+ id: EBMLId.DefaultDuration,
23376
+ data: 1e9 / frameRate
23377
+ } : null
23378
+ ];
23379
+ let rotation;
23380
+ let horizontalScale;
23381
+ let verticalScale;
23382
+ if (transformationMatrix) {
23383
+ rotation = extractRotationFromMatrix(transformationMatrix);
23384
+ const unrotated = multiplyMatrices(rotationMatrix(-rotation), transformationMatrix);
23385
+ horizontalScale = Math.sign(unrotated[0]);
23386
+ verticalScale = Math.sign(unrotated[4]);
23387
+ if (verticalScale === -1) {
23388
+ verticalScale = 1;
23389
+ horizontalScale = -horizontalScale;
23390
+ rotation = normalizeRotation(rotation + 180);
23391
+ }
23392
+ } else {
23393
+ rotation = trackData.track.metadata.rotation ?? 0;
23394
+ horizontalScale = trackData.track.metadata.flip ? -1 : 1;
23395
+ verticalScale = 1;
23396
+ }
23397
+ const roll = rotation ? normalizeRotation(horizontalScale === -1 ? rotation : -rotation) : 0;
23398
+ const yaw = Math.round(Math.acos(horizontalScale) * RAD_TO_DEG);
23399
+ const pitch = Math.round(Math.acos(verticalScale) * RAD_TO_DEG);
23400
+ const hasNonSquarePixelAspectRatio = !!trackData.info.aspectRatio && trackData.info.aspectRatio.num * trackData.info.height !== trackData.info.aspectRatio.den * trackData.info.width;
23401
+ const colorSpace = trackData.info.decoderConfig.colorSpace;
23402
+ const videoElement = { id: EBMLId.Video, data: [
23403
+ { id: EBMLId.PixelWidth, data: trackData.info.width },
23404
+ { id: EBMLId.PixelHeight, data: trackData.info.height },
23405
+ hasNonSquarePixelAspectRatio ? { id: EBMLId.DisplayWidth, data: trackData.info.aspectRatio.num } : null,
23406
+ hasNonSquarePixelAspectRatio ? { id: EBMLId.DisplayHeight, data: trackData.info.aspectRatio.den } : null,
23407
+ hasNonSquarePixelAspectRatio ? { id: EBMLId.DisplayUnit, data: 3 } : null,
23408
+ trackData.info.alphaMode ? { id: EBMLId.AlphaMode, data: 1 } : null,
23409
+ colorSpaceIsEmpty(colorSpace) ? null : {
23410
+ id: EBMLId.Colour,
23411
+ data: [
23412
+ {
23413
+ id: EBMLId.MatrixCoefficients,
23414
+ data: colorSpace?.matrix != null ? MATRIX_COEFFICIENTS_MAP[colorSpace.matrix] : 2
23415
+ },
23416
+ {
23417
+ id: EBMLId.TransferCharacteristics,
23418
+ data: colorSpace?.transfer != null ? TRANSFER_CHARACTERISTICS_MAP[colorSpace.transfer] : 2
23419
+ },
23420
+ {
23421
+ id: EBMLId.Primaries,
23422
+ data: colorSpace?.primaries != null ? COLOR_PRIMARIES_MAP[colorSpace.primaries] : 2
23423
+ },
23424
+ {
23425
+ id: EBMLId.Range,
23426
+ data: colorSpace?.fullRange != null ? colorSpace.fullRange ? 2 : 1 : 0
23427
+ }
23428
+ ]
23429
+ },
23430
+ roll || yaw || pitch ? {
23431
+ id: EBMLId.Projection,
23432
+ data: [
23433
+ {
23434
+ id: EBMLId.ProjectionType,
23435
+ data: 0
23436
+ },
23437
+ yaw ? {
23438
+ id: EBMLId.ProjectionPoseYaw,
23439
+ data: new EBMLFloat32(yaw)
23440
+ } : null,
23441
+ pitch ? {
23442
+ id: EBMLId.ProjectionPosePitch,
23443
+ data: new EBMLFloat32(pitch)
23444
+ } : null,
23445
+ roll ? {
23446
+ id: EBMLId.ProjectionPoseRoll,
23447
+ data: new EBMLFloat32((roll + 180) % 360 - 180)
23448
+ } : null
23449
+ ]
23450
+ } : null
23451
+ ] };
23452
+ elements.push(videoElement);
23453
+ return elements;
23454
+ }
23455
+ audioSpecificTrackInfo(trackData) {
23456
+ const pcmInfo = PCM_AUDIO_CODECS.includes(trackData.track.source._codec) ? parsePcmCodec(trackData.track.source._codec) : null;
23457
+ return [
23458
+ { id: EBMLId.Audio, data: [
23459
+ { id: EBMLId.SamplingFrequency, data: new EBMLFloat32(trackData.info.sampleRate) },
23460
+ { id: EBMLId.Channels, data: trackData.info.numberOfChannels },
23461
+ pcmInfo ? { id: EBMLId.BitDepth, data: 8 * pcmInfo.sampleSize } : null
23462
+ ] }
23463
+ ];
23464
+ }
23465
+ subtitleSpecificTrackInfo(trackData) {
23466
+ return [];
23467
+ }
23468
+ maybeCreateTags() {
23469
+ const simpleTags = [];
23470
+ const addSimpleTag = (key, value) => {
23471
+ simpleTags.push({ id: EBMLId.SimpleTag, data: [
23472
+ { id: EBMLId.TagName, data: new EBMLUnicodeString(key) },
23473
+ typeof value === "string" ? { id: EBMLId.TagString, data: new EBMLUnicodeString(value) } : { id: EBMLId.TagBinary, data: value }
23474
+ ] });
23475
+ };
23476
+ const metadataTags = this.output._metadataTags;
23477
+ const writtenTags = new Set;
23478
+ for (const { key, value } of keyValueIterator(metadataTags)) {
23479
+ switch (key) {
23480
+ case "title":
23481
+ {
23482
+ addSimpleTag("TITLE", value);
23483
+ writtenTags.add("TITLE");
23484
+ }
23485
+ ;
23486
+ break;
23487
+ case "description":
23488
+ {
23489
+ addSimpleTag("DESCRIPTION", value);
23490
+ writtenTags.add("DESCRIPTION");
23491
+ }
23492
+ ;
23493
+ break;
23494
+ case "artist":
23495
+ {
23496
+ addSimpleTag("ARTIST", value);
23497
+ writtenTags.add("ARTIST");
23498
+ }
23499
+ ;
23500
+ break;
23501
+ case "album":
23502
+ {
23503
+ addSimpleTag("ALBUM", value);
23504
+ writtenTags.add("ALBUM");
23505
+ }
23506
+ ;
23507
+ break;
23508
+ case "albumArtist":
23509
+ {
23510
+ addSimpleTag("ALBUM_ARTIST", value);
23511
+ writtenTags.add("ALBUM_ARTIST");
23512
+ }
23513
+ ;
23514
+ break;
23515
+ case "genre":
23516
+ {
23517
+ addSimpleTag("GENRE", value);
23518
+ writtenTags.add("GENRE");
23519
+ }
23520
+ ;
23521
+ break;
23522
+ case "beatsPerMinute":
23523
+ {
23524
+ addSimpleTag("BPM", value.toString());
23525
+ writtenTags.add("BPM");
23526
+ }
23527
+ ;
23528
+ break;
23529
+ case "comment":
23530
+ {
23531
+ addSimpleTag("COMMENT", value);
23532
+ writtenTags.add("COMMENT");
23533
+ }
23534
+ ;
23535
+ break;
23536
+ case "lyrics":
23537
+ {
23538
+ addSimpleTag("LYRICS", value);
23539
+ writtenTags.add("LYRICS");
23540
+ }
23541
+ ;
23542
+ break;
23543
+ case "date":
23544
+ {
23545
+ addSimpleTag("DATE", value.toISOString().slice(0, 10));
23546
+ writtenTags.add("DATE");
23547
+ }
23548
+ ;
23549
+ break;
23550
+ case "trackNumber":
23551
+ {
23552
+ const string = metadataTags.tracksTotal !== undefined ? \`\${value}/\${metadataTags.tracksTotal}\` : value.toString();
23553
+ addSimpleTag("PART_NUMBER", string);
23554
+ writtenTags.add("PART_NUMBER");
23555
+ }
23556
+ ;
23557
+ break;
23558
+ case "discNumber":
23559
+ {
23560
+ const string = metadataTags.discsTotal !== undefined ? \`\${value}/\${metadataTags.discsTotal}\` : value.toString();
23561
+ addSimpleTag("DISC", string);
23562
+ writtenTags.add("DISC");
23563
+ }
23564
+ ;
23565
+ break;
23566
+ case "tracksTotal":
23567
+ case "discsTotal":
23568
+ {}
23569
+ ;
23570
+ break;
23571
+ case "images":
23572
+ case "raw":
23573
+ {}
23574
+ ;
23575
+ break;
23576
+ default:
23577
+ assertNever(key);
23578
+ }
23579
+ }
23580
+ if (metadataTags.raw) {
23581
+ for (const key in metadataTags.raw) {
23582
+ const value = metadataTags.raw[key];
23583
+ if (value == null || writtenTags.has(key)) {
23584
+ continue;
23585
+ }
23586
+ if (typeof value === "string" || value instanceof Uint8Array) {
23587
+ addSimpleTag(key, value);
23588
+ }
23589
+ }
23590
+ }
23591
+ if (simpleTags.length === 0) {
23592
+ return;
23593
+ }
23594
+ this.tagsElement = {
23595
+ id: EBMLId.Tags,
23596
+ data: [{ id: EBMLId.Tag, data: [
23597
+ { id: EBMLId.Targets, data: [
23598
+ { id: EBMLId.TargetTypeValue, data: 50 },
23599
+ { id: EBMLId.TargetType, data: "MOVIE" }
23600
+ ] },
23601
+ ...simpleTags
23602
+ ] }]
23603
+ };
23604
+ }
23605
+ maybeCreateAttachments() {
23606
+ const metadataTags = this.output._metadataTags;
23607
+ const elements = [];
23608
+ const existingFileUids = new Set;
23609
+ const images = metadataTags.images ?? [];
23610
+ for (const image of images) {
23611
+ let imageName = image.name;
23612
+ if (imageName === undefined) {
23613
+ const baseName = image.kind === "coverFront" ? "cover" : image.kind === "coverBack" ? "back" : "image";
23614
+ imageName = baseName + (imageMimeTypeToExtension(image.mimeType) ?? "");
23615
+ }
23616
+ let fileUid;
23617
+ while (true) {
23618
+ fileUid = 0n;
23619
+ for (let i = 0;i < 8; i++) {
23620
+ fileUid <<= 8n;
23621
+ fileUid |= BigInt(Math.floor(Math.random() * 256));
23622
+ }
23623
+ if (fileUid !== 0n && !existingFileUids.has(fileUid)) {
23624
+ break;
23625
+ }
23626
+ }
23627
+ existingFileUids.add(fileUid);
23628
+ elements.push({
23629
+ id: EBMLId.AttachedFile,
23630
+ data: [
23631
+ image.description !== undefined ? { id: EBMLId.FileDescription, data: new EBMLUnicodeString(image.description) } : null,
23632
+ { id: EBMLId.FileName, data: new EBMLUnicodeString(imageName) },
23633
+ { id: EBMLId.FileMediaType, data: image.mimeType },
23634
+ { id: EBMLId.FileData, data: image.data },
23635
+ { id: EBMLId.FileUID, data: fileUid }
23636
+ ]
23637
+ });
23638
+ }
23639
+ for (const [key, value] of Object.entries(metadataTags.raw ?? {})) {
23640
+ if (!(value instanceof AttachedFile)) {
23641
+ continue;
23642
+ }
23643
+ const keyIsNumeric = /^\\d+$/.test(key);
23644
+ if (!keyIsNumeric) {
23645
+ continue;
23646
+ }
23647
+ if (images.find((x) => x.mimeType === value.mimeType && uint8ArraysAreEqual(x.data, value.data))) {
23648
+ continue;
23649
+ }
23650
+ elements.push({
23651
+ id: EBMLId.AttachedFile,
23652
+ data: [
23653
+ value.description !== undefined ? { id: EBMLId.FileDescription, data: new EBMLUnicodeString(value.description) } : null,
23654
+ { id: EBMLId.FileName, data: new EBMLUnicodeString(value.name ?? "") },
23655
+ { id: EBMLId.FileMediaType, data: value.mimeType ?? "" },
23656
+ { id: EBMLId.FileData, data: value.data },
23657
+ { id: EBMLId.FileUID, data: BigInt(key) }
23658
+ ]
23659
+ });
23660
+ }
23661
+ if (elements.length === 0) {
23662
+ return;
23663
+ }
23664
+ this.attachmentsElement = { id: EBMLId.Attachments, data: elements };
23665
+ }
23666
+ createSegment() {
23667
+ this.createTracks();
23668
+ this.maybeCreateTags();
23669
+ this.maybeCreateAttachments();
23670
+ this.maybeCreateSeekHead(false);
23671
+ const segment = {
23672
+ id: EBMLId.Segment,
23673
+ size: this.format._options.appendOnly ? -1 : SEGMENT_SIZE_BYTES,
23674
+ data: [
23675
+ this.seekHead,
23676
+ this.segmentInfo,
23677
+ this.tracksElement,
23678
+ this.attachmentsElement,
23679
+ this.tagsElement
23680
+ ]
23681
+ };
23682
+ this.segment = segment;
23683
+ if (this.format._options.onSegmentHeader) {
23684
+ this.writer.startTrackingWrites();
23685
+ }
23686
+ this.ebmlWriter.writeEBML(segment);
23687
+ if (this.format._options.onSegmentHeader) {
23688
+ const { data, start } = this.writer.stopTrackingWrites();
23689
+ this.format._options.onSegmentHeader(data, start);
23690
+ }
23691
+ }
23692
+ createCues() {
23693
+ this.cues = { id: EBMLId.Cues, data: [] };
23694
+ }
23695
+ get segmentDataOffset() {
23696
+ assert(this.segment);
23697
+ return this.ebmlWriter.dataOffsets.get(this.segment);
23698
+ }
23699
+ allTracksAreKnown() {
23700
+ for (const track of this.output.tracks) {
23701
+ if (!track.source._closed && !this.trackDatas.some((x) => x.track === track)) {
23702
+ return false;
23703
+ }
23704
+ }
23705
+ return true;
23706
+ }
23707
+ async getMimeType() {
23708
+ await this.allTracksKnown.promise;
23709
+ const codecStrings = this.trackDatas.map((trackData) => {
23710
+ if (trackData.type === "video") {
23711
+ return trackData.info.decoderConfig.codec;
23712
+ } else if (trackData.type === "audio") {
23713
+ return trackData.info.decoderConfig.codec;
23714
+ } else {
23715
+ const map = {
23716
+ webvtt: "wvtt"
23717
+ };
23718
+ return map[trackData.track.source._codec];
23719
+ }
23720
+ });
23721
+ return buildMatroskaMimeType({
23722
+ isWebM: this.format instanceof WebMOutputFormat,
23723
+ hasVideo: this.trackDatas.some((x) => x.type === "video"),
23724
+ hasAudio: this.trackDatas.some((x) => x.type === "audio"),
23725
+ codecStrings
23726
+ });
23727
+ }
23728
+ getVideoTrackData(track, packet, meta) {
23729
+ const existingTrackData = this.trackDatas.find((x) => x.track === track);
23730
+ if (existingTrackData) {
23731
+ return existingTrackData;
23732
+ }
23733
+ validateVideoChunkMetadata(meta, track.source._codec);
23734
+ assert(meta);
23735
+ assert(meta.decoderConfig);
23736
+ assert(meta.decoderConfig.codedWidth !== undefined);
23737
+ assert(meta.decoderConfig.codedHeight !== undefined);
23738
+ const displayAspectWidth = meta.decoderConfig.displayAspectWidth;
23739
+ const displayAspectHeight = meta.decoderConfig.displayAspectHeight;
23740
+ const aspectRatio = displayAspectWidth === undefined || displayAspectHeight === undefined ? null : simplifyRational({
23741
+ num: displayAspectWidth,
23742
+ den: displayAspectHeight
23743
+ });
23744
+ const newTrackData = {
23745
+ track,
23746
+ type: "video",
23747
+ info: {
23748
+ width: meta.decoderConfig.codedWidth,
23749
+ height: meta.decoderConfig.codedHeight,
23750
+ aspectRatio,
23751
+ decoderConfig: meta.decoderConfig,
23752
+ alphaMode: track.metadata.canBeTransparent || (packet ? !!packet.sideData.alpha : null)
23753
+ },
23754
+ chunkQueue: [],
23755
+ lastWrittenMsTimestamp: null,
23756
+ codecPrivate: meta.decoderConfig.description ?? null,
23757
+ closed: false
23758
+ };
23759
+ if (track.source._codec === "vp9") {
23760
+ newTrackData.codecPrivate = new Uint8Array(generateVp9CodecConfigurationFromCodecString(newTrackData.info.decoderConfig.codec));
23761
+ } else if (track.source._codec === "av1") {
23762
+ newTrackData.codecPrivate = new Uint8Array(generateAv1CodecConfigurationFromCodecString(newTrackData.info.decoderConfig.codec));
23763
+ } else if (track.source._codec === "prores") {
23764
+ newTrackData.codecPrivate = textEncoder.encode(meta.decoderConfig.codec);
23765
+ }
23766
+ this.trackDatas.push(newTrackData);
23767
+ this.trackDatas.sort((a, b) => a.track.id - b.track.id);
23768
+ if (this.allTracksAreKnown()) {
23769
+ this.allTracksKnown.resolve();
23770
+ }
23771
+ return newTrackData;
23772
+ }
23773
+ getAudioTrackData(track, packet, meta) {
23774
+ const existingTrackData = this.trackDatas.find((x) => x.track === track);
23775
+ if (existingTrackData) {
23776
+ return existingTrackData;
23777
+ }
23778
+ validateAudioChunkMetadata(meta, track.source._codec);
23779
+ assert(meta);
23780
+ assert(meta.decoderConfig);
23781
+ const decoderConfig = { ...meta.decoderConfig };
23782
+ let requiresAdtsStripping = false;
23783
+ if (track.source._codec === "aac" && !decoderConfig.description) {
23784
+ if (!packet) {
23785
+ throw new Error("No AAC description provided; you must therefore provide a priming packet.");
23786
+ }
23787
+ const adtsFrame = readAdtsFrameHeader(FileSlice.tempFromBytes(packet.data));
23788
+ if (!adtsFrame) {
23789
+ throw new Error("Couldn't parse ADTS header from the AAC packet. Make sure the packets are in ADTS format" + " (as specified in ISO 13818-7) when not providing a description, or provide a description" + " (must be an AudioSpecificConfig as specified in ISO 14496-3) and ensure the packets" + " are raw AAC data.");
23790
+ }
23791
+ const sampleRate = aacFrequencyTable[adtsFrame.samplingFrequencyIndex];
23792
+ const numberOfChannels = aacChannelMap[adtsFrame.channelConfiguration];
23793
+ if (sampleRate === undefined || numberOfChannels === undefined) {
23794
+ throw new Error("Invalid ADTS frame header.");
23795
+ }
23796
+ decoderConfig.description = buildAacAudioSpecificConfig({
23797
+ objectType: adtsFrame.objectType,
23798
+ outputSampleRate: sampleRate,
23799
+ outputNumberOfChannels: numberOfChannels
23800
+ });
23801
+ requiresAdtsStripping = true;
23802
+ }
23803
+ const newTrackData = {
23804
+ track,
23805
+ type: "audio",
23806
+ info: {
23807
+ numberOfChannels: meta.decoderConfig.numberOfChannels,
23808
+ sampleRate: meta.decoderConfig.sampleRate,
23809
+ decoderConfig,
23810
+ requiresAdtsStripping
23811
+ },
23812
+ chunkQueue: [],
23813
+ lastWrittenMsTimestamp: null,
23814
+ codecPrivate: decoderConfig.description ?? null,
23815
+ closed: false
23816
+ };
23817
+ this.trackDatas.push(newTrackData);
23818
+ this.trackDatas.sort((a, b) => a.track.id - b.track.id);
23819
+ if (this.allTracksAreKnown()) {
23820
+ this.allTracksKnown.resolve();
23821
+ }
23822
+ return newTrackData;
23823
+ }
23824
+ getSubtitleTrackData(track, meta) {
23825
+ const existingTrackData = this.trackDatas.find((x) => x.track === track);
23826
+ if (existingTrackData) {
23827
+ return existingTrackData;
23828
+ }
23829
+ validateSubtitleMetadata(meta);
23830
+ assert(meta);
23831
+ assert(meta.config);
23832
+ const newTrackData = {
23833
+ track,
23834
+ type: "subtitle",
23835
+ info: {
23836
+ config: meta.config
23837
+ },
23838
+ chunkQueue: [],
23839
+ lastWrittenMsTimestamp: null,
23840
+ codecPrivate: textEncoder.encode(meta.config.description),
23841
+ closed: false
23842
+ };
23843
+ this.trackDatas.push(newTrackData);
23844
+ this.trackDatas.sort((a, b) => a.track.id - b.track.id);
23845
+ if (this.allTracksAreKnown()) {
23846
+ this.allTracksKnown.resolve();
23847
+ }
23848
+ return newTrackData;
23849
+ }
23850
+ async addEncodedVideoPacket(track, packet, meta) {
23851
+ const release = await this.mutex.acquire();
23852
+ try {
23853
+ const trackData = this.getVideoTrackData(track, packet, meta);
23854
+ trackData.info.alphaMode ??= !!packet.sideData.alpha;
23855
+ let packetData = packet.data;
23856
+ if (track.source._codec === "prores") {
23857
+ if (packetData.byteLength < 8) {
23858
+ throw new Error("ProRes packet too small, expected at least 8 bytes.");
23859
+ }
23860
+ packetData = packetData.subarray(8);
23861
+ }
23862
+ const isKeyFrame = packet.type === "key";
23863
+ this.validateTimestamp(trackData.track, packet.timestamp, isKeyFrame);
23864
+ let timestamp = packet.timestamp;
23865
+ let duration = packet.duration;
23866
+ if (track.metadata.frameRate !== undefined) {
23867
+ timestamp = roundToDivisor(timestamp, track.metadata.frameRate);
23868
+ duration = roundToDivisor(duration, track.metadata.frameRate);
23869
+ }
23870
+ const additions = trackData.info.alphaMode ? packet.sideData.alpha ?? null : null;
23871
+ const videoChunk = this.createInternalChunk(packetData, timestamp, duration, packet.type, additions);
23872
+ if (track.source._codec === "vp9")
23873
+ this.fixVP9ColorSpace(trackData, videoChunk);
23874
+ trackData.chunkQueue.push(videoChunk);
23875
+ await this.interleaveChunks();
23876
+ } finally {
23877
+ release();
23878
+ }
23879
+ }
23880
+ async addEncodedAudioPacket(track, packet, meta) {
23881
+ const release = await this.mutex.acquire();
23882
+ try {
23883
+ const trackData = this.getAudioTrackData(track, packet, meta);
23884
+ let packetData = packet.data;
23885
+ if (trackData.info.requiresAdtsStripping) {
23886
+ const adtsFrame = readAdtsFrameHeader(FileSlice.tempFromBytes(packetData));
23887
+ if (!adtsFrame) {
23888
+ throw new Error("Expected ADTS frame, didn't get one.");
23889
+ }
23890
+ const headerLength = adtsFrame.crcCheck === null ? MIN_ADTS_FRAME_HEADER_SIZE : MAX_ADTS_FRAME_HEADER_SIZE;
23891
+ packetData = packetData.subarray(headerLength);
23892
+ }
23893
+ const isKeyFrame = packet.type === "key";
23894
+ this.validateTimestamp(trackData.track, packet.timestamp, isKeyFrame);
23895
+ const audioChunk = this.createInternalChunk(packetData, packet.timestamp, packet.duration, packet.type);
23896
+ trackData.chunkQueue.push(audioChunk);
23897
+ await this.interleaveChunks();
23898
+ } finally {
23899
+ release();
23900
+ }
23901
+ }
23902
+ async addSubtitleCue(track, cue, meta) {
23903
+ const release = await this.mutex.acquire();
23904
+ try {
23905
+ const trackData = this.getSubtitleTrackData(track, meta);
23906
+ this.validateTimestamp(trackData.track, cue.timestamp, true);
23907
+ let bodyText = cue.text;
23908
+ const timestampMs = Math.round(cue.timestamp * 1000);
23909
+ inlineTimestampRegex.lastIndex = 0;
23910
+ bodyText = bodyText.replace(inlineTimestampRegex, (match) => {
23911
+ const time = parseSubtitleTimestamp(match.slice(1, -1));
23912
+ const offsetTime = time - timestampMs;
23913
+ return \`<\${formatSubtitleTimestamp(offsetTime)}>\`;
23914
+ });
23915
+ const body = textEncoder.encode(bodyText);
23916
+ const additions = \`\${cue.settings ?? ""}
23917
+ \${cue.identifier ?? ""}
23918
+ \${cue.notes ?? ""}\`;
23919
+ const subtitleChunk = this.createInternalChunk(body, cue.timestamp, cue.duration, "key", additions.trim() ? textEncoder.encode(additions) : null);
23920
+ trackData.chunkQueue.push(subtitleChunk);
23921
+ await this.interleaveChunks();
23922
+ } finally {
23923
+ release();
23924
+ }
23925
+ }
23926
+ async interleaveChunks(isFinalCall = false) {
23927
+ if (!isFinalCall && !this.allTracksAreKnown()) {
23928
+ return;
23929
+ }
23930
+ outer:
23931
+ while (true) {
23932
+ let trackWithMinTimestamp = null;
23933
+ let minTimestamp = Infinity;
23934
+ for (const trackData of this.trackDatas) {
23935
+ if (!isFinalCall && trackData.chunkQueue.length === 0 && !trackData.closed) {
23936
+ break outer;
23937
+ }
23938
+ if (trackData.chunkQueue.length > 0 && trackData.chunkQueue[0].timestamp < minTimestamp) {
23939
+ trackWithMinTimestamp = trackData;
23940
+ minTimestamp = trackData.chunkQueue[0].timestamp;
23941
+ }
23942
+ }
23943
+ if (!trackWithMinTimestamp) {
23944
+ break;
23945
+ }
23946
+ const chunk = trackWithMinTimestamp.chunkQueue.shift();
23947
+ this.writeBlock(trackWithMinTimestamp, chunk);
23948
+ }
23949
+ if (!isFinalCall) {
23950
+ await this.writer.flush();
23951
+ }
23952
+ }
23953
+ fixVP9ColorSpace(trackData, chunk) {
23954
+ if (chunk.type !== "key")
23955
+ return;
23956
+ if (!trackData.info.decoderConfig.colorSpace || !trackData.info.decoderConfig.colorSpace.matrix)
23957
+ return;
23958
+ const bitstream = new Bitstream(chunk.data);
23959
+ bitstream.skipBits(2);
23960
+ const profileLowBit = bitstream.readBits(1);
23961
+ const profileHighBit = bitstream.readBits(1);
23962
+ const profile = (profileHighBit << 1) + profileLowBit;
23963
+ if (profile === 3)
23964
+ bitstream.skipBits(1);
23965
+ const showExistingFrame = bitstream.readBits(1);
23966
+ if (showExistingFrame)
23967
+ return;
23968
+ const frameType = bitstream.readBits(1);
23969
+ if (frameType !== 0)
23970
+ return;
23971
+ bitstream.skipBits(2);
23972
+ const syncCode = bitstream.readBits(24);
23973
+ if (syncCode !== 4817730)
23974
+ return;
23975
+ if (profile >= 2)
23976
+ bitstream.skipBits(1);
23977
+ const colorSpaceID = {
23978
+ rgb: 7,
23979
+ bt709: 2,
23980
+ bt470bg: 1,
23981
+ smpte170m: 3
23982
+ }[trackData.info.decoderConfig.colorSpace.matrix];
23983
+ writeBits(chunk.data, bitstream.pos, bitstream.pos + 3, colorSpaceID);
23984
+ }
23985
+ createInternalChunk(data, timestamp, duration, type, additions = null) {
23986
+ const internalChunk = {
23987
+ data,
23988
+ type,
23989
+ timestamp,
23990
+ duration,
23991
+ additions
23992
+ };
23993
+ return internalChunk;
23994
+ }
23995
+ writeBlock(trackData, chunk) {
23996
+ if (!this.segment) {
23997
+ this.createSegment();
23998
+ }
23999
+ const msTimestamp = Math.round(1000 * chunk.timestamp);
24000
+ const keyFrameQueuedEverywhere = this.trackDatas.every((otherTrackData) => {
24001
+ if (trackData === otherTrackData) {
24002
+ return chunk.type === "key";
24003
+ }
24004
+ const firstQueuedSample = otherTrackData.chunkQueue[0];
24005
+ if (firstQueuedSample) {
24006
+ return firstQueuedSample.type === "key";
24007
+ }
24008
+ return otherTrackData.closed;
24009
+ });
24010
+ let shouldCreateNewCluster = false;
24011
+ if (!this.currentCluster) {
24012
+ shouldCreateNewCluster = true;
24013
+ } else {
24014
+ assert(this.currentClusterStartMsTimestamp !== null);
24015
+ assert(this.currentClusterMaxMsTimestamp !== null);
24016
+ const relativeTimestamp = msTimestamp - this.currentClusterStartMsTimestamp;
24017
+ shouldCreateNewCluster = keyFrameQueuedEverywhere && msTimestamp > this.currentClusterMaxMsTimestamp && relativeTimestamp >= 1000 * (this.format._options.minimumClusterDuration ?? 1) || relativeTimestamp > MAX_CLUSTER_TIMESTAMP_MS;
24018
+ }
24019
+ if (shouldCreateNewCluster) {
24020
+ this.createNewCluster(msTimestamp);
24021
+ }
24022
+ const relativeTimestamp = msTimestamp - this.currentClusterStartMsTimestamp;
24023
+ if (relativeTimestamp < MIN_CLUSTER_TIMESTAMP_MS) {
24024
+ if (!this.warnedAboutTooNegativeTimestamp) {
24025
+ const formatName = this.format instanceof WebMOutputFormat ? "WebM" : "Matroska";
24026
+ Logging._warn(\`Packets had to be discarded because their timestamp is too negative to represent in\` + \` \${formatName}.\`);
24027
+ this.warnedAboutTooNegativeTimestamp = true;
24028
+ }
24029
+ return;
24030
+ }
24031
+ const prelude = new Uint8Array(4);
24032
+ const view = new DataView(prelude.buffer);
24033
+ view.setUint8(0, 128 | trackData.track.id);
24034
+ view.setInt16(1, relativeTimestamp, false);
24035
+ const msDuration = Math.round(1000 * chunk.duration);
24036
+ const needsBlockGroup = !!chunk.additions || trackData.type === "subtitle";
24037
+ if (!needsBlockGroup) {
24038
+ view.setUint8(3, Number(chunk.type === "key") << 7);
24039
+ const simpleBlock = { id: EBMLId.SimpleBlock, data: [
24040
+ prelude,
24041
+ chunk.data
24042
+ ] };
24043
+ this.ebmlWriter.writeEBML(simpleBlock);
24044
+ } else {
24045
+ const blockGroup = { id: EBMLId.BlockGroup, data: [
24046
+ { id: EBMLId.Block, data: [
24047
+ prelude,
24048
+ chunk.data
24049
+ ] },
24050
+ chunk.type === "delta" ? {
24051
+ id: EBMLId.ReferenceBlock,
24052
+ data: new EBMLSignedInt(trackData.lastWrittenMsTimestamp - msTimestamp)
24053
+ } : null,
24054
+ chunk.additions ? { id: EBMLId.BlockAdditions, data: [
24055
+ { id: EBMLId.BlockMore, data: [
24056
+ { id: EBMLId.BlockAddID, data: 1 },
24057
+ { id: EBMLId.BlockAdditional, data: chunk.additions }
24058
+ ] }
24059
+ ] } : null,
24060
+ msDuration > 0 ? { id: EBMLId.BlockDuration, data: msDuration } : null
24061
+ ] };
24062
+ this.ebmlWriter.writeEBML(blockGroup);
24063
+ }
24064
+ this.startTimestamp = Math.min(this.startTimestamp, msTimestamp);
24065
+ this.endTimestamp = Math.max(this.endTimestamp, msTimestamp + msDuration);
24066
+ trackData.lastWrittenMsTimestamp = msTimestamp;
24067
+ if (!this.trackDatasInCurrentCluster.has(trackData)) {
24068
+ this.trackDatasInCurrentCluster.set(trackData, {
24069
+ firstMsTimestamp: msTimestamp
24070
+ });
24071
+ }
24072
+ this.currentClusterMaxMsTimestamp = Math.max(this.currentClusterMaxMsTimestamp, msTimestamp);
24073
+ }
24074
+ createNewCluster(msTimestamp) {
24075
+ msTimestamp = Math.max(0, msTimestamp);
24076
+ if (this.currentCluster) {
24077
+ this.finalizeCurrentCluster();
24078
+ }
24079
+ if (this.format._options.onCluster) {
24080
+ this.writer.startTrackingWrites();
24081
+ }
24082
+ this.currentCluster = {
24083
+ id: EBMLId.Cluster,
24084
+ size: this.format._options.appendOnly ? -1 : CLUSTER_SIZE_BYTES,
24085
+ data: [
24086
+ { id: EBMLId.Timestamp, data: msTimestamp }
24087
+ ]
24088
+ };
24089
+ this.ebmlWriter.writeEBML(this.currentCluster);
24090
+ this.currentClusterStartMsTimestamp = msTimestamp;
24091
+ this.currentClusterMaxMsTimestamp = msTimestamp;
24092
+ this.trackDatasInCurrentCluster.clear();
24093
+ }
24094
+ finalizeCurrentCluster() {
24095
+ assert(this.currentCluster);
24096
+ if (!this.format._options.appendOnly) {
24097
+ const clusterSize = this.writer.getPos() - this.ebmlWriter.dataOffsets.get(this.currentCluster);
24098
+ const endPos = this.writer.getPos();
24099
+ this.writer.seek(this.ebmlWriter.offsets.get(this.currentCluster) + 4);
24100
+ this.ebmlWriter.writeVarInt(clusterSize, CLUSTER_SIZE_BYTES);
24101
+ this.writer.seek(endPos);
24102
+ }
24103
+ if (this.format._options.onCluster) {
24104
+ assert(this.currentClusterStartMsTimestamp !== null);
24105
+ const { data, start } = this.writer.stopTrackingWrites();
24106
+ this.format._options.onCluster(data, start, this.currentClusterStartMsTimestamp / 1000);
24107
+ }
24108
+ const clusterOffsetFromSegment = this.ebmlWriter.offsets.get(this.currentCluster) - this.segmentDataOffset;
24109
+ const groupedByTimestamp = new Map;
24110
+ for (const [trackData, { firstMsTimestamp }] of this.trackDatasInCurrentCluster) {
24111
+ if (!groupedByTimestamp.has(firstMsTimestamp)) {
24112
+ groupedByTimestamp.set(firstMsTimestamp, []);
24113
+ }
24114
+ groupedByTimestamp.get(firstMsTimestamp).push(trackData);
24115
+ }
24116
+ const groupedAndSortedByTimestamp = [...groupedByTimestamp.entries()].sort((a, b) => a[0] - b[0]);
24117
+ for (const [msTimestamp, trackDatas] of groupedAndSortedByTimestamp) {
24118
+ assert(this.cues);
24119
+ this.cues.data.push({ id: EBMLId.CuePoint, data: [
24120
+ { id: EBMLId.CueTime, data: Math.max(0, msTimestamp) },
24121
+ ...trackDatas.map((trackData) => {
24122
+ return { id: EBMLId.CueTrackPositions, data: [
24123
+ { id: EBMLId.CueTrack, data: trackData.track.id },
24124
+ { id: EBMLId.CueClusterPosition, data: clusterOffsetFromSegment }
24125
+ ] };
24126
+ })
24127
+ ] });
24128
+ }
24129
+ }
24130
+ async onTrackClose(track) {
24131
+ const release = await this.mutex.acquire();
24132
+ const trackData = this.trackDatas.find((x) => x.track === track);
24133
+ if (trackData) {
24134
+ trackData.closed = true;
24135
+ }
24136
+ if (this.allTracksAreKnown()) {
24137
+ this.allTracksKnown.resolve();
24138
+ }
24139
+ await this.interleaveChunks();
24140
+ release();
24141
+ }
24142
+ async finalize() {
24143
+ const release = await this.mutex.acquire();
24144
+ this.allTracksKnown.resolve();
24145
+ for (const trackData of this.trackDatas) {
24146
+ trackData.closed = true;
24147
+ }
24148
+ if (!this.segment) {
24149
+ this.createSegment();
24150
+ }
24151
+ await this.interleaveChunks(true);
24152
+ if (this.currentCluster) {
24153
+ this.finalizeCurrentCluster();
24154
+ }
24155
+ assert(this.cues);
24156
+ this.ebmlWriter.writeEBML(this.cues);
24157
+ if (!this.format._options.appendOnly) {
24158
+ const segmentSize = this.writer.getPos() - this.segmentDataOffset;
24159
+ this.writer.seek(this.ebmlWriter.offsets.get(this.segment) + 4);
24160
+ this.ebmlWriter.writeVarInt(segmentSize, SEGMENT_SIZE_BYTES);
24161
+ const duration = this.startTimestamp === Infinity ? 0 : this.endTimestamp;
24162
+ this.segmentDuration.data = new EBMLFloat64(duration);
24163
+ this.writer.seek(this.ebmlWriter.offsets.get(this.segmentDuration));
24164
+ this.ebmlWriter.writeEBML(this.segmentDuration);
24165
+ assert(this.seekHead);
24166
+ this.writer.seek(this.ebmlWriter.offsets.get(this.seekHead));
24167
+ this.maybeCreateSeekHead(true);
24168
+ this.ebmlWriter.writeEBML(this.seekHead);
24169
+ }
24170
+ release();
24171
+ }
24172
+ }
24173
+
21970
24174
  // ../../node_modules/.bun/mediabunny@1.61.3/node_modules/mediabunny/dist/modules/src/resample.js
21971
24175
  /*!
21972
24176
  * Copyright (c) 2026-present, Vanilagy and contributors
@@ -23737,6 +25941,97 @@ var workerCode = `(() => {
23737
25941
  }
23738
25942
  }
23739
25943
 
25944
+ class MkvOutputFormat extends OutputFormat {
25945
+ constructor(options = {}) {
25946
+ if (!options || typeof options !== "object") {
25947
+ throw new TypeError("options must be an object.");
25948
+ }
25949
+ if (options.appendOnly !== undefined && typeof options.appendOnly !== "boolean") {
25950
+ throw new TypeError("options.appendOnly, when provided, must be a boolean.");
25951
+ }
25952
+ if (options.minimumClusterDuration !== undefined && (!isNumber(options.minimumClusterDuration) || options.minimumClusterDuration < 0)) {
25953
+ throw new TypeError("options.minimumClusterDuration, when provided, must be a non-negative number.");
25954
+ }
25955
+ if (options.onEbmlHeader !== undefined && typeof options.onEbmlHeader !== "function") {
25956
+ throw new TypeError("options.onEbmlHeader, when provided, must be a function.");
25957
+ }
25958
+ if (options.onSegmentHeader !== undefined && typeof options.onSegmentHeader !== "function") {
25959
+ throw new TypeError("options.onHeader, when provided, must be a function.");
25960
+ }
25961
+ if (options.onCluster !== undefined && typeof options.onCluster !== "function") {
25962
+ throw new TypeError("options.onCluster, when provided, must be a function.");
25963
+ }
25964
+ super();
25965
+ this._options = options;
25966
+ }
25967
+ _createMuxer(output) {
25968
+ return new MatroskaMuxer(output, this);
25969
+ }
25970
+ get _name() {
25971
+ return "Matroska";
25972
+ }
25973
+ getSupportedTrackCounts() {
25974
+ const max = 127;
25975
+ return {
25976
+ video: { min: 0, max },
25977
+ audio: { min: 0, max },
25978
+ subtitle: { min: 0, max },
25979
+ total: { min: 0, max }
25980
+ };
25981
+ }
25982
+ get fileExtension() {
25983
+ return ".mkv";
25984
+ }
25985
+ get mimeType() {
25986
+ return "video/x-matroska";
25987
+ }
25988
+ getSupportedCodecs() {
25989
+ return [
25990
+ ...VIDEO_CODECS,
25991
+ ...NON_PCM_AUDIO_CODECS,
25992
+ ...PCM_AUDIO_CODECS.filter((codec) => !["pcm-s8", "pcm-f32be", "pcm-f64be", "ulaw", "alaw"].includes(codec)),
25993
+ ...SUBTITLE_CODECS
25994
+ ];
25995
+ }
25996
+ get supportsVideoTransformationMetadata() {
25997
+ return false;
25998
+ }
25999
+ get supportsTimestampedMediaData() {
26000
+ return true;
26001
+ }
26002
+ get negativeTimestampSupport() {
26003
+ return "prefer-non-negative";
26004
+ }
26005
+ }
26006
+
26007
+ class WebMOutputFormat extends MkvOutputFormat {
26008
+ constructor(options) {
26009
+ super(options);
26010
+ }
26011
+ getSupportedCodecs() {
26012
+ return [
26013
+ ...VIDEO_CODECS.filter((codec) => ["vp8", "vp9", "av1"].includes(codec)),
26014
+ ...AUDIO_CODECS.filter((codec) => ["opus", "vorbis"].includes(codec)),
26015
+ ...SUBTITLE_CODECS
26016
+ ];
26017
+ }
26018
+ get _name() {
26019
+ return "WebM";
26020
+ }
26021
+ get fileExtension() {
26022
+ return ".webm";
26023
+ }
26024
+ get mimeType() {
26025
+ return "video/webm";
26026
+ }
26027
+ _codecUnsupportedHint(codec) {
26028
+ if (new MkvOutputFormat().getSupportedCodecs().includes(codec)) {
26029
+ return " Switching to MKV will grant support for this codec.";
26030
+ }
26031
+ return "";
26032
+ }
26033
+ }
26034
+
23740
26035
  // ../../node_modules/.bun/mediabunny@1.61.3/node_modules/mediabunny/dist/modules/src/output.js
23741
26036
  /*!
23742
26037
  * Copyright (c) 2026-present, Vanilagy and contributors
@@ -24358,6 +26653,34 @@ Mediabunny was loaded twice.\` + " This will likely cause Mediabunny not to work
24358
26653
  return value;
24359
26654
  }
24360
26655
 
26656
+ // src/core/processor/worker/audio-only-config.ts
26657
+ var AUDIO_CODEC_CONTAINERS = {
26658
+ aac: "mp4",
26659
+ opus: "webm"
26660
+ };
26661
+ function validateAudioOnlyWorkerConfig(config, audioConfig) {
26662
+ if (!audioConfig) {
26663
+ throw new Error("Audio-only recording requires an audio input");
26664
+ }
26665
+ if (audioConfig.sampleRate <= 0) {
26666
+ throw new Error("Audio sample rate must be greater than zero");
26667
+ }
26668
+ if (audioConfig.numberOfChannels <= 0) {
26669
+ throw new Error("Audio channels must be greater than zero");
26670
+ }
26671
+ if (!Number.isFinite(config.audioBitrate) || config.audioBitrate <= 0) {
26672
+ throw new Error("Audio bitrate must be greater than zero");
26673
+ }
26674
+ const expectedFormat = AUDIO_CODEC_CONTAINERS[config.audioCodec];
26675
+ if (expectedFormat === undefined) {
26676
+ throw new Error(\`Audio codec \${config.audioCodec} is not supported\`);
26677
+ }
26678
+ if (config.format !== expectedFormat) {
26679
+ throw new Error(\`Audio codec \${config.audioCodec} requires the \${expectedFormat} container\`);
26680
+ }
26681
+ return audioConfig;
26682
+ }
26683
+
24361
26684
  // src/core/processor/worker/audio-state.ts
24362
26685
  var MILLISECONDS_PER_SECOND = 1000;
24363
26686
 
@@ -25539,6 +27862,7 @@ Mediabunny was loaded twice.\` + " This will likely cause Mediabunny not to work
25539
27862
  }
25540
27863
 
25541
27864
  // src/core/processor/worker/types.ts
27865
+ var WORKER_MESSAGE_TYPE_START_AUDIO = "startAudio";
25542
27866
  var WORKER_MESSAGE_TYPE_PROBE = "probe";
25543
27867
  var WORKER_MESSAGE_TYPE_WARMUP = "warmup";
25544
27868
  var WORKER_MESSAGE_TYPE_AUDIO_CHUNK = "audioChunk";
@@ -25758,6 +28082,7 @@ Mediabunny was loaded twice.\` + " This will likely cause Mediabunny not to work
25758
28082
  videoProcessingRunId = 0;
25759
28083
  totalFrameErrors = 0;
25760
28084
  totalFramesProcessed = 0;
28085
+ isAudioOnly = false;
25761
28086
  constructor() {
25762
28087
  this.bufferTracker = new BufferTracker({
25763
28088
  getBufferSize: () => this.totalSize,
@@ -25878,6 +28203,9 @@ Mediabunny was loaded twice.\` + " This will likely cause Mediabunny not to work
25878
28203
  case "start":
25879
28204
  this.handleStartMessage(message);
25880
28205
  return;
28206
+ case WORKER_MESSAGE_TYPE_START_AUDIO:
28207
+ this.handleStartAudioMessage(message);
28208
+ return;
25881
28209
  case "pause":
25882
28210
  this.handlePause();
25883
28211
  return;
@@ -25927,6 +28255,40 @@ Mediabunny was loaded twice.\` + " This will likely cause Mediabunny not to work
25927
28255
  }
25928
28256
  this.handleAsyncOperation(this.handleStart(message, videoTrack, videoStream), "handleStart");
25929
28257
  }
28258
+ handleStartAudioMessage(message) {
28259
+ if (this.shouldIgnoreMessage()) {
28260
+ logger.debug("[RecorderWorker] startAudio ignored (stopping/finalized)");
28261
+ return;
28262
+ }
28263
+ this.handleAsyncOperation(this.handleAudioOnlyStart(message), "handleAudioOnlyStart");
28264
+ }
28265
+ async handleAudioOnlyStart(message) {
28266
+ const audioConfig = validateAudioOnlyWorkerConfig(message.config, message.audioConfig);
28267
+ this.isStopping = false;
28268
+ this.isFinalized = false;
28269
+ if (this.output) {
28270
+ await this.cleanup();
28271
+ }
28272
+ this.isAudioOnly = true;
28273
+ this.config = null;
28274
+ this.audioState.reset();
28275
+ this.pendingWriteCount = 0;
28276
+ this.expectedAudioChannels = audioConfig.numberOfChannels;
28277
+ this.expectedAudioSampleRate = audioConfig.sampleRate;
28278
+ this.createOutput(message.config.format);
28279
+ this.audioSource = new AudioSampleSource({
28280
+ codec: message.config.audioCodec,
28281
+ bitrate: message.config.audioBitrate,
28282
+ bitrateMode: "variable"
28283
+ });
28284
+ const output = requireNonNull(this.output, "Output must be initialized before adding audio track");
28285
+ output.addAudioTrack(this.audioSource);
28286
+ this.audioState.setProcessingActive(true);
28287
+ await output.start();
28288
+ this.bufferTracker.start();
28289
+ this.sendReady();
28290
+ this.sendStateChange("recording");
28291
+ }
25930
28292
  handleStopMessage() {
25931
28293
  if (this.shouldIgnoreMessage()) {
25932
28294
  logger.debug("[RecorderWorker] stop ignored (stopping/finalized)");
@@ -26003,12 +28365,12 @@ Mediabunny was loaded twice.\` + " This will likely cause Mediabunny not to work
26003
28365
  };
26004
28366
  logger.debug("[RecorderWorker] Overlay config initialized", logData);
26005
28367
  }
26006
- createOutput() {
28368
+ createOutput(container = "mp4") {
26007
28369
  const writable = new WritableStream({
26008
28370
  write: (chunk) => this.handleOutputChunkWrite(chunk)
26009
28371
  });
26010
28372
  this.output = new Output({
26011
- format: new Mp4OutputFormat({
28373
+ format: container === "webm" ? new WebMOutputFormat : new Mp4OutputFormat({
26012
28374
  fastStart: MP4_FAST_START_DISABLED
26013
28375
  }),
26014
28376
  target: new StreamTarget(writable, {
@@ -26164,6 +28526,7 @@ Mediabunny was loaded twice.\` + " This will likely cause Mediabunny not to work
26164
28526
  logger.debug("[RecorderWorker] Cleaning up existing output");
26165
28527
  await this.cleanup();
26166
28528
  }
28529
+ this.isAudioOnly = false;
26167
28530
  this.initializeRecordingState(config);
26168
28531
  const resolvedVideoSettings = message.videoSettings;
26169
28532
  if (resolvedVideoSettings) {
@@ -26619,6 +28982,9 @@ Mediabunny was loaded twice.\` + " This will likely cause Mediabunny not to work
26619
28982
  timeoutMilliseconds: STOP_PENDING_WRITES_TIMEOUT_MILLISECONDS2
26620
28983
  });
26621
28984
  this.sendRecordingStats();
28985
+ if (this.isAudioOnly) {
28986
+ return;
28987
+ }
26622
28988
  this.logEarlyFrameWarning();
26623
28989
  const integrity = validateRecordingIntegrity({
26624
28990
  frameCount: this.timestampManager.getFrameCount(),
@@ -26793,6 +29159,7 @@ Mediabunny was loaded twice.\` + " This will likely cause Mediabunny not to work
26793
29159
  this.totalFrameErrors = 0;
26794
29160
  this.totalFramesProcessed = 0;
26795
29161
  this.visibilityTracker.reset(this.recordingStartTime, this.isScreenCapture);
29162
+ this.isAudioOnly = false;
26796
29163
  }
26797
29164
  setExpectedAudioFormat(sampleRate, numberOfChannels) {
26798
29165
  if (this.expectedAudioSampleRate === null) {
@@ -27038,6 +29405,7 @@ class WorkerProcessor {
27038
29405
  videoFramePreflightDeps;
27039
29406
  noFrameWatchdogId = null;
27040
29407
  watchdogDelayMs;
29408
+ audioOnlyRecording = null;
27041
29409
  constructor(dependencies = {}) {
27042
29410
  let createWorkerFn = (workerUrl) => new Worker(workerUrl, { type: "classic" });
27043
29411
  if (dependencies.createWorker) {
@@ -27216,9 +29584,14 @@ class WorkerProcessor {
27216
29584
  return this.workerProbeManager.getProbeResult(worker);
27217
29585
  }
27218
29586
  async startProcessing(stream, config, overlayConfig) {
29587
+ if (config.mediaType === "audio") {
29588
+ await this.startAudioOnlyProcessing(stream, config.audioRecording);
29589
+ return;
29590
+ }
27219
29591
  this.getWorkerOrThrow();
27220
29592
  this.ensureProcessingInactive();
27221
29593
  this.resetProcessingState(overlayConfig);
29594
+ this.audioOnlyRecording = null;
27222
29595
  this.stopAudioWorklet();
27223
29596
  this.audioWorkletManager = new AudioWorkletManager({
27224
29597
  onChunk: this.handleAudioWorkletChunk.bind(this)
@@ -27310,6 +29683,57 @@ class WorkerProcessor {
27310
29683
  throw error;
27311
29684
  }
27312
29685
  }
29686
+ async startAudioOnlyProcessing(stream, audioRecording) {
29687
+ if (!audioRecording) {
29688
+ throw new Error("Audio recording config is required");
29689
+ }
29690
+ this.getWorkerOrThrow();
29691
+ this.ensureProcessingInactive();
29692
+ this.resetProcessingState();
29693
+ this.stopAudioWorklet();
29694
+ this.audioWorkletManager = new AudioWorkletManager({
29695
+ onChunk: this.handleAudioWorkletChunk.bind(this)
29696
+ });
29697
+ await this.prepareAudioOnlyStart(stream, audioRecording).catch((error) => {
29698
+ this.audioOnlyRecording = null;
29699
+ this.resetStartProcessingStateAfterFailure();
29700
+ throw error;
29701
+ });
29702
+ this.startAudioHealthMonitoring();
29703
+ }
29704
+ async prepareAudioOnlyStart(stream, audioRecording) {
29705
+ const audioTrack = prepareAudioTrack(stream.getAudioTracks(), this.getVideoInputSelectorDependencies());
29706
+ if (!audioTrack) {
29707
+ throw createAudioNoTrackAtStartError();
29708
+ }
29709
+ const audioConfig = await this.audioWorkletManager.prepareAudioConfig(audioTrack, audioRecording.channels);
29710
+ if (!audioConfig) {
29711
+ throw this.createBrowserUnsupportedError();
29712
+ }
29713
+ const outputFormat = await resolveAudioOutputFormat(audioRecording.format, {
29714
+ isLinuxPlatform: this.isLinuxPlatformFn(),
29715
+ canEncode: (format) => canEncodeAudio2(format, {
29716
+ numberOfChannels: audioConfig.numberOfChannels,
29717
+ sampleRate: audioConfig.sampleRate,
29718
+ bitrate: audioRecording.bitrate
29719
+ })
29720
+ });
29721
+ this.audioOnlyRecording = {
29722
+ outputFormat,
29723
+ bitrate: audioRecording.bitrate,
29724
+ peaks: new AudioPeaksAccumulator
29725
+ };
29726
+ const message = {
29727
+ type: WORKER_MESSAGE_TYPE_START_AUDIO,
29728
+ audioConfig,
29729
+ config: {
29730
+ format: outputFormat.container,
29731
+ audioCodec: outputFormat.format,
29732
+ audioBitrate: audioRecording.bitrate
29733
+ }
29734
+ };
29735
+ await this.postStartMessage(message, [], true);
29736
+ }
27313
29737
  getWorkerOrThrow() {
27314
29738
  if (!this.worker) {
27315
29739
  throw new Error("Worker not initialized");
@@ -27326,6 +29750,7 @@ class WorkerProcessor {
27326
29750
  return;
27327
29751
  }
27328
29752
  this.recordAudioHealthChunk(chunk);
29753
+ this.audioOnlyRecording?.peaks.addPlanarSamples(chunk.data, chunk.frames, chunk.numberOfChannels, chunk.sampleRate);
27329
29754
  const message = {
27330
29755
  type: WORKER_MESSAGE_TYPE_AUDIO_CHUNK,
27331
29756
  data: chunk.data,
@@ -27428,7 +29853,7 @@ class WorkerProcessor {
27428
29853
  this.noFrameWatchdogId = null;
27429
29854
  }
27430
29855
  handleNoFrameWatchdogStats(stats) {
27431
- if (!this.isActive || stats.videoFrameCount > 0) {
29856
+ if (!this.isActive || this.audioOnlyRecording !== null || stats.videoFrameCount > 0) {
27432
29857
  return;
27433
29858
  }
27434
29859
  logger.error("[WorkerProcessor] No video frames received after watchdog delay", {
@@ -27701,6 +30126,7 @@ class WorkerProcessor {
27701
30126
  const message = { type: "pause" };
27702
30127
  this.worker.postMessage(message);
27703
30128
  this.setAudioWorkletPaused(true);
30129
+ this.audioHealthMonitor.pause(performance.now());
27704
30130
  }
27705
30131
  resume() {
27706
30132
  if (!(this.isWorkerActive() && this.worker)) {
@@ -27709,6 +30135,7 @@ class WorkerProcessor {
27709
30135
  const message = { type: "resume" };
27710
30136
  this.worker.postMessage(message);
27711
30137
  this.setAudioWorkletPaused(false);
30138
+ this.audioHealthMonitor.resume(performance.now());
27712
30139
  }
27713
30140
  isWorkerActive() {
27714
30141
  return !!(this.worker && this.isActive);
@@ -27945,6 +30372,9 @@ class WorkerProcessor {
27945
30372
  for (const chunk of sortedChunks) {
27946
30373
  view.set(chunk.data, chunk.position);
27947
30374
  }
30375
+ if (this.audioOnlyRecording) {
30376
+ return this.createAudioOnlyResult(buffer, this.audioOnlyRecording);
30377
+ }
27948
30378
  assertMp4ContainerIsNonFragmented(buffer);
27949
30379
  assertMp4HasVideoTrack(buffer);
27950
30380
  if (this.audioWasExpected) {
@@ -27968,6 +30398,29 @@ class WorkerProcessor {
27968
30398
  }
27969
30399
  return streamProcessorResult;
27970
30400
  }
30401
+ createAudioOnlyResult(buffer, recording) {
30402
+ this.audioOnlyRecording = null;
30403
+ if (recording.outputFormat.container === "mp4") {
30404
+ assertMp4ContainerIsNonFragmented(buffer);
30405
+ }
30406
+ this.assertRuntimeAudioHealth();
30407
+ if (recording.outputFormat.container === "mp4") {
30408
+ assertMp4HasAudioTrack(buffer);
30409
+ }
30410
+ const blob = new Blob([buffer], {
30411
+ type: recording.outputFormat.mimeType
30412
+ });
30413
+ return {
30414
+ blob,
30415
+ totalSize: this.totalSize,
30416
+ audio: {
30417
+ blob,
30418
+ outputFormat: recording.outputFormat,
30419
+ bitrate: recording.bitrate,
30420
+ peaks: recording.peaks.toPeaks()
30421
+ }
30422
+ };
30423
+ }
27971
30424
  rejectFinalizeBlobCreationError(reject, error, finalizeStartedAtMilliseconds) {
27972
30425
  logger.error("[WorkerProcessor] Finalize failed while creating blob", {
27973
30426
  elapsedSeconds: (performance.now() - finalizeStartedAtMilliseconds) / MILLISECONDS_PER_SECOND3,
@@ -27992,6 +30445,7 @@ class WorkerProcessor {
27992
30445
  this.isPaused = false;
27993
30446
  this.chunks = [];
27994
30447
  this.totalSize = 0;
30448
+ this.audioOnlyRecording = null;
27995
30449
  this.audioWasExpected = false;
27996
30450
  this.emittedAudioWarnings = new Set;
27997
30451
  this.audioTrackEndedDuringSession = false;
@@ -28407,19 +30861,19 @@ class StreamProcessor {
28407
30861
  }
28408
30862
 
28409
30863
  // src/core/transcode/deferred-video-transcoder.ts
28410
- import { ALL_FORMATS as ALL_FORMATS3, BlobSource as BlobSource4, EncodedPacketSink as EncodedPacketSink2, Input as Input4 } from "mediabunny";
30864
+ import { ALL_FORMATS as ALL_FORMATS4, BlobSource as BlobSource5, EncodedPacketSink as EncodedPacketSink2, Input as Input5 } from "mediabunny";
28411
30865
 
28412
30866
  // src/core/transcode/deferred-video-timing-repair.ts
28413
30867
  import {
28414
- ALL_FORMATS as ALL_FORMATS2,
28415
- BlobSource as BlobSource3,
28416
- BufferTarget as BufferTarget2,
30868
+ ALL_FORMATS as ALL_FORMATS3,
30869
+ BlobSource as BlobSource4,
30870
+ BufferTarget as BufferTarget3,
28417
30871
  EncodedAudioPacketSource,
28418
30872
  EncodedPacketSink,
28419
30873
  EncodedVideoPacketSource,
28420
- Input as Input3,
28421
- Mp4OutputFormat as Mp4OutputFormat2,
28422
- Output as Output2
30874
+ Input as Input4,
30875
+ Mp4OutputFormat as Mp4OutputFormat3,
30876
+ Output as Output3
28423
30877
  } from "mediabunny";
28424
30878
  var MINIMUM_IMPLAUSIBLE_DURATION_SECONDS = 60;
28425
30879
  var EXPECTED_DURATION_MULTIPLIER = 2;
@@ -28609,9 +31063,9 @@ async function remuxRepairedVideo(input, videoTrack, plan) {
28609
31063
  throw new Error("Timing repair requires a supported audio codec");
28610
31064
  }
28611
31065
  }
28612
- const target = new BufferTarget2;
28613
- const output = new Output2({
28614
- format: new Mp4OutputFormat2({ fastStart: "in-memory" }),
31066
+ const target = new BufferTarget3;
31067
+ const output = new Output3({
31068
+ format: new Mp4OutputFormat3({ fastStart: "in-memory" }),
28615
31069
  target
28616
31070
  });
28617
31071
  const videoSource = new EncodedVideoPacketSource(videoCodec);
@@ -28636,9 +31090,9 @@ async function remuxRepairedVideo(input, videoTrack, plan) {
28636
31090
  return new Blob([target.buffer], { type: "video/mp4" });
28637
31091
  }
28638
31092
  async function repairDeferredVideoTiming(blob, expectedDurationSeconds) {
28639
- const input = new Input3({
28640
- formats: ALL_FORMATS2,
28641
- source: new BlobSource3(blob)
31093
+ const input = new Input4({
31094
+ formats: ALL_FORMATS3,
31095
+ source: new BlobSource4(blob)
28642
31096
  });
28643
31097
  try {
28644
31098
  const videoTrack = await input.getPrimaryVideoTrack();
@@ -28813,9 +31267,9 @@ function validateDeferredVideoStatsWithDiagnostics(validation, additionalDiagnos
28813
31267
  }
28814
31268
  }
28815
31269
  async function inspectVideoPackets(blob) {
28816
- const input = new Input4({
28817
- formats: ALL_FORMATS3,
28818
- source: new BlobSource4(blob)
31270
+ const input = new Input5({
31271
+ formats: ALL_FORMATS4,
31272
+ source: new BlobSource5(blob)
28819
31273
  });
28820
31274
  const videoTrack = await input.getPrimaryVideoTrack();
28821
31275
  if (videoTrack === null) {
@@ -28950,7 +31404,8 @@ var DEFAULT_ROUTE_DECISION2 = {
28950
31404
  reasonCodes: ["webcodecs-supported", "target-codec-supported"]
28951
31405
  };
28952
31406
  var DEFAULT_RECORDING_MANAGER_DEPENDENCIES = {
28953
- transcodeVideo: transcodeDeferredVideo
31407
+ transcodeVideo: transcodeDeferredVideo,
31408
+ convertRecordedAudio
28954
31409
  };
28955
31410
 
28956
31411
  class RecordingManager {
@@ -28962,6 +31417,8 @@ class RecordingManager {
28962
31417
  countdownStartTime = null;
28963
31418
  isPaused = false;
28964
31419
  maxRecordingTime = null;
31420
+ activeMaxRecordingTime = null;
31421
+ lastAudioRecording = null;
28965
31422
  maxTimeTimer = null;
28966
31423
  recordingStartTime = null;
28967
31424
  maxTimeRemaining = null;
@@ -29004,6 +31461,12 @@ class RecordingManager {
29004
31461
  isPausedState() {
29005
31462
  return this.isPaused;
29006
31463
  }
31464
+ getAudioRecording(blob) {
31465
+ if (this.lastAudioRecording?.blob !== blob) {
31466
+ return null;
31467
+ }
31468
+ return this.lastAudioRecording;
31469
+ }
29007
31470
  getRecordingSeconds() {
29008
31471
  return this.recordingSeconds;
29009
31472
  }
@@ -29151,8 +31614,9 @@ class RecordingManager {
29151
31614
  this.callbacks.onStateChange(this.recordingState);
29152
31615
  this.startRecordingTimer();
29153
31616
  this.recordingStartTime = Date.now();
29154
- if (this.maxRecordingTime && this.maxRecordingTime > 0) {
29155
- this.maxTimeRemaining = this.maxRecordingTime;
31617
+ this.activeMaxRecordingTime = this.maxRecordingTime;
31618
+ if (this.activeMaxRecordingTime && this.activeMaxRecordingTime > 0) {
31619
+ this.maxTimeRemaining = this.activeMaxRecordingTime;
29156
31620
  this.startMaxTimeTimer();
29157
31621
  }
29158
31622
  }
@@ -29186,7 +31650,9 @@ class RecordingManager {
29186
31650
  this.callbacks.onStopAudioTracking();
29187
31651
  logger.debug("[RecordingManager] Stopping recording in stream manager");
29188
31652
  const stopResult = await this.streamManager.stopRecording();
29189
- const finalResult = await this.resolveFinalRecordingBlob(stopResult.blob, stopResult.captureFrameRate, stopResult.expectedDurationSeconds);
31653
+ const audioRecording = await this.resolveAudioRecording(stopResult.blob, stopResult.audio);
31654
+ this.lastAudioRecording = audioRecording;
31655
+ const finalResult = audioRecording === null ? await this.resolveFinalRecordingBlob(stopResult.blob, stopResult.captureFrameRate, stopResult.expectedDurationSeconds) : { blob: audioRecording.blob };
29190
31656
  const finalBlob = finalResult.blob;
29191
31657
  logger.info("[RecordingManager] Recording stopped, blob size:", finalBlob.size);
29192
31658
  this.recordingState = RECORDING_STATE_IDLE;
@@ -29212,6 +31678,15 @@ class RecordingManager {
29212
31678
  if (finalResult.diagnostics !== undefined) {
29213
31679
  Object.assign(telemetryProperties, finalResult.diagnostics);
29214
31680
  }
31681
+ if (audioRecording !== null) {
31682
+ telemetryProperties.mediaType = "audio";
31683
+ telemetryProperties.audioFormat = audioRecording.format;
31684
+ return {
31685
+ blob: finalBlob,
31686
+ telemetryProperties,
31687
+ audio: audioRecording
31688
+ };
31689
+ }
29215
31690
  return {
29216
31691
  blob: finalBlob,
29217
31692
  telemetryProperties
@@ -29241,9 +31716,9 @@ class RecordingManager {
29241
31716
  this.clearTimer(this.recordingIntervalId, clearInterval);
29242
31717
  this.recordingIntervalId = null;
29243
31718
  this.pauseStartTime = Date.now();
29244
- if (this.maxTimeTimer !== null && this.recordingStartTime !== null && this.maxRecordingTime !== null) {
31719
+ if (this.maxTimeTimer !== null && this.recordingStartTime !== null && this.activeMaxRecordingTime !== null) {
29245
31720
  const elapsed = Date.now() - this.recordingStartTime - this.totalPausedTime;
29246
- this.maxTimeRemaining = Math.max(0, this.maxRecordingTime - elapsed);
31721
+ this.maxTimeRemaining = Math.max(0, this.activeMaxRecordingTime - elapsed);
29247
31722
  this.clearTimer(this.maxTimeTimer, clearTimeout);
29248
31723
  this.maxTimeTimer = null;
29249
31724
  }
@@ -29292,6 +31767,23 @@ class RecordingManager {
29292
31767
  this.recordingStartTime = null;
29293
31768
  this.maxTimeRemaining = null;
29294
31769
  }
31770
+ async resolveAudioRecording(blob, encodedAudio) {
31771
+ if (this.activeRecordingConfig?.mediaType !== "audio") {
31772
+ return null;
31773
+ }
31774
+ if (encodedAudio) {
31775
+ return await createAudioRecording(encodedAudio);
31776
+ }
31777
+ const audioConfig = requireEnabledAudioRecordingConfig(this.activeRecordingConfig.audioRecording);
31778
+ this.callbacks.onTranscodingProgress?.(0);
31779
+ const recording = await this.dependencies.convertRecordedAudio(blob, audioConfig, {
31780
+ onProgress: (progress) => {
31781
+ this.callbacks.onTranscodingProgress?.(progress);
31782
+ }
31783
+ });
31784
+ this.callbacks.onTranscodingProgress?.(1);
31785
+ return recording;
31786
+ }
29295
31787
  async resolveFinalRecordingBlob(blob, captureFrameRate, expectedDurationSeconds) {
29296
31788
  if (this.activeRouteDecision.route !== "safe-capture-post-recording-transcode") {
29297
31789
  return { blob };
@@ -29573,7 +32065,7 @@ class RecorderController {
29573
32065
  this.uploadMetadataManager = new UploadMetadataManager;
29574
32066
  const recordingCallbacks = createRecordingCallbacks(callbacks, {
29575
32067
  stopAudioTracking: () => this.audioLevelAnalyzer.stopTracking(),
29576
- getConfig: () => Promise.resolve(this.configManager.getConfigForRecording()),
32068
+ getConfig: () => Promise.resolve().then(() => this.resolveRecordingConfig()),
29577
32069
  getRouteDecision: async () => await this.resolveRecordingRouteDecision("live-recorder"),
29578
32070
  onAudioWarning: callbacks.recording?.onAudioWarning ? (warning) => {
29579
32071
  this.sendAudioWarningTelemetry(warning);
@@ -29680,6 +32172,13 @@ class RecorderController {
29680
32172
  });
29681
32173
  }
29682
32174
  async startStream(requestedSourceType = "camera") {
32175
+ if (requestedSourceType === "audio") {
32176
+ await this.startAudioStream();
32177
+ return;
32178
+ }
32179
+ if (this.getCurrentSourceType() === "audio") {
32180
+ this.streamManager.stopStream();
32181
+ }
29683
32182
  const shouldStartScreenOnly = requestedSourceType === "screen" && this.sourceSwitchManager.shouldStartScreenWithoutCamera() && !this.streamManager.isActive();
29684
32183
  let sourceType = this.getCurrentSourceType();
29685
32184
  if (shouldStartScreenOnly) {
@@ -29708,6 +32207,72 @@ class RecorderController {
29708
32207
  }
29709
32208
  });
29710
32209
  }
32210
+ async startAudioStream() {
32211
+ await this.telemetryManager.executeAction({
32212
+ succeededEvent: "preview.start.succeeded",
32213
+ failedEvent: "preview.start.failed",
32214
+ action: async () => {
32215
+ await this.ensureConfigReady();
32216
+ const config = await this.configManager.getConfig();
32217
+ const audioConfig = requireEnabledAudioRecordingConfig(config.audioRecording);
32218
+ const isAudioStreamActive = this.getCurrentSourceType() === "audio" && this.streamManager.isActive();
32219
+ if (isAudioStreamActive) {
32220
+ return;
32221
+ }
32222
+ if (this.streamManager.isActive()) {
32223
+ this.streamManager.stopStream();
32224
+ }
32225
+ await this.sourceSwitchManager.startAudioOnly(buildAudioRecordingConstraints(audioConfig, this.deviceManager.getSelectedMicDeviceId()));
32226
+ },
32227
+ properties: {
32228
+ sourceType: "audio"
32229
+ }
32230
+ });
32231
+ }
32232
+ resolveRecordingConfig() {
32233
+ const config = this.configManager.getConfigForRecording();
32234
+ if (this.getCurrentSourceType() !== "audio") {
32235
+ return config;
32236
+ }
32237
+ return {
32238
+ ...config,
32239
+ mediaType: "audio",
32240
+ audioRecording: requireEnabledAudioRecordingConfig(config.audioRecording)
32241
+ };
32242
+ }
32243
+ getRecordingMediaType(blob) {
32244
+ if (this.recordingManager.getAudioRecording(blob)) {
32245
+ return "audio";
32246
+ }
32247
+ return "video";
32248
+ }
32249
+ async resolveRecordingUploadDetails(blob, title) {
32250
+ const audioRecording = this.recordingManager.getAudioRecording(blob);
32251
+ if (audioRecording) {
32252
+ return {
32253
+ filename: createRecordingFilename(title, audioRecording.extension),
32254
+ mimeType: audioRecording.mimeType,
32255
+ duration: audioRecording.duration,
32256
+ mediaType: "audio",
32257
+ sourceType: "audio",
32258
+ audio: {
32259
+ format: audioRecording.format,
32260
+ bitrate: audioRecording.bitrate,
32261
+ channels: audioRecording.channels,
32262
+ sampleRate: audioRecording.sampleRate,
32263
+ peaks: audioRecording.peaks
32264
+ }
32265
+ };
32266
+ }
32267
+ const outputFormat = this.configManager.getConfigForRecording().format;
32268
+ return {
32269
+ filename: createRecordingFilename(title, getFileExtensionForOutputFormat(outputFormat)),
32270
+ mimeType: getMimeTypeForOutputFormat(outputFormat),
32271
+ duration: await extractVideoDuration(blob),
32272
+ mediaType: "video",
32273
+ sourceType: this.getCurrentSourceType()
32274
+ };
32275
+ }
29711
32276
  async stopStream() {
29712
32277
  await this.streamManager.stopStream();
29713
32278
  }
@@ -29719,7 +32284,11 @@ class RecorderController {
29719
32284
  return this.streamManager.switchVideoDevice(deviceId);
29720
32285
  }
29721
32286
  switchAudioDevice(deviceId) {
29722
- return this.streamManager.switchAudioDevice(deviceId);
32287
+ if (this.getCurrentSourceType() !== "audio") {
32288
+ return this.streamManager.switchAudioDevice(deviceId);
32289
+ }
32290
+ const audioConfig = requireEnabledAudioRecordingConfig(this.configManager.getConfigForRecording().audioRecording);
32291
+ return this.streamManager.switchAudioDevice(deviceId, buildAudioRecordingConstraints(audioConfig, null));
29723
32292
  }
29724
32293
  async startRecording(options = {}) {
29725
32294
  const sourceType = this.getCurrentSourceType();
@@ -29882,18 +32451,21 @@ class RecorderController {
29882
32451
  }
29883
32452
  async prepareLocalRecording(blob) {
29884
32453
  const title = this.recordingTitle;
29885
- const outputFormat = this.configManager.getConfigForRecording().format;
29886
- const sourceType = this.getCurrentSourceType();
29887
- const duration = await extractVideoDuration(blob);
32454
+ const details = await this.resolveRecordingUploadDetails(blob, title);
29888
32455
  this.clearRecordingTitle(title);
29889
- return {
32456
+ const localRecording = {
29890
32457
  blob,
29891
32458
  title,
29892
- filename: createRecordingFilename(title, getFileExtensionForOutputFormat(outputFormat)),
29893
- mimeType: getMimeTypeForOutputFormat(outputFormat),
29894
- duration,
29895
- sourceType
32459
+ filename: details.filename,
32460
+ mimeType: details.mimeType,
32461
+ duration: details.duration,
32462
+ sourceType: details.sourceType,
32463
+ mediaType: details.mediaType
29896
32464
  };
32465
+ if (details.audio) {
32466
+ localRecording.audio = details.audio;
32467
+ }
32468
+ return localRecording;
29897
32469
  }
29898
32470
  uploadVideo(blob, apiKey, backendUrl, metadata, getAccessToken) {
29899
32471
  this.uploadCallbacks.onClearStatus();
@@ -29909,11 +32481,8 @@ class RecorderController {
29909
32481
  }
29910
32482
  async uploadVideoAfterStatusClear(blob, apiKey, backendUrl, metadata, getAccessToken) {
29911
32483
  const title = this.recordingTitle;
29912
- const duration = await extractVideoDuration(blob);
29913
- const outputFormat = this.configManager.getConfigForRecording().format;
29914
- const filename = createRecordingFilename(title, getFileExtensionForOutputFormat(outputFormat));
29915
- const mimeType = getMimeTypeForOutputFormat(outputFormat);
29916
- const sourceType = this.getCurrentSourceType();
32484
+ const details = await this.resolveRecordingUploadDetails(blob, title);
32485
+ const { filename, mimeType, duration, sourceType } = details;
29917
32486
  let userMetadata;
29918
32487
  if (Object.keys(metadata).length > 0) {
29919
32488
  userMetadata = metadata;
@@ -29933,7 +32502,9 @@ class RecorderController {
29933
32502
  mimeType,
29934
32503
  duration,
29935
32504
  sourceType,
29936
- userMetadata
32505
+ userMetadata,
32506
+ mediaType: details.mediaType,
32507
+ audio: details.audio
29937
32508
  });
29938
32509
  this.clearRecordingTitle(title);
29939
32510
  return;
@@ -29946,7 +32517,9 @@ class RecorderController {
29946
32517
  mimeType,
29947
32518
  duration,
29948
32519
  metadata: undefined,
29949
- userMetadata
32520
+ userMetadata,
32521
+ mediaType: details.mediaType,
32522
+ audio: details.audio
29950
32523
  });
29951
32524
  this.clearRecordingTitle(title);
29952
32525
  this.uploadMetadataManager.setMetadata(uploadId, {
@@ -29980,6 +32553,8 @@ class RecorderController {
29980
32553
  duration: options.duration,
29981
32554
  metadata: undefined,
29982
32555
  userMetadata: options.userMetadata,
32556
+ mediaType: options.mediaType,
32557
+ audio: options.audio,
29983
32558
  onProgress: this.uploadCallbacks.onProgress,
29984
32559
  onProgressDetails: (details) => {
29985
32560
  progressDetails = details;
@@ -29990,7 +32565,7 @@ class RecorderController {
29990
32565
  ...uploadOptions,
29991
32566
  ...options.apiKey !== null ? { apiKey: options.apiKey } : { getAccessToken: options.getAccessToken }
29992
32567
  });
29993
- this.uploadCallbacks.onSuccess(createRecordingUploadResult(result));
32568
+ this.uploadCallbacks.onSuccess(createRecordingUploadResult(result, options.mediaType));
29994
32569
  this.telemetryManager.sendEvent("upload.succeeded", {
29995
32570
  filename: options.filename,
29996
32571
  duration: options.duration,
@@ -30141,6 +32716,20 @@ class RecorderController {
30141
32716
  }
30142
32717
  }
30143
32718
  async resolveRecordingRouteDecision(routeSourceType) {
32719
+ if (this.getCurrentSourceType() === "audio") {
32720
+ const capabilities = detectAudioRecordingCapabilities();
32721
+ const audioDecision = decideAudioRecordingRoute(capabilities);
32722
+ this.telemetryManager.sendEvent("recording.route.selected", {
32723
+ routeDecision: {
32724
+ selectedRoute: audioDecision.route,
32725
+ reasonCodes: audioDecision.reasonCodes,
32726
+ sourceType: routeSourceType,
32727
+ mediaType: "audio",
32728
+ capabilitySummary: capabilities
32729
+ }
32730
+ });
32731
+ return audioDecision;
32732
+ }
30144
32733
  const config = this.configManager.getConfigForRecording();
30145
32734
  const codecCandidates = this.buildCodecProbeCandidates(config);
30146
32735
  const codecProbeResults = await this.resolveCodecProbeResults(codecCandidates);
@@ -30500,7 +33089,7 @@ class QuotaManager {
30500
33089
  }
30501
33090
  }
30502
33091
  // src/core/transcode/upload-transcoder.ts
30503
- import { ALL_FORMATS as ALL_FORMATS4, BlobSource as BlobSource5, Input as Input5 } from "mediabunny";
33092
+ import { ALL_FORMATS as ALL_FORMATS5, BlobSource as BlobSource6, Input as Input6 } from "mediabunny";
30504
33093
  var BITRATE_TOLERANCE = 1.25;
30505
33094
  var FRAME_RATE_TOLERANCE = 1.05;
30506
33095
  var DEFAULT_VIDEO_CODEC = "avc";
@@ -30547,9 +33136,9 @@ function canUploadWithoutTranscoding(fileType, source, config) {
30547
33136
  return source.bitrate <= config.bitrate * BITRATE_TOLERANCE;
30548
33137
  }
30549
33138
  async function readSourceVideo(file) {
30550
- const input = new Input5({
30551
- formats: ALL_FORMATS4,
30552
- source: new BlobSource5(file)
33139
+ const input = new Input6({
33140
+ formats: ALL_FORMATS5,
33141
+ source: new BlobSource6(file)
30553
33142
  });
30554
33143
  const videoTrack = await input.getPrimaryVideoTrack();
30555
33144
  if (!videoTrack) {
@@ -30873,7 +33462,8 @@ class VidtreoRecorder {
30873
33462
  }
30874
33463
  async startRecording(options = {}, sourceType = "camera") {
30875
33464
  await this.ensureInitialized();
30876
- if (!this.controller.isActive()) {
33465
+ const isAudioTransition = sourceType === "audio" || this.controller.getCurrentSourceType() === "audio";
33466
+ if (!this.controller.isActive() || isAudioTransition) {
30877
33467
  await this.startPreview(sourceType);
30878
33468
  }
30879
33469
  if (this.controller.getCurrentSourceType() !== sourceType) {
@@ -30925,37 +33515,71 @@ class VidtreoRecorder {
30925
33515
  async stopRecording() {
30926
33516
  await this.ensureInitialized();
30927
33517
  const blob = await this.controller.stopRecording();
33518
+ const title = this.controller.getRecordingTitle();
30928
33519
  if (this.config.autoUpload === false) {
30929
- this.controller.clearRecordingTitle(this.controller.getRecordingTitle());
30930
- return { recordingId: "", uploadUrl: "", blob };
33520
+ this.controller.clearRecordingTitle(title);
33521
+ return {
33522
+ recordingId: "",
33523
+ uploadUrl: "",
33524
+ blob,
33525
+ mediaType: this.controller.getRecordingMediaType(blob)
33526
+ };
30931
33527
  }
30932
- const outputFormat = (await this.controller.getConfig()).format;
30933
- const title = this.controller.getRecordingTitle();
30934
- const filename = createRecordingFilename(title, getFileExtensionForOutputFormat(outputFormat));
30935
- const mimeType = getMimeTypeForOutputFormat(outputFormat);
30936
- const duration = await extractVideoDuration(blob);
33528
+ const details = await this.controller.resolveRecordingUploadDetails(blob, title);
33529
+ const result = await this.uploadMedia(blob, details);
33530
+ this.controller.clearRecordingTitle(title);
33531
+ return result;
33532
+ }
33533
+ async getAudioRecordingConfig() {
33534
+ await this.ensureInitialized();
33535
+ const config = await this.controller.getConfig();
33536
+ if (!config.audioRecording) {
33537
+ return { ...DISABLED_AUDIO_RECORDING_CONFIG };
33538
+ }
33539
+ return config.audioRecording;
33540
+ }
33541
+ async uploadAudio(audio) {
33542
+ requireEnabledAudioRecordingConfig(await this.getAudioRecordingConfig());
33543
+ return await this.uploadMedia(audio.blob, {
33544
+ filename: audio.filename,
33545
+ mimeType: audio.mimeType,
33546
+ duration: audio.duration,
33547
+ mediaType: "audio",
33548
+ audio: {
33549
+ format: audio.format,
33550
+ bitrate: audio.bitrate,
33551
+ channels: audio.channels,
33552
+ sampleRate: audio.sampleRate,
33553
+ peaks: audio.peaks
33554
+ }
33555
+ });
33556
+ }
33557
+ async uploadMedia(blob, details) {
30937
33558
  const uploadService = this.controller.getUploadService();
30938
33559
  if (!uploadService) {
30939
33560
  throw new Error("Upload service not available");
30940
33561
  }
30941
- const uploadOptions = {
33562
+ const uploadResult = await uploadService.uploadVideo(blob, {
30942
33563
  backendUrl: this.config.apiUrl,
30943
- filename,
30944
- mimeType,
30945
- duration,
33564
+ filename: details.filename,
33565
+ mimeType: details.mimeType,
33566
+ duration: details.duration,
30946
33567
  userMetadata: this.config.userMetadata,
30947
- onProgress: this.config.onUploadProgress
30948
- };
30949
- const uploadResult = await uploadService.uploadVideo(blob, {
30950
- ...uploadOptions,
33568
+ onProgress: this.config.onUploadProgress,
33569
+ mediaType: details.mediaType,
33570
+ audio: details.audio,
30951
33571
  ...this.config.apiKey !== undefined ? { apiKey: this.config.apiKey } : { getAccessToken: this.config.getAccessToken }
30952
33572
  });
30953
- this.controller.clearRecordingTitle(title);
30954
- return {
33573
+ const result = {
30955
33574
  recordingId: uploadResult.id,
30956
33575
  uploadUrl: uploadResult.uploadUrl || "",
30957
- blob
33576
+ blob,
33577
+ mediaType: details.mediaType
30958
33578
  };
33579
+ if (details.mediaType === "audio") {
33580
+ result.audioId = uploadResult.id;
33581
+ }
33582
+ return result;
30959
33583
  }
30960
33584
  async getAvailableDevices() {
30961
33585
  if (this.config.enableDeviceChange === false) {
@@ -31022,6 +33646,7 @@ class VidtreoRecorder {
31022
33646
  }
31023
33647
  }
31024
33648
  export {
33649
+ AUDIO_OUTPUT_FORMATS,
31025
33650
  AudioLevelAnalyzer,
31026
33651
  CONFIG_ERROR_CREDENTIAL_CONFLICT,
31027
33652
  CREDENTIAL_CONFLICT_MESSAGE,
@@ -31033,8 +33658,14 @@ export {
31033
33658
  DEFAULT_RECORDING_OPTIONS,
31034
33659
  DEFAULT_STREAM_CONFIG,
31035
33660
  DEFAULT_TRANSCODE_CONFIG,
33661
+ DISABLED_AUDIO_RECORDING_CONFIG,
31036
33662
  DeviceManager,
33663
+ ERROR_AUDIO_CODEC_UNSUPPORTED,
33664
+ ERROR_AUDIO_NO_AUDIO_TRACK,
31037
33665
  ERROR_AUDIO_NO_TRACK_AT_START,
33666
+ ERROR_AUDIO_RECORDING_DISABLED,
33667
+ ERROR_AUDIO_SOURCE_SWITCH_UNSUPPORTED,
33668
+ ERROR_AUDIO_UNSUPPORTED_INPUT,
31038
33669
  ERROR_PICTURE_IN_PICTURE_UNSUPPORTED,
31039
33670
  ERROR_RECORDING_AUDIO_INVALID_SAMPLE_RATE,
31040
33671
  ERROR_RECORDING_AUDIO_NO_CHUNKS,
@@ -31098,6 +33729,7 @@ export {
31098
33729
  isPictureInPictureSupported,
31099
33730
  logger,
31100
33731
  mapPresetToConfig,
33732
+ parseAudioRecordingConfig,
31101
33733
  parseBrowserErrorLinkContent,
31102
33734
  recoverPendingUpload,
31103
33735
  requireActive,
@@ -31110,6 +33742,7 @@ export {
31110
33742
  resolveDeviceError,
31111
33743
  resolvePictureInPictureConfig,
31112
33744
  sanitizeRecordingTitle,
33745
+ transcodeAudioFile,
31113
33746
  transcodeVideo,
31114
33747
  transcodeVideoForNativeCamera,
31115
33748
  transcodeVideoForUpload,