framewebworker 0.1.1 → 0.1.2
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.cjs +251 -249
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -2
- package/dist/index.d.ts +1 -2
- package/dist/index.js +251 -249
- package/dist/index.js.map +1 -1
- package/dist/react/index.cjs +5 -10
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +2 -3
- package/dist/react/index.d.ts +2 -3
- package/dist/react/index.js +5 -10
- package/dist/react/index.js.map +1 -1
- package/dist/render-worker.js +177 -0
- package/dist/render-worker.js.map +1 -0
- package/package.json +1 -1
- package/dist/worker/render-worker.js +0 -256
package/dist/index.cjs
CHANGED
|
@@ -472,87 +472,48 @@ function seekVideo(video, time) {
|
|
|
472
472
|
}
|
|
473
473
|
|
|
474
474
|
// src/worker/pool.ts
|
|
475
|
-
var ASPECT_RATIO_MAP2 = {
|
|
476
|
-
"16:9": [16, 9],
|
|
477
|
-
"9:16": [9, 16],
|
|
478
|
-
"1:1": [1, 1],
|
|
479
|
-
"4:3": [4, 3],
|
|
480
|
-
"3:4": [3, 4],
|
|
481
|
-
original: [0, 0]
|
|
482
|
-
};
|
|
483
|
-
function resolveOutputDimensions2(clip, videoWidth, videoHeight, width, height) {
|
|
484
|
-
const ar = clip.aspectRatio ?? "original";
|
|
485
|
-
const ratio = ASPECT_RATIO_MAP2[ar] ?? [0, 0];
|
|
486
|
-
if (ratio[0] === 0) return [width, height];
|
|
487
|
-
const w = width;
|
|
488
|
-
const h = Math.round(w * (ratio[1] / ratio[0]));
|
|
489
|
-
return [w, h];
|
|
490
|
-
}
|
|
491
|
-
function seekVideo2(video, time) {
|
|
492
|
-
return new Promise((resolve) => {
|
|
493
|
-
if (Math.abs(video.currentTime - time) < 1e-3) {
|
|
494
|
-
resolve();
|
|
495
|
-
return;
|
|
496
|
-
}
|
|
497
|
-
const onSeeked = () => {
|
|
498
|
-
video.removeEventListener("seeked", onSeeked);
|
|
499
|
-
resolve();
|
|
500
|
-
};
|
|
501
|
-
video.addEventListener("seeked", onSeeked);
|
|
502
|
-
video.currentTime = time;
|
|
503
|
-
});
|
|
504
|
-
}
|
|
505
|
-
function drawVideoFrame2(ctx, video, clip, outW, outH) {
|
|
506
|
-
const vw = video.videoWidth;
|
|
507
|
-
const vh = video.videoHeight;
|
|
508
|
-
if (clip.crop) {
|
|
509
|
-
const { x, y, width, height } = clip.crop;
|
|
510
|
-
ctx.drawImage(video, x * vw, y * vh, width * vw, height * vh, 0, 0, outW, outH);
|
|
511
|
-
} else {
|
|
512
|
-
const videoAR = vw / vh;
|
|
513
|
-
const outAR = outW / outH;
|
|
514
|
-
let sx = 0, sy = 0, sw = vw, sh = vh;
|
|
515
|
-
if (videoAR > outAR) {
|
|
516
|
-
sw = vh * outAR;
|
|
517
|
-
sx = (vw - sw) / 2;
|
|
518
|
-
} else if (videoAR < outAR) {
|
|
519
|
-
sh = vw / outAR;
|
|
520
|
-
sy = (vh - sh) / 2;
|
|
521
|
-
}
|
|
522
|
-
ctx.drawImage(video, sx, sy, sw, sh, 0, 0, outW, outH);
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
475
|
var WorkerPool = class {
|
|
526
476
|
constructor(maxConcurrency) {
|
|
527
477
|
this.workers = [];
|
|
528
|
-
this.
|
|
529
|
-
this.
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
478
|
+
this.idleWorkers = [];
|
|
479
|
+
this.queue = [];
|
|
480
|
+
const count = Math.min(maxConcurrency, navigator.hardwareConcurrency || 2, 4);
|
|
481
|
+
for (let i = 0; i < count; i++) {
|
|
482
|
+
const worker = new Worker(
|
|
483
|
+
new URL("./render-worker.js", (typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href))),
|
|
484
|
+
{ type: "module" }
|
|
485
|
+
);
|
|
486
|
+
this.workers.push(worker);
|
|
487
|
+
this.idleWorkers.push(worker);
|
|
534
488
|
}
|
|
535
489
|
}
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
490
|
+
dispatch(jobId, clip, options, onProgress) {
|
|
491
|
+
return new Promise((resolve, reject) => {
|
|
492
|
+
this.queue.push({ jobId, clip, options, onProgress, resolve, reject });
|
|
493
|
+
this.processQueue();
|
|
494
|
+
});
|
|
539
495
|
}
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
this.
|
|
543
|
-
|
|
544
|
-
this.
|
|
496
|
+
processQueue() {
|
|
497
|
+
while (this.queue.length > 0 && this.idleWorkers.length > 0) {
|
|
498
|
+
const worker = this.idleWorkers.pop();
|
|
499
|
+
const job = this.queue.shift();
|
|
500
|
+
this.runJob(worker, job);
|
|
545
501
|
}
|
|
546
502
|
}
|
|
547
|
-
async
|
|
548
|
-
const worker = await this.acquire();
|
|
503
|
+
async runJob(worker, job) {
|
|
549
504
|
try {
|
|
550
|
-
|
|
505
|
+
const frames = await this.executeJob(worker, job);
|
|
506
|
+
job.resolve(frames);
|
|
507
|
+
} catch (err) {
|
|
508
|
+
job.reject(err instanceof Error ? err : new Error(String(err)));
|
|
551
509
|
} finally {
|
|
552
|
-
this.
|
|
510
|
+
this.idleWorkers.push(worker);
|
|
511
|
+
this.processQueue();
|
|
553
512
|
}
|
|
554
513
|
}
|
|
555
|
-
async
|
|
514
|
+
async executeJob(worker, job) {
|
|
515
|
+
const { jobId, clip, options, onProgress } = job;
|
|
516
|
+
const { fps, width: outW, height: outH, signal } = options;
|
|
556
517
|
let srcUrl;
|
|
557
518
|
let needsRevoke = false;
|
|
558
519
|
if (typeof clip.source === "string") {
|
|
@@ -567,115 +528,245 @@ var WorkerPool = class {
|
|
|
567
528
|
video.muted = true;
|
|
568
529
|
video.crossOrigin = "anonymous";
|
|
569
530
|
video.preload = "auto";
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
531
|
+
try {
|
|
532
|
+
await new Promise((res, rej) => {
|
|
533
|
+
video.onloadedmetadata = () => res();
|
|
534
|
+
video.onerror = () => rej(new Error(`Failed to load video: ${srcUrl}`));
|
|
535
|
+
video.src = srcUrl;
|
|
536
|
+
});
|
|
537
|
+
} catch (err) {
|
|
538
|
+
if (needsRevoke) URL.revokeObjectURL(srcUrl);
|
|
539
|
+
throw err;
|
|
540
|
+
}
|
|
575
541
|
const duration = video.duration;
|
|
576
|
-
const
|
|
577
|
-
const
|
|
578
|
-
const clipDuration =
|
|
579
|
-
const [outW, outH] = resolveOutputDimensions2(clip, video.videoWidth, video.videoHeight, width, height);
|
|
542
|
+
const startSec = clip.startTime ?? 0;
|
|
543
|
+
const endSec = clip.endTime ?? duration;
|
|
544
|
+
const clipDuration = endSec - startSec;
|
|
580
545
|
const totalFrames = Math.ceil(clipDuration * fps);
|
|
581
|
-
const
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
const
|
|
585
|
-
const
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
546
|
+
const captionSegments = clip.captions?.segments ?? [];
|
|
547
|
+
const baseStylePreset = clip.captions?.style?.preset ?? "modern";
|
|
548
|
+
const captionStyle = mergeStyle(STYLE_PRESETS[baseStylePreset], clip.captions?.style);
|
|
549
|
+
const offscreen = new OffscreenCanvas(outW, outH);
|
|
550
|
+
const initMsg = {
|
|
551
|
+
type: "init",
|
|
552
|
+
jobId,
|
|
553
|
+
canvas: offscreen,
|
|
554
|
+
meta: { captions: captionSegments, captionStyle, width: outW, height: outH, totalFrames }
|
|
555
|
+
};
|
|
556
|
+
worker.postMessage(initMsg, [offscreen]);
|
|
557
|
+
const resultFrames = [];
|
|
558
|
+
await new Promise((res, rej) => {
|
|
559
|
+
let aborted = false;
|
|
560
|
+
const onMessage = (event) => {
|
|
561
|
+
const msg = event.data;
|
|
562
|
+
if (msg.jobId !== jobId) return;
|
|
563
|
+
if (msg.type === "progress") {
|
|
564
|
+
onProgress(msg.currentFrame, msg.totalFrames);
|
|
565
|
+
} else if (msg.type === "done") {
|
|
589
566
|
worker.removeEventListener("message", onMessage);
|
|
590
|
-
|
|
567
|
+
for (const tf of msg.frames) {
|
|
568
|
+
resultFrames.push({
|
|
569
|
+
imageData: new ImageData(new Uint8ClampedArray(tf.buffer), tf.width, tf.height),
|
|
570
|
+
timestamp: tf.timestamp,
|
|
571
|
+
width: tf.width,
|
|
572
|
+
height: tf.height
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
res();
|
|
591
576
|
} else if (msg.type === "error") {
|
|
592
577
|
worker.removeEventListener("message", onMessage);
|
|
593
|
-
|
|
594
|
-
} else if (msg.type === "progress") {
|
|
595
|
-
onProgress?.(msg.value);
|
|
578
|
+
rej(new Error(msg.message));
|
|
596
579
|
}
|
|
597
580
|
};
|
|
598
581
|
worker.addEventListener("message", onMessage);
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
worker.postMessage({ type: "abort" });
|
|
609
|
-
throw new DOMException("Render cancelled", "AbortError");
|
|
610
|
-
}
|
|
611
|
-
const t = startTime + i / fps;
|
|
612
|
-
await seekVideo2(video, t);
|
|
613
|
-
ctx.clearRect(0, 0, outW, outH);
|
|
614
|
-
drawVideoFrame2(ctx, video, clip, outW, outH);
|
|
615
|
-
const bitmap = await createImageBitmap(canvas);
|
|
616
|
-
const frameMsg = { type: "frame", bitmap, timestamp: t - startTime, index: i };
|
|
617
|
-
worker.postMessage(frameMsg, [bitmap]);
|
|
582
|
+
if (signal) {
|
|
583
|
+
signal.addEventListener("abort", () => {
|
|
584
|
+
if (aborted) return;
|
|
585
|
+
aborted = true;
|
|
586
|
+
const abortMsg = { type: "abort", jobId };
|
|
587
|
+
worker.postMessage(abortMsg);
|
|
588
|
+
worker.removeEventListener("message", onMessage);
|
|
589
|
+
rej(new DOMException("Render cancelled", "AbortError"));
|
|
590
|
+
}, { once: true });
|
|
618
591
|
}
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
592
|
+
(async () => {
|
|
593
|
+
try {
|
|
594
|
+
for (let i = 0; i < totalFrames; i++) {
|
|
595
|
+
if (signal?.aborted || aborted) break;
|
|
596
|
+
const t = startSec + i / fps;
|
|
597
|
+
await seekVideo2(video, t);
|
|
598
|
+
const vw = video.videoWidth;
|
|
599
|
+
const vh = video.videoHeight;
|
|
600
|
+
let sx = 0, sy = 0, sw = vw, sh = vh;
|
|
601
|
+
if (clip.crop) {
|
|
602
|
+
sx = clip.crop.x * vw;
|
|
603
|
+
sy = clip.crop.y * vw;
|
|
604
|
+
sw = clip.crop.width * vw;
|
|
605
|
+
sh = clip.crop.height * vh;
|
|
606
|
+
} else {
|
|
607
|
+
const videoAR = vw / vh;
|
|
608
|
+
const outAR = outW / outH;
|
|
609
|
+
if (videoAR > outAR) {
|
|
610
|
+
sw = vh * outAR;
|
|
611
|
+
sx = (vw - sw) / 2;
|
|
612
|
+
} else if (videoAR < outAR) {
|
|
613
|
+
sh = vw / outAR;
|
|
614
|
+
sy = (vh - sh) / 2;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
const bitmap = await createImageBitmap(video, sx, sy, sw, sh, {
|
|
618
|
+
resizeWidth: outW,
|
|
619
|
+
resizeHeight: outH
|
|
620
|
+
});
|
|
621
|
+
const frameMsg = {
|
|
622
|
+
type: "frame",
|
|
623
|
+
jobId,
|
|
624
|
+
frameIndex: i,
|
|
625
|
+
bitmap,
|
|
626
|
+
timestamp: t - startSec
|
|
627
|
+
};
|
|
628
|
+
worker.postMessage(frameMsg, [bitmap]);
|
|
629
|
+
}
|
|
630
|
+
if (!signal?.aborted && !aborted) {
|
|
631
|
+
const endMsg = { type: "end", jobId };
|
|
632
|
+
worker.postMessage(endMsg);
|
|
633
|
+
}
|
|
634
|
+
} catch (err) {
|
|
635
|
+
if (!aborted) rej(err instanceof Error ? err : new Error(String(err)));
|
|
636
|
+
}
|
|
637
|
+
})();
|
|
638
|
+
});
|
|
639
|
+
if (needsRevoke) URL.revokeObjectURL(srcUrl);
|
|
640
|
+
return resultFrames;
|
|
630
641
|
}
|
|
631
642
|
terminate() {
|
|
632
|
-
for (const
|
|
633
|
-
|
|
634
|
-
|
|
643
|
+
for (const worker of this.workers) {
|
|
644
|
+
worker.terminate();
|
|
645
|
+
}
|
|
646
|
+
this.workers = [];
|
|
647
|
+
this.idleWorkers = [];
|
|
635
648
|
}
|
|
636
649
|
};
|
|
650
|
+
function seekVideo2(video, time) {
|
|
651
|
+
return new Promise((resolve) => {
|
|
652
|
+
if (Math.abs(video.currentTime - time) < 1e-3) {
|
|
653
|
+
resolve();
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
const onSeeked = () => {
|
|
657
|
+
video.removeEventListener("seeked", onSeeked);
|
|
658
|
+
resolve();
|
|
659
|
+
};
|
|
660
|
+
video.addEventListener("seeked", onSeeked);
|
|
661
|
+
video.currentTime = time;
|
|
662
|
+
});
|
|
663
|
+
}
|
|
637
664
|
|
|
638
665
|
// src/stitch.ts
|
|
639
|
-
function supportsOffscreenWorkers() {
|
|
640
|
-
return typeof Worker !== "undefined" && typeof OffscreenCanvas !== "undefined" && typeof createImageBitmap !== "undefined";
|
|
641
|
-
}
|
|
642
666
|
async function stitchClips(clips, backend, options) {
|
|
643
|
-
if (supportsOffscreenWorkers() && clips.length > 1) {
|
|
644
|
-
return stitchParallel(clips, backend, options);
|
|
645
|
-
}
|
|
646
|
-
return stitchSequential(clips, backend, options);
|
|
647
|
-
}
|
|
648
|
-
async function stitchSequential(clips, backend, options) {
|
|
649
667
|
const fps = options.fps ?? 30;
|
|
650
668
|
const width = options.width ?? 1280;
|
|
651
669
|
const height = options.height ?? 720;
|
|
652
|
-
const
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
const
|
|
659
|
-
|
|
670
|
+
const onProgress = options.onProgress;
|
|
671
|
+
if (!("OffscreenCanvas" in globalThis) || !("Worker" in globalThis)) {
|
|
672
|
+
return sequentialStitch(clips, backend, options, fps, width, height);
|
|
673
|
+
}
|
|
674
|
+
const concurrency = Math.min(clips.length, navigator.hardwareConcurrency || 2, 4);
|
|
675
|
+
const pool = new WorkerPool(concurrency);
|
|
676
|
+
const clipStatuses = clips.map(() => "queued");
|
|
677
|
+
const clipProgresses = clips.map(() => 0);
|
|
678
|
+
const emitProgress = () => {
|
|
679
|
+
if (!onProgress) return;
|
|
680
|
+
const overall = clipProgresses.reduce((a, b) => a + b, 0) / clips.length;
|
|
681
|
+
const clipData = clips.map((_, i) => ({
|
|
682
|
+
index: i,
|
|
683
|
+
status: clipStatuses[i],
|
|
684
|
+
progress: clipProgresses[i]
|
|
685
|
+
}));
|
|
686
|
+
onProgress({ overall, clips: clipData });
|
|
660
687
|
};
|
|
688
|
+
try {
|
|
689
|
+
const frameArrays = await Promise.all(
|
|
690
|
+
clips.map(async (clip, i) => {
|
|
691
|
+
clipStatuses[i] = "rendering";
|
|
692
|
+
emitProgress();
|
|
693
|
+
try {
|
|
694
|
+
const frames = await pool.dispatch(
|
|
695
|
+
`clip-${i}`,
|
|
696
|
+
clip,
|
|
697
|
+
{ fps, width, height, signal: options.signal },
|
|
698
|
+
(current, total) => {
|
|
699
|
+
clipProgresses[i] = current / total;
|
|
700
|
+
emitProgress();
|
|
701
|
+
}
|
|
702
|
+
);
|
|
703
|
+
return frames;
|
|
704
|
+
} catch (err) {
|
|
705
|
+
clipStatuses[i] = "failed";
|
|
706
|
+
emitProgress();
|
|
707
|
+
throw err;
|
|
708
|
+
}
|
|
709
|
+
})
|
|
710
|
+
);
|
|
711
|
+
const blobs = [];
|
|
712
|
+
for (let i = 0; i < frameArrays.length; i++) {
|
|
713
|
+
const blob = await backend.encode(frameArrays[i], {
|
|
714
|
+
width,
|
|
715
|
+
height,
|
|
716
|
+
fps,
|
|
717
|
+
mimeType: options.mimeType ?? "video/mp4",
|
|
718
|
+
quality: options.quality ?? 0.92,
|
|
719
|
+
encoderOptions: options.encoderOptions,
|
|
720
|
+
signal: options.signal
|
|
721
|
+
});
|
|
722
|
+
clipStatuses[i] = "done";
|
|
723
|
+
clipProgresses[i] = 1;
|
|
724
|
+
emitProgress();
|
|
725
|
+
blobs.push(blob);
|
|
726
|
+
}
|
|
727
|
+
if (blobs.length === 1) {
|
|
728
|
+
onProgress?.({
|
|
729
|
+
overall: 1,
|
|
730
|
+
clips: clips.map((_, i) => ({ index: i, status: "done", progress: 1 }))
|
|
731
|
+
});
|
|
732
|
+
return blobs[0];
|
|
733
|
+
}
|
|
734
|
+
return backend.concat(blobs, {
|
|
735
|
+
width,
|
|
736
|
+
height,
|
|
737
|
+
fps,
|
|
738
|
+
mimeType: options.mimeType ?? "video/mp4",
|
|
739
|
+
quality: options.quality ?? 0.92,
|
|
740
|
+
signal: options.signal
|
|
741
|
+
});
|
|
742
|
+
} finally {
|
|
743
|
+
pool.terminate();
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
async function sequentialStitch(clips, backend, options, fps, width, height) {
|
|
747
|
+
const onProgress = options.onProgress;
|
|
661
748
|
const blobs = [];
|
|
662
749
|
for (let ci = 0; ci < clips.length; ci++) {
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
750
|
+
const clip = clips[ci];
|
|
751
|
+
const emitClipProgress = (p) => {
|
|
752
|
+
if (!onProgress) return;
|
|
753
|
+
const overall = (ci + p) / clips.length;
|
|
754
|
+
onProgress({
|
|
755
|
+
overall,
|
|
756
|
+
clips: clips.map((_, i) => ({
|
|
757
|
+
index: i,
|
|
758
|
+
status: i < ci ? "done" : i === ci ? "rendering" : "queued",
|
|
759
|
+
progress: i < ci ? 1 : i === ci ? p : 0
|
|
760
|
+
}))
|
|
761
|
+
});
|
|
762
|
+
};
|
|
763
|
+
const frames = await extractFrames(clip, {
|
|
764
|
+
...options,
|
|
667
765
|
width,
|
|
668
766
|
height,
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
encoderOptions: options.encoderOptions,
|
|
672
|
-
signal,
|
|
673
|
-
onProgress: (p) => {
|
|
674
|
-
clipStatuses[ci].progress = p * 0.9;
|
|
675
|
-
emit((ci + p * 0.9) / clips.length);
|
|
676
|
-
}
|
|
767
|
+
fps,
|
|
768
|
+
onProgress: (p) => emitClipProgress(p * 0.9)
|
|
677
769
|
});
|
|
678
|
-
clipStatuses[ci].status = "encoding";
|
|
679
770
|
const blob = await backend.encode(frames, {
|
|
680
771
|
width,
|
|
681
772
|
height,
|
|
@@ -683,18 +774,16 @@ async function stitchSequential(clips, backend, options) {
|
|
|
683
774
|
mimeType: options.mimeType ?? "video/mp4",
|
|
684
775
|
quality: options.quality ?? 0.92,
|
|
685
776
|
encoderOptions: options.encoderOptions,
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
clipStatuses[ci].progress = 0.9 + p * 0.1;
|
|
689
|
-
emit((ci + 0.9 + p * 0.1) / clips.length);
|
|
690
|
-
}
|
|
777
|
+
onProgress: (p) => emitClipProgress(0.9 + p * 0.1),
|
|
778
|
+
signal: options.signal
|
|
691
779
|
});
|
|
692
|
-
clipStatuses[ci].status = "done";
|
|
693
|
-
clipStatuses[ci].progress = 1;
|
|
694
780
|
blobs.push(blob);
|
|
695
781
|
}
|
|
696
782
|
if (blobs.length === 1) {
|
|
697
|
-
|
|
783
|
+
onProgress?.({
|
|
784
|
+
overall: 1,
|
|
785
|
+
clips: clips.map((_, i) => ({ index: i, status: "done", progress: 1 }))
|
|
786
|
+
});
|
|
698
787
|
return blobs[0];
|
|
699
788
|
}
|
|
700
789
|
return backend.concat(blobs, {
|
|
@@ -703,96 +792,9 @@ async function stitchSequential(clips, backend, options) {
|
|
|
703
792
|
fps,
|
|
704
793
|
mimeType: options.mimeType ?? "video/mp4",
|
|
705
794
|
quality: options.quality ?? 0.92,
|
|
706
|
-
signal
|
|
707
|
-
onProgress: (p) => emit((clips.length - 1 + p) / clips.length)
|
|
795
|
+
signal: options.signal
|
|
708
796
|
});
|
|
709
797
|
}
|
|
710
|
-
async function stitchParallel(clips, backend, options) {
|
|
711
|
-
const fps = options.fps ?? 30;
|
|
712
|
-
const width = options.width ?? 1280;
|
|
713
|
-
const height = options.height ?? 720;
|
|
714
|
-
const { onProgress, signal } = options;
|
|
715
|
-
const concurrency = Math.min(
|
|
716
|
-
clips.length,
|
|
717
|
-
typeof navigator !== "undefined" ? navigator.hardwareConcurrency || 2 : 2,
|
|
718
|
-
4
|
|
719
|
-
);
|
|
720
|
-
const clipStatuses = clips.map((_, i) => ({
|
|
721
|
-
index: i,
|
|
722
|
-
status: "pending",
|
|
723
|
-
progress: 0
|
|
724
|
-
}));
|
|
725
|
-
const emit = () => {
|
|
726
|
-
const overall = clipStatuses.reduce((sum, c) => sum + c.progress, 0) / clips.length;
|
|
727
|
-
onProgress?.({ overall, clips: clipStatuses.slice() });
|
|
728
|
-
};
|
|
729
|
-
const pool = new WorkerPool(concurrency);
|
|
730
|
-
const blobs = new Array(clips.length);
|
|
731
|
-
let encodeChain = Promise.resolve();
|
|
732
|
-
try {
|
|
733
|
-
await Promise.all(
|
|
734
|
-
clips.map(async (clip, ci) => {
|
|
735
|
-
clipStatuses[ci].status = "rendering";
|
|
736
|
-
emit();
|
|
737
|
-
const frames = await pool.dispatch(
|
|
738
|
-
clip,
|
|
739
|
-
width,
|
|
740
|
-
height,
|
|
741
|
-
fps,
|
|
742
|
-
signal,
|
|
743
|
-
(p) => {
|
|
744
|
-
clipStatuses[ci].progress = p * 0.85;
|
|
745
|
-
emit();
|
|
746
|
-
}
|
|
747
|
-
);
|
|
748
|
-
clipStatuses[ci].status = "encoding";
|
|
749
|
-
clipStatuses[ci].progress = 0.85;
|
|
750
|
-
emit();
|
|
751
|
-
await new Promise((resolve, reject) => {
|
|
752
|
-
encodeChain = encodeChain.then(async () => {
|
|
753
|
-
try {
|
|
754
|
-
blobs[ci] = await backend.encode(frames, {
|
|
755
|
-
width,
|
|
756
|
-
height,
|
|
757
|
-
fps,
|
|
758
|
-
mimeType: options.mimeType ?? "video/mp4",
|
|
759
|
-
quality: options.quality ?? 0.92,
|
|
760
|
-
encoderOptions: options.encoderOptions,
|
|
761
|
-
signal,
|
|
762
|
-
onProgress: (p) => {
|
|
763
|
-
clipStatuses[ci].progress = 0.85 + p * 0.15;
|
|
764
|
-
emit();
|
|
765
|
-
}
|
|
766
|
-
});
|
|
767
|
-
clipStatuses[ci].status = "done";
|
|
768
|
-
clipStatuses[ci].progress = 1;
|
|
769
|
-
emit();
|
|
770
|
-
resolve();
|
|
771
|
-
} catch (err) {
|
|
772
|
-
clipStatuses[ci].status = "error";
|
|
773
|
-
reject(err);
|
|
774
|
-
throw err;
|
|
775
|
-
}
|
|
776
|
-
});
|
|
777
|
-
});
|
|
778
|
-
})
|
|
779
|
-
);
|
|
780
|
-
if (blobs.length === 1) {
|
|
781
|
-
onProgress?.({ overall: 1, clips: clipStatuses.slice() });
|
|
782
|
-
return blobs[0];
|
|
783
|
-
}
|
|
784
|
-
return backend.concat(blobs, {
|
|
785
|
-
width,
|
|
786
|
-
height,
|
|
787
|
-
fps,
|
|
788
|
-
mimeType: options.mimeType ?? "video/mp4",
|
|
789
|
-
quality: options.quality ?? 0.92,
|
|
790
|
-
signal
|
|
791
|
-
});
|
|
792
|
-
} finally {
|
|
793
|
-
pool.terminate();
|
|
794
|
-
}
|
|
795
|
-
}
|
|
796
798
|
|
|
797
799
|
// src/index.ts
|
|
798
800
|
function createFrameWorker(config = {}) {
|