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