@vidtreo/recorder 1.2.0 → 1.2.1
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 +1644 -1595
- package/dist/index.js +432 -69
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -300,7 +300,8 @@ function getDefaultConfigForFormat(format) {
|
|
|
300
300
|
};
|
|
301
301
|
}
|
|
302
302
|
// src/core/processor/codec-detector.ts
|
|
303
|
-
var
|
|
303
|
+
var VIDEO_CODEC_AV1 = "av1";
|
|
304
|
+
var VIDEO_CODEC_VP9 = "vp9";
|
|
304
305
|
var VIDEO_CODEC_AVC = "avc";
|
|
305
306
|
var AUDIO_CODEC_AAC = "aac";
|
|
306
307
|
var AUDIO_CODEC_OPUS = "opus";
|
|
@@ -347,6 +348,9 @@ function createVideoCodecSupportResult(supported) {
|
|
|
347
348
|
function createVideoCodecSupportError(error) {
|
|
348
349
|
return { kind: "error", error };
|
|
349
350
|
}
|
|
351
|
+
async function getVideoCodecSupport(canEncodeVideo, codec, checkOptions) {
|
|
352
|
+
return await canEncodeVideo(codec, checkOptions).then((supported) => createVideoCodecSupportResult(supported)).catch((error) => createVideoCodecSupportError(error));
|
|
353
|
+
}
|
|
350
354
|
function createAudioCodecLookupResult(codec) {
|
|
351
355
|
return { kind: "result", codec };
|
|
352
356
|
}
|
|
@@ -363,15 +367,41 @@ async function detectBestCodec(width, height, bitrate, dependencies) {
|
|
|
363
367
|
return VIDEO_CODEC_AVC;
|
|
364
368
|
}
|
|
365
369
|
const checkOptions = buildVideoCodecCheckOptions(width, height, bitrate);
|
|
366
|
-
const videoCodecSupportResult = await canEncodeVideo
|
|
370
|
+
const videoCodecSupportResult = await getVideoCodecSupport(canEncodeVideo, VIDEO_CODEC_AV1, checkOptions);
|
|
367
371
|
if (videoCodecSupportResult.kind === "error") {
|
|
368
372
|
return VIDEO_CODEC_AVC;
|
|
369
373
|
}
|
|
370
374
|
if (videoCodecSupportResult.supported) {
|
|
371
|
-
return
|
|
375
|
+
return VIDEO_CODEC_AV1;
|
|
372
376
|
}
|
|
373
377
|
return VIDEO_CODEC_AVC;
|
|
374
378
|
}
|
|
379
|
+
async function detectBestWebmCodec(width, height, bitrate, dependencies) {
|
|
380
|
+
const mediabunnyModuleResult = await resolveMediabunnyModule(dependencies).then((module) => createMediabunnyModuleResult(module)).catch((error) => createMediabunnyModuleError(error));
|
|
381
|
+
if (mediabunnyModuleResult.kind === "error") {
|
|
382
|
+
return VIDEO_CODEC_VP9;
|
|
383
|
+
}
|
|
384
|
+
const { canEncodeVideo } = mediabunnyModuleResult.module;
|
|
385
|
+
if (typeof canEncodeVideo !== "function") {
|
|
386
|
+
return VIDEO_CODEC_VP9;
|
|
387
|
+
}
|
|
388
|
+
const checkOptions = buildVideoCodecCheckOptions(width, height, bitrate);
|
|
389
|
+
const av1SupportResult = await getVideoCodecSupport(canEncodeVideo, VIDEO_CODEC_AV1, checkOptions);
|
|
390
|
+
if (av1SupportResult.kind === "error") {
|
|
391
|
+
return VIDEO_CODEC_VP9;
|
|
392
|
+
}
|
|
393
|
+
if (av1SupportResult.supported) {
|
|
394
|
+
return VIDEO_CODEC_AV1;
|
|
395
|
+
}
|
|
396
|
+
const vp9SupportResult = await getVideoCodecSupport(canEncodeVideo, VIDEO_CODEC_VP9, checkOptions);
|
|
397
|
+
if (vp9SupportResult.kind === "error") {
|
|
398
|
+
return VIDEO_CODEC_VP9;
|
|
399
|
+
}
|
|
400
|
+
if (vp9SupportResult.supported) {
|
|
401
|
+
return VIDEO_CODEC_VP9;
|
|
402
|
+
}
|
|
403
|
+
return VIDEO_CODEC_VP9;
|
|
404
|
+
}
|
|
375
405
|
async function detectBestAudioCodec(bitrate, dependencies) {
|
|
376
406
|
const mediabunnyModuleResult = await resolveMediabunnyModule(dependencies).then((module) => createMediabunnyModuleResult(module)).catch((error) => createMediabunnyModuleError(error));
|
|
377
407
|
if (mediabunnyModuleResult.kind === "error") {
|
|
@@ -735,6 +765,7 @@ import {
|
|
|
735
765
|
WebMOutputFormat
|
|
736
766
|
} from "mediabunny";
|
|
737
767
|
var ALLOW_ROTATION_METADATA = false;
|
|
768
|
+
var OUTPUT_FORMAT_WEBM = "webm";
|
|
738
769
|
function createSource(input) {
|
|
739
770
|
if (typeof input === "string") {
|
|
740
771
|
return new FilePathSource(input);
|
|
@@ -772,6 +803,12 @@ function getMimeTypeForFormat(format) {
|
|
|
772
803
|
throw new Error(`Unsupported output format: ${format}`);
|
|
773
804
|
}
|
|
774
805
|
}
|
|
806
|
+
async function resolvePreferredVideoCodec(config) {
|
|
807
|
+
if (config.format === OUTPUT_FORMAT_WEBM) {
|
|
808
|
+
return await detectBestWebmCodec(config.width, config.height, config.bitrate);
|
|
809
|
+
}
|
|
810
|
+
return await detectBestCodec(config.width, config.height, config.bitrate);
|
|
811
|
+
}
|
|
775
812
|
function createConversionOptions(config, optimizeForSpeed = false) {
|
|
776
813
|
const audioCodec = getAudioCodecForFormat(config.format, config.audioCodec);
|
|
777
814
|
const video = {
|
|
@@ -822,6 +859,7 @@ async function transcodeVideo(input, config = {}, onProgress) {
|
|
|
822
859
|
if (!finalConfig.audioCodec) {
|
|
823
860
|
finalConfig.audioCodec = await detectBestAudioCodec(finalConfig.audioBitrate);
|
|
824
861
|
}
|
|
862
|
+
finalConfig.codec = await resolvePreferredVideoCodec(finalConfig);
|
|
825
863
|
const source = createSource(input);
|
|
826
864
|
const mediabunnyInput = new Input2({
|
|
827
865
|
formats: ALL_FORMATS,
|
|
@@ -861,6 +899,7 @@ async function transcodeVideoForNativeCamera(file, config = {}, onProgress) {
|
|
|
861
899
|
if (!finalConfig.audioCodec) {
|
|
862
900
|
finalConfig.audioCodec = await detectBestAudioCodec(finalConfig.audioBitrate);
|
|
863
901
|
}
|
|
902
|
+
finalConfig.codec = await resolvePreferredVideoCodec(finalConfig);
|
|
864
903
|
const source = new BlobSource2(file);
|
|
865
904
|
const mediabunnyInput = new Input2({
|
|
866
905
|
formats: ALL_FORMATS,
|
|
@@ -1341,7 +1380,7 @@ function getEmptyProbeResult() {
|
|
|
1341
1380
|
}
|
|
1342
1381
|
// src/core/storage/video-storage.ts
|
|
1343
1382
|
var DB_NAME = "vidtreo-recorder";
|
|
1344
|
-
var DB_VERSION =
|
|
1383
|
+
var DB_VERSION = 2;
|
|
1345
1384
|
var STORE_NAME = "pending-uploads";
|
|
1346
1385
|
var STATUS_INDEX = "status";
|
|
1347
1386
|
var CREATED_AT_INDEX = "createdAt";
|
|
@@ -1350,28 +1389,55 @@ var MAX_RETRIES = 10;
|
|
|
1350
1389
|
var MILLISECONDS_PER_HOUR = 60 * 60 * 1000;
|
|
1351
1390
|
var ID_PREFIX = "upload-";
|
|
1352
1391
|
var ID_RANDOM_LENGTH = 9;
|
|
1392
|
+
var VERSION_ERROR_NAME = "VersionError";
|
|
1393
|
+
var ERROR_SCHEMA_MISSING_STORE = "Database schema is missing required object store: pending-uploads";
|
|
1394
|
+
var ERROR_SCHEMA_MISSING_STATUS_INDEX = "Database schema is missing required index: status";
|
|
1395
|
+
var ERROR_SCHEMA_MISSING_CREATED_AT_INDEX = "Database schema is missing required index: createdAt";
|
|
1353
1396
|
|
|
1354
1397
|
class VideoStorageService {
|
|
1355
1398
|
db = null;
|
|
1399
|
+
databaseFactory;
|
|
1400
|
+
constructor(databaseFactory) {
|
|
1401
|
+
if (databaseFactory) {
|
|
1402
|
+
this.databaseFactory = databaseFactory;
|
|
1403
|
+
return;
|
|
1404
|
+
}
|
|
1405
|
+
this.databaseFactory = indexedDB;
|
|
1406
|
+
}
|
|
1356
1407
|
init() {
|
|
1357
1408
|
if (this.db) {
|
|
1358
1409
|
return Promise.resolve();
|
|
1359
1410
|
}
|
|
1411
|
+
return this.openDatabase(DB_VERSION, true);
|
|
1412
|
+
}
|
|
1413
|
+
openDatabase(databaseVersion, canRetryWithoutVersion) {
|
|
1360
1414
|
return new Promise((resolve, reject) => {
|
|
1361
|
-
const request =
|
|
1415
|
+
const request = this.createOpenRequest(databaseVersion);
|
|
1362
1416
|
request.onerror = () => {
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1417
|
+
const requestError = request.error;
|
|
1418
|
+
if (canRetryWithoutVersion && requestError && requestError.name === VERSION_ERROR_NAME) {
|
|
1419
|
+
this.openDatabase(undefined, false).then(resolve).catch(reject);
|
|
1420
|
+
return;
|
|
1367
1421
|
}
|
|
1422
|
+
if (requestError) {
|
|
1423
|
+
reject(requestError);
|
|
1424
|
+
return;
|
|
1425
|
+
}
|
|
1426
|
+
reject(new Error("Failed to open database"));
|
|
1368
1427
|
};
|
|
1369
1428
|
request.onsuccess = () => {
|
|
1370
1429
|
if (!request.result) {
|
|
1371
1430
|
reject(new Error("Database result is null"));
|
|
1372
1431
|
return;
|
|
1373
1432
|
}
|
|
1374
|
-
|
|
1433
|
+
const database = request.result;
|
|
1434
|
+
const schemaValidationError = this.validateRequiredSchema(database);
|
|
1435
|
+
if (schemaValidationError) {
|
|
1436
|
+
database.close();
|
|
1437
|
+
reject(schemaValidationError);
|
|
1438
|
+
return;
|
|
1439
|
+
}
|
|
1440
|
+
this.db = database;
|
|
1375
1441
|
resolve();
|
|
1376
1442
|
};
|
|
1377
1443
|
request.onupgradeneeded = (event) => {
|
|
@@ -1380,16 +1446,42 @@ class VideoStorageService {
|
|
|
1380
1446
|
reject(new Error("Database upgrade result is null"));
|
|
1381
1447
|
return;
|
|
1382
1448
|
}
|
|
1383
|
-
|
|
1384
|
-
const store = db.createObjectStore(STORE_NAME, { keyPath: "id" });
|
|
1385
|
-
store.createIndex(STATUS_INDEX, STATUS_INDEX, { unique: false });
|
|
1386
|
-
store.createIndex(CREATED_AT_INDEX, CREATED_AT_INDEX, {
|
|
1387
|
-
unique: false
|
|
1388
|
-
});
|
|
1389
|
-
}
|
|
1449
|
+
this.initializeStoreSchema(db);
|
|
1390
1450
|
};
|
|
1391
1451
|
});
|
|
1392
1452
|
}
|
|
1453
|
+
createOpenRequest(databaseVersion) {
|
|
1454
|
+
if (databaseVersion === undefined) {
|
|
1455
|
+
return this.databaseFactory.open(DB_NAME);
|
|
1456
|
+
}
|
|
1457
|
+
return this.databaseFactory.open(DB_NAME, databaseVersion);
|
|
1458
|
+
}
|
|
1459
|
+
initializeStoreSchema(database) {
|
|
1460
|
+
if (database.objectStoreNames.contains(STORE_NAME)) {
|
|
1461
|
+
return;
|
|
1462
|
+
}
|
|
1463
|
+
const objectStore = database.createObjectStore(STORE_NAME, {
|
|
1464
|
+
keyPath: "id"
|
|
1465
|
+
});
|
|
1466
|
+
objectStore.createIndex(STATUS_INDEX, STATUS_INDEX, { unique: false });
|
|
1467
|
+
objectStore.createIndex(CREATED_AT_INDEX, CREATED_AT_INDEX, {
|
|
1468
|
+
unique: false
|
|
1469
|
+
});
|
|
1470
|
+
}
|
|
1471
|
+
validateRequiredSchema(database) {
|
|
1472
|
+
if (!database.objectStoreNames.contains(STORE_NAME)) {
|
|
1473
|
+
return new Error(ERROR_SCHEMA_MISSING_STORE);
|
|
1474
|
+
}
|
|
1475
|
+
const transaction = database.transaction([STORE_NAME], "readonly");
|
|
1476
|
+
const store = transaction.objectStore(STORE_NAME);
|
|
1477
|
+
if (!store.indexNames.contains(STATUS_INDEX)) {
|
|
1478
|
+
return new Error(ERROR_SCHEMA_MISSING_STATUS_INDEX);
|
|
1479
|
+
}
|
|
1480
|
+
if (!store.indexNames.contains(CREATED_AT_INDEX)) {
|
|
1481
|
+
return new Error(ERROR_SCHEMA_MISSING_CREATED_AT_INDEX);
|
|
1482
|
+
}
|
|
1483
|
+
return null;
|
|
1484
|
+
}
|
|
1393
1485
|
isInitialized() {
|
|
1394
1486
|
return this.db !== null;
|
|
1395
1487
|
}
|
|
@@ -3105,9 +3197,11 @@ class StreamRecordingState {
|
|
|
3105
3197
|
this.startRecordingTimer();
|
|
3106
3198
|
}
|
|
3107
3199
|
async stopRecording() {
|
|
3200
|
+
const recordingElapsedSeconds = (performance.now() - this.recordingStartTime - this.totalPausedTime) / MILLISECONDS_PER_SECOND;
|
|
3108
3201
|
logger.debug("[StreamRecordingState] stopRecording called", {
|
|
3109
3202
|
hasStreamProcessor: !!this.streamProcessor,
|
|
3110
|
-
isRecording: this.isRecording()
|
|
3203
|
+
isRecording: this.isRecording(),
|
|
3204
|
+
recordingElapsedSeconds
|
|
3111
3205
|
});
|
|
3112
3206
|
if (!(this.streamProcessor && this.isRecording())) {
|
|
3113
3207
|
throw new Error("Not currently recording");
|
|
@@ -3429,7 +3523,7 @@ class CameraStreamManager {
|
|
|
3429
3523
|
// package.json
|
|
3430
3524
|
var package_default = {
|
|
3431
3525
|
name: "@vidtreo/recorder",
|
|
3432
|
-
version: "1.2.
|
|
3526
|
+
version: "1.2.1",
|
|
3433
3527
|
type: "module",
|
|
3434
3528
|
description: "Vidtreo SDK for browser-based video recording and transcoding. Features include camera/screen recording, real-time MP4 transcoding, audio level analysis, mute/pause controls, source switching, device selection, and automatic backend uploads. Similar to Ziggeo and Addpipe, Vidtreo provides enterprise-grade video processing capabilities for web applications.",
|
|
3435
3529
|
main: "./dist/index.js",
|
|
@@ -4348,6 +4442,118 @@ function serializeBitrate(bitrate) {
|
|
|
4348
4442
|
return "high";
|
|
4349
4443
|
}
|
|
4350
4444
|
|
|
4445
|
+
// src/core/processor/mp4-container-guard.ts
|
|
4446
|
+
var MP4_BOX_HEADER_BYTES = 8;
|
|
4447
|
+
var MP4_BOX_TYPE_OFFSET_BYTES = 4;
|
|
4448
|
+
var MP4_EXTENDED_SIZE_MARKER = 1;
|
|
4449
|
+
var MP4_ZERO_SIZE_MARKER = 0;
|
|
4450
|
+
var MP4_EXTENDED_SIZE_BYTES = 8;
|
|
4451
|
+
var MP4_EXTENDED_BOX_HEADER_BYTES = MP4_BOX_HEADER_BYTES + MP4_EXTENDED_SIZE_BYTES;
|
|
4452
|
+
var TWO_POWER_32 = 4294967296;
|
|
4453
|
+
var MP4_FRAGMENT_BOX_TYPE_MOOF = "moof";
|
|
4454
|
+
var MP4_FRAGMENT_BOX_TYPE_MFRA = "mfra";
|
|
4455
|
+
var ERROR_RECORDING_INVALID_CONTAINER_LAYOUT = "recording.invalid-container-layout";
|
|
4456
|
+
function createInvalidMp4ContainerLayoutError(detectedBoxTypes) {
|
|
4457
|
+
const error = new Error(ERROR_RECORDING_INVALID_CONTAINER_LAYOUT);
|
|
4458
|
+
error.code = ERROR_RECORDING_INVALID_CONTAINER_LAYOUT;
|
|
4459
|
+
error.detectedBoxTypes = detectedBoxTypes;
|
|
4460
|
+
return error;
|
|
4461
|
+
}
|
|
4462
|
+
function toUint8Array(input) {
|
|
4463
|
+
if (input instanceof Uint8Array) {
|
|
4464
|
+
return input;
|
|
4465
|
+
}
|
|
4466
|
+
return new Uint8Array(input);
|
|
4467
|
+
}
|
|
4468
|
+
function readBoxType(view, offset) {
|
|
4469
|
+
const firstCharCode = view.getUint8(offset);
|
|
4470
|
+
const secondCharCode = view.getUint8(offset + 1);
|
|
4471
|
+
const thirdCharCode = view.getUint8(offset + 2);
|
|
4472
|
+
const fourthCharCode = view.getUint8(offset + 3);
|
|
4473
|
+
return String.fromCharCode(firstCharCode, secondCharCode, thirdCharCode, fourthCharCode);
|
|
4474
|
+
}
|
|
4475
|
+
function readLargeSize(view, offset) {
|
|
4476
|
+
const highBits = view.getUint32(offset, false);
|
|
4477
|
+
const lowBits = view.getUint32(offset + MP4_BOX_TYPE_OFFSET_BYTES, false);
|
|
4478
|
+
return highBits * TWO_POWER_32 + lowBits;
|
|
4479
|
+
}
|
|
4480
|
+
function getUniqueValues(values) {
|
|
4481
|
+
const uniqueValues = new Set;
|
|
4482
|
+
for (const value of values) {
|
|
4483
|
+
uniqueValues.add(value);
|
|
4484
|
+
}
|
|
4485
|
+
return [...uniqueValues];
|
|
4486
|
+
}
|
|
4487
|
+
function parseMp4TopLevelBoxes(input) {
|
|
4488
|
+
const bytes = toUint8Array(input);
|
|
4489
|
+
const totalBytes = bytes.byteLength;
|
|
4490
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
4491
|
+
const boxes = [];
|
|
4492
|
+
let currentOffset = 0;
|
|
4493
|
+
while (currentOffset < totalBytes) {
|
|
4494
|
+
const hasMinimumHeader = currentOffset + MP4_BOX_HEADER_BYTES <= totalBytes;
|
|
4495
|
+
if (!hasMinimumHeader) {
|
|
4496
|
+
const detectedBoxTypes = boxes.map((box) => box.type);
|
|
4497
|
+
throw createInvalidMp4ContainerLayoutError(detectedBoxTypes);
|
|
4498
|
+
}
|
|
4499
|
+
const declaredSize = view.getUint32(currentOffset, false);
|
|
4500
|
+
const typeOffset = currentOffset + MP4_BOX_TYPE_OFFSET_BYTES;
|
|
4501
|
+
const boxType = readBoxType(view, typeOffset);
|
|
4502
|
+
let boxSize = declaredSize;
|
|
4503
|
+
let headerSize = MP4_BOX_HEADER_BYTES;
|
|
4504
|
+
if (declaredSize === MP4_EXTENDED_SIZE_MARKER) {
|
|
4505
|
+
const largeSizeOffset = currentOffset + MP4_BOX_HEADER_BYTES;
|
|
4506
|
+
const hasExtendedHeader = largeSizeOffset + MP4_EXTENDED_SIZE_BYTES <= totalBytes;
|
|
4507
|
+
if (!hasExtendedHeader) {
|
|
4508
|
+
const detectedBoxTypes = boxes.map((box) => box.type);
|
|
4509
|
+
throw createInvalidMp4ContainerLayoutError(detectedBoxTypes);
|
|
4510
|
+
}
|
|
4511
|
+
boxSize = readLargeSize(view, largeSizeOffset);
|
|
4512
|
+
headerSize = MP4_EXTENDED_BOX_HEADER_BYTES;
|
|
4513
|
+
}
|
|
4514
|
+
if (declaredSize === MP4_ZERO_SIZE_MARKER) {
|
|
4515
|
+
boxSize = totalBytes - currentOffset;
|
|
4516
|
+
}
|
|
4517
|
+
const hasValidHeaderSize = boxSize >= headerSize;
|
|
4518
|
+
if (!hasValidHeaderSize) {
|
|
4519
|
+
const detectedBoxTypes = boxes.map((box) => box.type);
|
|
4520
|
+
throw createInvalidMp4ContainerLayoutError(detectedBoxTypes);
|
|
4521
|
+
}
|
|
4522
|
+
const nextOffset = currentOffset + boxSize;
|
|
4523
|
+
const hasValidBoxRange = nextOffset <= totalBytes;
|
|
4524
|
+
if (!hasValidBoxRange) {
|
|
4525
|
+
const detectedBoxTypes = boxes.map((box) => box.type);
|
|
4526
|
+
throw createInvalidMp4ContainerLayoutError(detectedBoxTypes);
|
|
4527
|
+
}
|
|
4528
|
+
boxes.push({
|
|
4529
|
+
type: boxType,
|
|
4530
|
+
size: boxSize,
|
|
4531
|
+
startOffset: currentOffset,
|
|
4532
|
+
endOffset: nextOffset
|
|
4533
|
+
});
|
|
4534
|
+
currentOffset = nextOffset;
|
|
4535
|
+
}
|
|
4536
|
+
return boxes;
|
|
4537
|
+
}
|
|
4538
|
+
function assertMp4ContainerIsNonFragmented(input) {
|
|
4539
|
+
const topLevelBoxes = parseMp4TopLevelBoxes(input);
|
|
4540
|
+
const detectedFragmentBoxes = topLevelBoxes.filter((box) => {
|
|
4541
|
+
if (box.type === MP4_FRAGMENT_BOX_TYPE_MOOF) {
|
|
4542
|
+
return true;
|
|
4543
|
+
}
|
|
4544
|
+
if (box.type === MP4_FRAGMENT_BOX_TYPE_MFRA) {
|
|
4545
|
+
return true;
|
|
4546
|
+
}
|
|
4547
|
+
return false;
|
|
4548
|
+
}).map((box) => box.type);
|
|
4549
|
+
const hasFragmentBoxes = detectedFragmentBoxes.length > 0;
|
|
4550
|
+
if (!hasFragmentBoxes) {
|
|
4551
|
+
return;
|
|
4552
|
+
}
|
|
4553
|
+
const uniqueDetectedBoxTypes = getUniqueValues(detectedFragmentBoxes);
|
|
4554
|
+
throw createInvalidMp4ContainerLayoutError(uniqueDetectedBoxTypes);
|
|
4555
|
+
}
|
|
4556
|
+
|
|
4351
4557
|
// src/core/utils/shared-object-url-store.ts
|
|
4352
4558
|
function createSharedObjectUrlStore(dependencies) {
|
|
4353
4559
|
let activeUrl = null;
|
|
@@ -13374,6 +13580,54 @@ class FrameCompositor {
|
|
|
13374
13580
|
}
|
|
13375
13581
|
}
|
|
13376
13582
|
|
|
13583
|
+
// src/core/processor/worker/stop-finalization.ts
|
|
13584
|
+
var STOP_PENDING_WRITES_TIMEOUT_MILLISECONDS = 500;
|
|
13585
|
+
var STOP_PENDING_WRITES_POLL_INTERVAL_MILLISECONDS = 10;
|
|
13586
|
+
var ERROR_STOP_PENDING_WRITES_TIMEOUT = "stop.pending-writes-timeout";
|
|
13587
|
+
function createDefaultNowMilliseconds() {
|
|
13588
|
+
return () => performance.now();
|
|
13589
|
+
}
|
|
13590
|
+
function createDefaultWaitMilliseconds() {
|
|
13591
|
+
return (milliseconds) => new Promise((resolve) => {
|
|
13592
|
+
globalThis.setTimeout(resolve, milliseconds);
|
|
13593
|
+
});
|
|
13594
|
+
}
|
|
13595
|
+
async function waitForPendingWritesToDrain(dependencies) {
|
|
13596
|
+
let getNowMilliseconds = dependencies.getNowMilliseconds;
|
|
13597
|
+
if (!getNowMilliseconds) {
|
|
13598
|
+
getNowMilliseconds = createDefaultNowMilliseconds();
|
|
13599
|
+
}
|
|
13600
|
+
let waitMilliseconds = dependencies.waitMilliseconds;
|
|
13601
|
+
if (!waitMilliseconds) {
|
|
13602
|
+
waitMilliseconds = createDefaultWaitMilliseconds();
|
|
13603
|
+
}
|
|
13604
|
+
let timeoutMilliseconds = dependencies.timeoutMilliseconds;
|
|
13605
|
+
if (timeoutMilliseconds === undefined) {
|
|
13606
|
+
timeoutMilliseconds = STOP_PENDING_WRITES_TIMEOUT_MILLISECONDS;
|
|
13607
|
+
}
|
|
13608
|
+
const startedAtMilliseconds = getNowMilliseconds();
|
|
13609
|
+
let pendingWriteCount = dependencies.getPendingWriteCount();
|
|
13610
|
+
while (pendingWriteCount > 0) {
|
|
13611
|
+
const elapsedMilliseconds = getNowMilliseconds() - startedAtMilliseconds;
|
|
13612
|
+
if (elapsedMilliseconds >= timeoutMilliseconds) {
|
|
13613
|
+
throw new Error(ERROR_STOP_PENDING_WRITES_TIMEOUT);
|
|
13614
|
+
}
|
|
13615
|
+
await waitMilliseconds(STOP_PENDING_WRITES_POLL_INTERVAL_MILLISECONDS);
|
|
13616
|
+
pendingWriteCount = dependencies.getPendingWriteCount();
|
|
13617
|
+
}
|
|
13618
|
+
}
|
|
13619
|
+
|
|
13620
|
+
// src/core/processor/worker/stop-transition.ts
|
|
13621
|
+
async function runStopTransition(dependencies) {
|
|
13622
|
+
await dependencies.finalizeStopSequence().then(() => dependencies.completeStop()).catch((error) => {
|
|
13623
|
+
return dependencies.recoverStopFailure().then(() => {
|
|
13624
|
+
throw error;
|
|
13625
|
+
});
|
|
13626
|
+
}).finally(() => {
|
|
13627
|
+
dependencies.clearStoppingFlag();
|
|
13628
|
+
});
|
|
13629
|
+
}
|
|
13630
|
+
|
|
13377
13631
|
// src/core/processor/worker/timestamp-manager.ts
|
|
13378
13632
|
var DEFAULT_FRAME_RATE = 30;
|
|
13379
13633
|
var DEFAULT_KEY_FRAME_INTERVAL_SECONDS = 5;
|
|
@@ -13584,7 +13838,6 @@ function clampValue(value, min, max) {
|
|
|
13584
13838
|
var WORKER_MESSAGE_TYPE_PROBE = "probe";
|
|
13585
13839
|
var WORKER_MESSAGE_TYPE_AUDIO_CHUNK = "audioChunk";
|
|
13586
13840
|
var WORKER_RESPONSE_TYPE_PROBE_RESULT = "probeResult";
|
|
13587
|
-
var WORKER_RESPONSE_TYPE_DEBUG_LOG = "debugLog";
|
|
13588
13841
|
var WORKER_AUDIO_SAMPLE_FORMAT_F32_PLANAR = "f32-planar";
|
|
13589
13842
|
|
|
13590
13843
|
// src/core/processor/worker/visibility-tracker.ts
|
|
@@ -13707,6 +13960,8 @@ var ERROR_AUDIO_CHANNELS_INVALID = "Audio channels must be greater than zero";
|
|
|
13707
13960
|
var ERROR_AUDIO_FRAMES_INVALID = "Audio frames must be greater than zero";
|
|
13708
13961
|
var STEREO_CHANNEL_COUNT = 2;
|
|
13709
13962
|
var AUDIO_SAMPLE_AVERAGE_SCALE = 0.5;
|
|
13963
|
+
var STOP_PENDING_WRITES_TIMEOUT_MILLISECONDS2 = 500;
|
|
13964
|
+
var MP4_FAST_START_DISABLED = false;
|
|
13710
13965
|
|
|
13711
13966
|
class RecorderWorker {
|
|
13712
13967
|
output = null;
|
|
@@ -13729,6 +13984,7 @@ class RecorderWorker {
|
|
|
13729
13984
|
totalSize = 0;
|
|
13730
13985
|
expectedAudioChannels = null;
|
|
13731
13986
|
expectedAudioSampleRate = null;
|
|
13987
|
+
pendingWriteCount = 0;
|
|
13732
13988
|
constructor() {
|
|
13733
13989
|
this.bufferTracker = new BufferTracker({
|
|
13734
13990
|
getBufferSize: () => this.totalSize,
|
|
@@ -13759,14 +14015,6 @@ class RecorderWorker {
|
|
|
13759
14015
|
},
|
|
13760
14016
|
getNowMilliseconds: () => performance.now()
|
|
13761
14017
|
});
|
|
13762
|
-
const sendDebugLog = (message, payload) => {
|
|
13763
|
-
const response = {
|
|
13764
|
-
type: WORKER_RESPONSE_TYPE_DEBUG_LOG,
|
|
13765
|
-
message,
|
|
13766
|
-
payload
|
|
13767
|
-
};
|
|
13768
|
-
self.postMessage(response);
|
|
13769
|
-
};
|
|
13770
14018
|
this.frameCompositor = new FrameCompositor({
|
|
13771
14019
|
logger: {
|
|
13772
14020
|
debug: (message, data) => logger.debug(message, data),
|
|
@@ -13775,7 +14023,9 @@ class RecorderWorker {
|
|
|
13775
14023
|
},
|
|
13776
14024
|
fetchResource: (input, init) => fetch(input, init),
|
|
13777
14025
|
createImageBitmap: (image) => createImageBitmap(image),
|
|
13778
|
-
sendDebugLog
|
|
14026
|
+
sendDebugLog: (_message, _payload) => {
|
|
14027
|
+
return;
|
|
14028
|
+
}
|
|
13779
14029
|
});
|
|
13780
14030
|
self.addEventListener("message", this.handleMessage);
|
|
13781
14031
|
}
|
|
@@ -13909,6 +14159,7 @@ class RecorderWorker {
|
|
|
13909
14159
|
this.audioState.reset();
|
|
13910
14160
|
this.expectedAudioChannels = null;
|
|
13911
14161
|
this.expectedAudioSampleRate = null;
|
|
14162
|
+
this.pendingWriteCount = 0;
|
|
13912
14163
|
this.videoProcessingActive = false;
|
|
13913
14164
|
this.frameCompositor.reset();
|
|
13914
14165
|
this.recordingStartTime = 0;
|
|
@@ -13939,13 +14190,11 @@ class RecorderWorker {
|
|
|
13939
14190
|
}
|
|
13940
14191
|
createOutput() {
|
|
13941
14192
|
const writable = new WritableStream({
|
|
13942
|
-
write: (chunk) =>
|
|
13943
|
-
this.sendChunk(chunk.data, chunk.position);
|
|
13944
|
-
}
|
|
14193
|
+
write: (chunk) => this.handleOutputChunkWrite(chunk)
|
|
13945
14194
|
});
|
|
13946
14195
|
this.output = new Output({
|
|
13947
14196
|
format: new Mp4OutputFormat({
|
|
13948
|
-
fastStart:
|
|
14197
|
+
fastStart: MP4_FAST_START_DISABLED
|
|
13949
14198
|
}),
|
|
13950
14199
|
target: new StreamTarget(writable, {
|
|
13951
14200
|
chunked: true,
|
|
@@ -13953,6 +14202,24 @@ class RecorderWorker {
|
|
|
13953
14202
|
})
|
|
13954
14203
|
});
|
|
13955
14204
|
}
|
|
14205
|
+
decrementPendingWriteCount() {
|
|
14206
|
+
this.pendingWriteCount -= 1;
|
|
14207
|
+
if (this.pendingWriteCount < 0) {
|
|
14208
|
+
this.pendingWriteCount = 0;
|
|
14209
|
+
}
|
|
14210
|
+
}
|
|
14211
|
+
handleOutputChunkWrite(chunk) {
|
|
14212
|
+
this.pendingWriteCount += 1;
|
|
14213
|
+
const writeOperation = Promise.resolve().then(() => {
|
|
14214
|
+
this.sendChunk(chunk.data, chunk.position);
|
|
14215
|
+
});
|
|
14216
|
+
return writeOperation.then(() => {
|
|
14217
|
+
this.decrementPendingWriteCount();
|
|
14218
|
+
}, (error) => {
|
|
14219
|
+
this.decrementPendingWriteCount();
|
|
14220
|
+
throw error;
|
|
14221
|
+
});
|
|
14222
|
+
}
|
|
13956
14223
|
createVideoSource(config) {
|
|
13957
14224
|
const fps = this.timestampManager.getFrameRate();
|
|
13958
14225
|
const keyFrameIntervalSeconds = config.keyFrameInterval;
|
|
@@ -14442,19 +14709,42 @@ class RecorderWorker {
|
|
|
14442
14709
|
}
|
|
14443
14710
|
this.sendStateChange("recording");
|
|
14444
14711
|
}
|
|
14445
|
-
|
|
14712
|
+
handleStop() {
|
|
14446
14713
|
if (this.isStopping) {
|
|
14447
14714
|
logger.debug("[RecorderWorker] handleStop ignored (stopping/finalized)");
|
|
14448
|
-
return;
|
|
14715
|
+
return Promise.resolve();
|
|
14449
14716
|
}
|
|
14450
14717
|
if (this.isFinalized) {
|
|
14451
14718
|
logger.debug("[RecorderWorker] handleStop ignored (stopping/finalized)");
|
|
14452
|
-
return;
|
|
14719
|
+
return Promise.resolve();
|
|
14453
14720
|
}
|
|
14454
14721
|
this.isStopping = true;
|
|
14455
14722
|
this.isFinalized = true;
|
|
14456
14723
|
this.videoProcessingActive = false;
|
|
14457
14724
|
this.audioState.setProcessingActive(false);
|
|
14725
|
+
return runStopTransition({
|
|
14726
|
+
finalizeStopSequence: () => this.finalizeStopSequence(),
|
|
14727
|
+
completeStop: () => this.completeStop(),
|
|
14728
|
+
recoverStopFailure: () => {
|
|
14729
|
+
if (this.isFinalized) {
|
|
14730
|
+
this.resetStopStateAfterFailure();
|
|
14731
|
+
}
|
|
14732
|
+
return this.cleanup().catch((cleanupError) => {
|
|
14733
|
+
logger.error("[RecorderWorker] Stop failure cleanup failed", {
|
|
14734
|
+
error: extractErrorMessage(cleanupError)
|
|
14735
|
+
});
|
|
14736
|
+
});
|
|
14737
|
+
},
|
|
14738
|
+
clearStoppingFlag: () => {
|
|
14739
|
+
this.isStopping = false;
|
|
14740
|
+
}
|
|
14741
|
+
});
|
|
14742
|
+
}
|
|
14743
|
+
async completeStop() {
|
|
14744
|
+
await this.cleanup();
|
|
14745
|
+
this.sendStateChange("stopped");
|
|
14746
|
+
}
|
|
14747
|
+
async finalizeStopSequence() {
|
|
14458
14748
|
if (this.videoProcessor) {
|
|
14459
14749
|
await this.videoProcessor.cancel();
|
|
14460
14750
|
this.videoProcessor = null;
|
|
@@ -14464,13 +14754,17 @@ class RecorderWorker {
|
|
|
14464
14754
|
this.audioProcessor = null;
|
|
14465
14755
|
}
|
|
14466
14756
|
if (this.output) {
|
|
14467
|
-
await this.output.finalize()
|
|
14468
|
-
logger.warn("[RecorderWorker] finalize failed (ignored, already finalized?)", error);
|
|
14469
|
-
});
|
|
14757
|
+
await this.output.finalize();
|
|
14470
14758
|
}
|
|
14471
|
-
await
|
|
14472
|
-
|
|
14473
|
-
|
|
14759
|
+
await waitForPendingWritesToDrain({
|
|
14760
|
+
getPendingWriteCount: () => this.pendingWriteCount,
|
|
14761
|
+
timeoutMilliseconds: STOP_PENDING_WRITES_TIMEOUT_MILLISECONDS2
|
|
14762
|
+
});
|
|
14763
|
+
}
|
|
14764
|
+
resetStopStateAfterFailure() {
|
|
14765
|
+
this.isFinalized = false;
|
|
14766
|
+
this.videoProcessingActive = false;
|
|
14767
|
+
this.audioState.setProcessingActive(false);
|
|
14474
14768
|
}
|
|
14475
14769
|
handleToggleMute() {
|
|
14476
14770
|
this.audioState.toggleMuted();
|
|
@@ -14586,6 +14880,7 @@ class RecorderWorker {
|
|
|
14586
14880
|
this.isScreenCapture = false;
|
|
14587
14881
|
this.expectedAudioChannels = null;
|
|
14588
14882
|
this.expectedAudioSampleRate = null;
|
|
14883
|
+
this.pendingWriteCount = 0;
|
|
14589
14884
|
this.visibilityTracker.reset(this.recordingStartTime, this.isScreenCapture);
|
|
14590
14885
|
}
|
|
14591
14886
|
setExpectedAudioFormat(sampleRate, numberOfChannels) {
|
|
@@ -14708,6 +15003,7 @@ var WORKER_PROBE_TIMEOUT_MILLISECONDS = 2000;
|
|
|
14708
15003
|
var FINALIZE_TIMEOUT_MILLISECONDS = 30000;
|
|
14709
15004
|
var MILLISECONDS_PER_SECOND2 = 1000;
|
|
14710
15005
|
var DEFAULT_RECORDING_FORMAT = "mp4";
|
|
15006
|
+
var OUTPUT_FORMAT_WEBM2 = "webm";
|
|
14711
15007
|
|
|
14712
15008
|
class WorkerProcessor {
|
|
14713
15009
|
worker = null;
|
|
@@ -14876,7 +15172,7 @@ class WorkerProcessor {
|
|
|
14876
15172
|
this.stopAudioWorklet();
|
|
14877
15173
|
const format = this.resolveRecordingFormat(config);
|
|
14878
15174
|
const audioCodec = await this.resolveAudioCodec(config);
|
|
14879
|
-
const codec = await this.resolveVideoCodec(config);
|
|
15175
|
+
const codec = await this.resolveVideoCodec(config, format);
|
|
14880
15176
|
const isScreenCapture = isScreenCaptureStream(stream);
|
|
14881
15177
|
logger.debug("[WorkerProcessor] Starting processing", {
|
|
14882
15178
|
isScreenCapture,
|
|
@@ -14967,12 +15263,11 @@ class WorkerProcessor {
|
|
|
14967
15263
|
}
|
|
14968
15264
|
return audioCodec;
|
|
14969
15265
|
}
|
|
14970
|
-
async resolveVideoCodec(config) {
|
|
14971
|
-
|
|
14972
|
-
|
|
14973
|
-
codec = await detectBestCodec(config.width, config.height, config.bitrate);
|
|
15266
|
+
async resolveVideoCodec(config, format) {
|
|
15267
|
+
if (format === OUTPUT_FORMAT_WEBM2) {
|
|
15268
|
+
return await detectBestWebmCodec(config.width, config.height, config.bitrate);
|
|
14974
15269
|
}
|
|
14975
|
-
return
|
|
15270
|
+
return await detectBestCodec(config.width, config.height, config.bitrate);
|
|
14976
15271
|
}
|
|
14977
15272
|
buildWorkerTranscodeConfig(config, audioCodec, codec, format) {
|
|
14978
15273
|
return {
|
|
@@ -14990,6 +15285,7 @@ class WorkerProcessor {
|
|
|
14990
15285
|
}
|
|
14991
15286
|
async prepareAudioPipeline(audioTrack, workerProbeResult) {
|
|
14992
15287
|
if (!audioTrack) {
|
|
15288
|
+
logger.debug("[WorkerProcessor] Audio pipeline disabled (no track)");
|
|
14993
15289
|
return {
|
|
14994
15290
|
audioConfig: null,
|
|
14995
15291
|
audioStream: null,
|
|
@@ -15000,6 +15296,10 @@ class WorkerProcessor {
|
|
|
15000
15296
|
if (canUseMainThreadAudioPipeline) {
|
|
15001
15297
|
const audioStream = this.createAudioStreamFromTrack(audioTrack);
|
|
15002
15298
|
if (audioStream) {
|
|
15299
|
+
logger.debug("[WorkerProcessor] Audio pipeline selected", {
|
|
15300
|
+
path: "main-thread-audio-stream",
|
|
15301
|
+
hasAudioDataInWorker: workerProbeResult.hasAudioData
|
|
15302
|
+
});
|
|
15003
15303
|
return {
|
|
15004
15304
|
audioConfig: null,
|
|
15005
15305
|
audioStream,
|
|
@@ -15009,6 +15309,11 @@ class WorkerProcessor {
|
|
|
15009
15309
|
}
|
|
15010
15310
|
const audioConfig = await this.prepareAudioConfig(audioTrack);
|
|
15011
15311
|
if (audioConfig) {
|
|
15312
|
+
logger.debug("[WorkerProcessor] Audio pipeline selected", {
|
|
15313
|
+
path: "audio-worklet-chunks",
|
|
15314
|
+
sampleRate: audioConfig.sampleRate,
|
|
15315
|
+
numberOfChannels: audioConfig.numberOfChannels
|
|
15316
|
+
});
|
|
15012
15317
|
return {
|
|
15013
15318
|
audioConfig,
|
|
15014
15319
|
audioStream: null,
|
|
@@ -15143,42 +15448,88 @@ class WorkerProcessor {
|
|
|
15143
15448
|
if (!this.isWorkerActive()) {
|
|
15144
15449
|
throw new Error("Processing not active");
|
|
15145
15450
|
}
|
|
15451
|
+
const finalizeStartedAtMilliseconds = performance.now();
|
|
15146
15452
|
return new Promise((resolve, reject) => {
|
|
15147
|
-
const
|
|
15148
|
-
|
|
15149
|
-
|
|
15150
|
-
|
|
15151
|
-
|
|
15453
|
+
const worker = this.worker;
|
|
15454
|
+
if (!worker) {
|
|
15455
|
+
reject(new Error("Worker not initialized"));
|
|
15456
|
+
return;
|
|
15457
|
+
}
|
|
15458
|
+
let timeoutId = null;
|
|
15459
|
+
const clearFinalizeTimeout = () => {
|
|
15460
|
+
if (timeoutId === null) {
|
|
15152
15461
|
return;
|
|
15153
15462
|
}
|
|
15154
|
-
|
|
15463
|
+
clearTimeout(timeoutId);
|
|
15464
|
+
timeoutId = null;
|
|
15465
|
+
};
|
|
15466
|
+
const removeWorkerListener = () => {
|
|
15467
|
+
worker.removeEventListener("message", messageHandler);
|
|
15468
|
+
};
|
|
15469
|
+
let hasSettled = false;
|
|
15470
|
+
const settleOnce = () => {
|
|
15471
|
+
if (hasSettled) {
|
|
15472
|
+
return false;
|
|
15473
|
+
}
|
|
15474
|
+
hasSettled = true;
|
|
15475
|
+
clearFinalizeTimeout();
|
|
15476
|
+
removeWorkerListener();
|
|
15477
|
+
return true;
|
|
15155
15478
|
};
|
|
15156
15479
|
const messageHandler = (event) => {
|
|
15480
|
+
if (hasSettled) {
|
|
15481
|
+
return;
|
|
15482
|
+
}
|
|
15157
15483
|
const response = event.data;
|
|
15158
15484
|
const isStopped = response.type === "stateChange" && response.state === "stopped";
|
|
15159
15485
|
if (isStopped) {
|
|
15160
|
-
|
|
15161
|
-
|
|
15162
|
-
|
|
15163
|
-
|
|
15164
|
-
|
|
15486
|
+
const canSettle2 = settleOnce();
|
|
15487
|
+
if (!canSettle2) {
|
|
15488
|
+
return;
|
|
15489
|
+
}
|
|
15490
|
+
this.resetFinalizeRuntimeState();
|
|
15491
|
+
Promise.resolve().then(() => this.createBlobFromChunks()).then((streamProcessorResult) => {
|
|
15492
|
+
resolve(streamProcessorResult);
|
|
15493
|
+
}, (error) => {
|
|
15494
|
+
this.rejectFinalizeBlobCreationError(reject, error, finalizeStartedAtMilliseconds);
|
|
15495
|
+
});
|
|
15165
15496
|
return;
|
|
15166
15497
|
}
|
|
15167
15498
|
const isError = response.type === "error";
|
|
15168
|
-
if (isError) {
|
|
15169
|
-
|
|
15170
|
-
clearTimeout(timeout);
|
|
15171
|
-
this.stopAudioWorklet();
|
|
15172
|
-
reject(new Error(response.error));
|
|
15499
|
+
if (!isError) {
|
|
15500
|
+
return;
|
|
15173
15501
|
}
|
|
15502
|
+
const canSettle = settleOnce();
|
|
15503
|
+
if (!canSettle) {
|
|
15504
|
+
return;
|
|
15505
|
+
}
|
|
15506
|
+
this.resetFinalizeRuntimeState();
|
|
15507
|
+
logger.error("[WorkerProcessor] Finalize failed", {
|
|
15508
|
+
elapsedSeconds: (performance.now() - finalizeStartedAtMilliseconds) / MILLISECONDS_PER_SECOND2,
|
|
15509
|
+
error: response.error
|
|
15510
|
+
});
|
|
15511
|
+
reject(new Error(response.error));
|
|
15174
15512
|
};
|
|
15175
|
-
|
|
15176
|
-
|
|
15177
|
-
|
|
15178
|
-
|
|
15179
|
-
|
|
15513
|
+
timeoutId = setTimeout(() => {
|
|
15514
|
+
const canSettle = settleOnce();
|
|
15515
|
+
if (!canSettle) {
|
|
15516
|
+
return;
|
|
15517
|
+
}
|
|
15518
|
+
logger.error("[WorkerProcessor] Finalize timeout reached", {
|
|
15519
|
+
elapsedSeconds: (performance.now() - finalizeStartedAtMilliseconds) / MILLISECONDS_PER_SECOND2
|
|
15520
|
+
});
|
|
15521
|
+
this.resetFinalizeRuntimeState();
|
|
15522
|
+
reject(new Error("Finalize timeout"));
|
|
15523
|
+
}, FINALIZE_TIMEOUT_MILLISECONDS);
|
|
15524
|
+
worker.addEventListener("message", messageHandler);
|
|
15525
|
+
const message = { type: "stop" };
|
|
15526
|
+
worker.postMessage(message);
|
|
15180
15527
|
});
|
|
15181
15528
|
}
|
|
15529
|
+
resetFinalizeRuntimeState() {
|
|
15530
|
+
this.isActive = false;
|
|
15531
|
+
this.stopAudioWorklet();
|
|
15532
|
+
}
|
|
15182
15533
|
createBlobFromChunks() {
|
|
15183
15534
|
const sortedChunks = [...this.chunks].sort((a, b) => a.position - b.position);
|
|
15184
15535
|
const buffer = new ArrayBuffer(this.totalSize);
|
|
@@ -15186,12 +15537,24 @@ class WorkerProcessor {
|
|
|
15186
15537
|
for (const chunk of sortedChunks) {
|
|
15187
15538
|
view.set(chunk.data, chunk.position);
|
|
15188
15539
|
}
|
|
15540
|
+
assertMp4ContainerIsNonFragmented(buffer);
|
|
15189
15541
|
const blob = new Blob([buffer], { type: "video/mp4" });
|
|
15190
15542
|
return {
|
|
15191
15543
|
blob,
|
|
15192
15544
|
totalSize: this.totalSize
|
|
15193
15545
|
};
|
|
15194
15546
|
}
|
|
15547
|
+
rejectFinalizeBlobCreationError(reject, error, finalizeStartedAtMilliseconds) {
|
|
15548
|
+
logger.error("[WorkerProcessor] Finalize failed while creating blob", {
|
|
15549
|
+
elapsedSeconds: (performance.now() - finalizeStartedAtMilliseconds) / MILLISECONDS_PER_SECOND2,
|
|
15550
|
+
error: extractErrorMessage(error)
|
|
15551
|
+
});
|
|
15552
|
+
if (error instanceof Error) {
|
|
15553
|
+
reject(error);
|
|
15554
|
+
return;
|
|
15555
|
+
}
|
|
15556
|
+
reject(new Error(extractErrorMessage(error)));
|
|
15557
|
+
}
|
|
15195
15558
|
cancel() {
|
|
15196
15559
|
if (this.worker && this.isActive) {
|
|
15197
15560
|
const message = { type: "stop" };
|