@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.
- package/dist/index.d.ts +252 -13
- package/dist/index.js +2987 -354
- 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/
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
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
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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
|
-
|
|
448
|
-
|
|
892
|
+
let resolutionMap = RESOLUTION_MAP;
|
|
893
|
+
if (isMobileDeviceDetected) {
|
|
894
|
+
resolutionMap = MOBILE_RESOLUTION_MAP;
|
|
449
895
|
}
|
|
450
|
-
|
|
451
|
-
|
|
896
|
+
const { width, height } = resolutionMap[preset];
|
|
897
|
+
let format = outputFormat;
|
|
898
|
+
if (format === undefined) {
|
|
899
|
+
format = "mp4";
|
|
452
900
|
}
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
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
|
|
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
|
|
971
|
-
|
|
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
|
|
999
|
-
|
|
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
|
-
|
|
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 (
|
|
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 =
|
|
1200
|
-
const hasPromptState =
|
|
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
|
|
1280
|
-
const input = new
|
|
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
|
|
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
|
|
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
|
|
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
|
|
2701
|
+
return new Mp4OutputFormat2;
|
|
2274
2702
|
case "webm":
|
|
2275
|
-
return new
|
|
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
|
|
2366
|
-
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
|
|
2798
|
+
const output = new Output2({
|
|
2371
2799
|
format: outputFormat,
|
|
2372
|
-
target: new
|
|
2800
|
+
target: new BufferTarget2
|
|
2373
2801
|
});
|
|
2374
|
-
const conversion = await
|
|
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
|
|
2441
|
-
const mediabunnyInput = new
|
|
2442
|
-
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
|
|
2874
|
+
const output = new Output2({
|
|
2447
2875
|
format: outputFormat,
|
|
2448
|
-
target: new
|
|
2876
|
+
target: new BufferTarget2
|
|
2449
2877
|
});
|
|
2450
|
-
const conversion = await
|
|
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
|
-
|
|
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",
|
|
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/
|
|
6971
|
-
var
|
|
6972
|
-
var
|
|
6973
|
-
var
|
|
6974
|
-
var
|
|
6975
|
-
|
|
6976
|
-
|
|
6977
|
-
|
|
6978
|
-
|
|
6979
|
-
|
|
6980
|
-
|
|
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
|
-
|
|
6983
|
-
|
|
6984
|
-
|
|
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
|
-
|
|
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(() =>
|
|
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.
|
|
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.
|
|
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,
|
|
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.
|
|
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,
|
|
10330
|
-
const path = `${
|
|
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,
|
|
10478
|
-
const path = `${
|
|
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,
|
|
10485
|
-
const path = `${
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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/
|
|
10968
|
-
|
|
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.
|
|
11083
|
-
this.
|
|
11084
|
-
|
|
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
|
-
|
|
11371
|
-
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
28415
|
-
BlobSource as
|
|
28416
|
-
BufferTarget as
|
|
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
|
|
28421
|
-
Mp4OutputFormat as
|
|
28422
|
-
Output as
|
|
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
|
|
28613
|
-
const output = new
|
|
28614
|
-
format: new
|
|
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
|
|
28640
|
-
formats:
|
|
28641
|
-
source: new
|
|
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
|
|
28817
|
-
formats:
|
|
28818
|
-
source: new
|
|
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
|
-
|
|
29155
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
32456
|
+
const localRecording = {
|
|
29890
32457
|
blob,
|
|
29891
32458
|
title,
|
|
29892
|
-
filename:
|
|
29893
|
-
mimeType:
|
|
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
|
|
29913
|
-
const
|
|
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
|
|
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
|
|
30551
|
-
formats:
|
|
30552
|
-
source: new
|
|
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
|
-
|
|
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(
|
|
30930
|
-
return {
|
|
33520
|
+
this.controller.clearRecordingTitle(title);
|
|
33521
|
+
return {
|
|
33522
|
+
recordingId: "",
|
|
33523
|
+
uploadUrl: "",
|
|
33524
|
+
blob,
|
|
33525
|
+
mediaType: this.controller.getRecordingMediaType(blob)
|
|
33526
|
+
};
|
|
30931
33527
|
}
|
|
30932
|
-
const
|
|
30933
|
-
const
|
|
30934
|
-
|
|
30935
|
-
|
|
30936
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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,
|