framewebworker 0.1.0 → 0.1.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/README.md +8 -8
- package/dist/index.cjs +292 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +17 -3
- package/dist/index.d.ts +17 -3
- package/dist/index.js +291 -14
- package/dist/index.js.map +1 -1
- package/dist/react/index.cjs +9 -4
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +18 -4
- package/dist/react/index.d.ts +18 -4
- package/dist/react/index.js +9 -4
- package/dist/react/index.js.map +1 -1
- package/dist/worker/render-worker.js +256 -0
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -114,15 +114,29 @@ interface FrameWorkerConfig {
|
|
|
114
114
|
/** Default output height */
|
|
115
115
|
height?: number;
|
|
116
116
|
}
|
|
117
|
+
type ClipStatus = 'pending' | 'rendering' | 'encoding' | 'done' | 'error';
|
|
118
|
+
interface ClipProgress {
|
|
119
|
+
index: number;
|
|
120
|
+
status: ClipStatus;
|
|
121
|
+
progress: number;
|
|
122
|
+
}
|
|
123
|
+
interface RichProgress {
|
|
124
|
+
overall: number;
|
|
125
|
+
clips: ClipProgress[];
|
|
126
|
+
}
|
|
127
|
+
/** Extends RenderOptions with rich per-clip progress reporting */
|
|
128
|
+
interface StitchOptions extends Omit<RenderOptions, 'onProgress'> {
|
|
129
|
+
onProgress?: (progress: RichProgress) => void;
|
|
130
|
+
}
|
|
117
131
|
interface FrameWorker {
|
|
118
132
|
/** Render a single clip to a Blob */
|
|
119
133
|
render(clip: ClipInput, options?: RenderOptions): Promise<Blob>;
|
|
120
134
|
/** Render a single clip and return an object URL */
|
|
121
135
|
renderToUrl(clip: ClipInput, options?: RenderOptions): Promise<string>;
|
|
122
136
|
/** Stitch multiple clips into one Blob */
|
|
123
|
-
stitch(clips: ClipInput[], options?:
|
|
137
|
+
stitch(clips: ClipInput[], options?: StitchOptions): Promise<Blob>;
|
|
124
138
|
/** Stitch multiple clips and return an object URL */
|
|
125
|
-
stitchToUrl(clips: ClipInput[], options?:
|
|
139
|
+
stitchToUrl(clips: ClipInput[], options?: StitchOptions): Promise<string>;
|
|
126
140
|
}
|
|
127
141
|
|
|
128
142
|
declare const STYLE_PRESETS: Record<CaptionStylePreset, CaptionStyle>;
|
|
@@ -141,4 +155,4 @@ declare function createFFmpegBackend(): FFmpegBackend;
|
|
|
141
155
|
|
|
142
156
|
declare function createFrameWorker(config?: FrameWorkerConfig): FrameWorker;
|
|
143
157
|
|
|
144
|
-
export { type AspectRatio, type CaptionOptions, type CaptionSegment, type CaptionStyle, type CaptionStylePreset, type ClipInput, type CropOptions, type EncodeOptions, FFmpegBackend, type FrameData, type FrameWorker, type FrameWorkerConfig, type RenderOptions, type RendererBackend, STYLE_PRESETS, createFFmpegBackend, createFrameWorker };
|
|
158
|
+
export { type AspectRatio, type CaptionOptions, type CaptionSegment, type CaptionStyle, type CaptionStylePreset, type ClipInput, type ClipProgress, type ClipStatus, type CropOptions, type EncodeOptions, FFmpegBackend, type FrameData, type FrameWorker, type FrameWorkerConfig, type RenderOptions, type RendererBackend, type RichProgress, STYLE_PRESETS, type StitchOptions, createFFmpegBackend, createFrameWorker };
|
package/dist/index.d.ts
CHANGED
|
@@ -114,15 +114,29 @@ interface FrameWorkerConfig {
|
|
|
114
114
|
/** Default output height */
|
|
115
115
|
height?: number;
|
|
116
116
|
}
|
|
117
|
+
type ClipStatus = 'pending' | 'rendering' | 'encoding' | 'done' | 'error';
|
|
118
|
+
interface ClipProgress {
|
|
119
|
+
index: number;
|
|
120
|
+
status: ClipStatus;
|
|
121
|
+
progress: number;
|
|
122
|
+
}
|
|
123
|
+
interface RichProgress {
|
|
124
|
+
overall: number;
|
|
125
|
+
clips: ClipProgress[];
|
|
126
|
+
}
|
|
127
|
+
/** Extends RenderOptions with rich per-clip progress reporting */
|
|
128
|
+
interface StitchOptions extends Omit<RenderOptions, 'onProgress'> {
|
|
129
|
+
onProgress?: (progress: RichProgress) => void;
|
|
130
|
+
}
|
|
117
131
|
interface FrameWorker {
|
|
118
132
|
/** Render a single clip to a Blob */
|
|
119
133
|
render(clip: ClipInput, options?: RenderOptions): Promise<Blob>;
|
|
120
134
|
/** Render a single clip and return an object URL */
|
|
121
135
|
renderToUrl(clip: ClipInput, options?: RenderOptions): Promise<string>;
|
|
122
136
|
/** Stitch multiple clips into one Blob */
|
|
123
|
-
stitch(clips: ClipInput[], options?:
|
|
137
|
+
stitch(clips: ClipInput[], options?: StitchOptions): Promise<Blob>;
|
|
124
138
|
/** Stitch multiple clips and return an object URL */
|
|
125
|
-
stitchToUrl(clips: ClipInput[], options?:
|
|
139
|
+
stitchToUrl(clips: ClipInput[], options?: StitchOptions): Promise<string>;
|
|
126
140
|
}
|
|
127
141
|
|
|
128
142
|
declare const STYLE_PRESETS: Record<CaptionStylePreset, CaptionStyle>;
|
|
@@ -141,4 +155,4 @@ declare function createFFmpegBackend(): FFmpegBackend;
|
|
|
141
155
|
|
|
142
156
|
declare function createFrameWorker(config?: FrameWorkerConfig): FrameWorker;
|
|
143
157
|
|
|
144
|
-
export { type AspectRatio, type CaptionOptions, type CaptionSegment, type CaptionStyle, type CaptionStylePreset, type ClipInput, type CropOptions, type EncodeOptions, FFmpegBackend, type FrameData, type FrameWorker, type FrameWorkerConfig, type RenderOptions, type RendererBackend, STYLE_PRESETS, createFFmpegBackend, createFrameWorker };
|
|
158
|
+
export { type AspectRatio, type CaptionOptions, type CaptionSegment, type CaptionStyle, type CaptionStylePreset, type ClipInput, type ClipProgress, type ClipStatus, type CropOptions, type EncodeOptions, FFmpegBackend, type FrameData, type FrameWorker, type FrameWorkerConfig, type RenderOptions, type RendererBackend, type RichProgress, STYLE_PRESETS, type StitchOptions, createFFmpegBackend, createFrameWorker };
|
package/dist/index.js
CHANGED
|
@@ -468,25 +468,211 @@ function seekVideo(video, time) {
|
|
|
468
468
|
});
|
|
469
469
|
}
|
|
470
470
|
|
|
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
|
+
var WorkerPool = class {
|
|
523
|
+
constructor(maxConcurrency) {
|
|
524
|
+
this.workers = [];
|
|
525
|
+
this.available = [];
|
|
526
|
+
this.waiters = [];
|
|
527
|
+
for (let i = 0; i < maxConcurrency; i++) {
|
|
528
|
+
const w = new Worker(new URL("./render-worker.js", import.meta.url), { type: "module" });
|
|
529
|
+
this.workers.push(w);
|
|
530
|
+
this.available.push(w);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
acquire() {
|
|
534
|
+
if (this.available.length > 0) return Promise.resolve(this.available.pop());
|
|
535
|
+
return new Promise((resolve) => this.waiters.push(resolve));
|
|
536
|
+
}
|
|
537
|
+
release(worker) {
|
|
538
|
+
if (this.waiters.length > 0) {
|
|
539
|
+
this.waiters.shift()(worker);
|
|
540
|
+
} else {
|
|
541
|
+
this.available.push(worker);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
async dispatch(clip, width, height, fps, signal, onProgress) {
|
|
545
|
+
const worker = await this.acquire();
|
|
546
|
+
try {
|
|
547
|
+
return await this.processClip(worker, clip, width, height, fps, signal, onProgress);
|
|
548
|
+
} finally {
|
|
549
|
+
this.release(worker);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
async processClip(worker, clip, width, height, fps, signal, onProgress) {
|
|
553
|
+
let srcUrl;
|
|
554
|
+
let needsRevoke = false;
|
|
555
|
+
if (typeof clip.source === "string") {
|
|
556
|
+
srcUrl = clip.source;
|
|
557
|
+
} else if (clip.source instanceof HTMLVideoElement) {
|
|
558
|
+
srcUrl = clip.source.src;
|
|
559
|
+
} else {
|
|
560
|
+
srcUrl = URL.createObjectURL(clip.source);
|
|
561
|
+
needsRevoke = true;
|
|
562
|
+
}
|
|
563
|
+
const video = document.createElement("video");
|
|
564
|
+
video.muted = true;
|
|
565
|
+
video.crossOrigin = "anonymous";
|
|
566
|
+
video.preload = "auto";
|
|
567
|
+
await new Promise((resolve, reject) => {
|
|
568
|
+
video.onloadedmetadata = () => resolve();
|
|
569
|
+
video.onerror = () => reject(new Error(`Failed to load video: ${srcUrl}`));
|
|
570
|
+
video.src = srcUrl;
|
|
571
|
+
});
|
|
572
|
+
const duration = video.duration;
|
|
573
|
+
const startTime = clip.startTime ?? 0;
|
|
574
|
+
const endTime = clip.endTime ?? duration;
|
|
575
|
+
const clipDuration = endTime - startTime;
|
|
576
|
+
const [outW, outH] = resolveOutputDimensions2(clip, video.videoWidth, video.videoHeight, width, height);
|
|
577
|
+
const totalFrames = Math.ceil(clipDuration * fps);
|
|
578
|
+
const canvas = document.createElement("canvas");
|
|
579
|
+
canvas.width = outW;
|
|
580
|
+
canvas.height = outH;
|
|
581
|
+
const ctx = canvas.getContext("2d");
|
|
582
|
+
const resultPromise = new Promise((resolve, reject) => {
|
|
583
|
+
const onMessage = (e) => {
|
|
584
|
+
const msg = e.data;
|
|
585
|
+
if (msg.type === "done") {
|
|
586
|
+
worker.removeEventListener("message", onMessage);
|
|
587
|
+
resolve(msg.frames);
|
|
588
|
+
} else if (msg.type === "error") {
|
|
589
|
+
worker.removeEventListener("message", onMessage);
|
|
590
|
+
reject(new Error(msg.message));
|
|
591
|
+
} else if (msg.type === "progress") {
|
|
592
|
+
onProgress?.(msg.value);
|
|
593
|
+
}
|
|
594
|
+
};
|
|
595
|
+
worker.addEventListener("message", onMessage);
|
|
596
|
+
});
|
|
597
|
+
const initMsg = {
|
|
598
|
+
type: "init",
|
|
599
|
+
meta: { width: outW, height: outH, fps, captions: clip.captions, totalFrames }
|
|
600
|
+
};
|
|
601
|
+
worker.postMessage(initMsg);
|
|
602
|
+
try {
|
|
603
|
+
for (let i = 0; i < totalFrames; i++) {
|
|
604
|
+
if (signal?.aborted) {
|
|
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]);
|
|
615
|
+
}
|
|
616
|
+
worker.postMessage({ type: "end" });
|
|
617
|
+
const transferableFrames = await resultPromise;
|
|
618
|
+
return transferableFrames.map((f) => ({
|
|
619
|
+
imageData: new ImageData(new Uint8ClampedArray(f.buffer), f.width, f.height),
|
|
620
|
+
timestamp: f.timestamp,
|
|
621
|
+
width: f.width,
|
|
622
|
+
height: f.height
|
|
623
|
+
}));
|
|
624
|
+
} finally {
|
|
625
|
+
if (needsRevoke) URL.revokeObjectURL(srcUrl);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
terminate() {
|
|
629
|
+
for (const w of this.workers) w.terminate();
|
|
630
|
+
this.workers.length = 0;
|
|
631
|
+
this.available.length = 0;
|
|
632
|
+
}
|
|
633
|
+
};
|
|
634
|
+
|
|
471
635
|
// src/stitch.ts
|
|
636
|
+
function supportsOffscreenWorkers() {
|
|
637
|
+
return typeof Worker !== "undefined" && typeof OffscreenCanvas !== "undefined" && typeof createImageBitmap !== "undefined";
|
|
638
|
+
}
|
|
472
639
|
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) {
|
|
473
646
|
const fps = options.fps ?? 30;
|
|
474
647
|
const width = options.width ?? 1280;
|
|
475
648
|
const height = options.height ?? 720;
|
|
476
|
-
const onProgress = options
|
|
649
|
+
const { onProgress, signal } = options;
|
|
650
|
+
const clipStatuses = clips.map((_, i) => ({
|
|
651
|
+
index: i,
|
|
652
|
+
status: "pending",
|
|
653
|
+
progress: 0
|
|
654
|
+
}));
|
|
655
|
+
const emit = (overall) => {
|
|
656
|
+
onProgress?.({ overall, clips: clipStatuses.slice() });
|
|
657
|
+
};
|
|
477
658
|
const blobs = [];
|
|
478
659
|
for (let ci = 0; ci < clips.length; ci++) {
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
const frames = await extractFrames(clip, {
|
|
484
|
-
...options,
|
|
660
|
+
clipStatuses[ci].status = "rendering";
|
|
661
|
+
emit(ci / clips.length);
|
|
662
|
+
const frames = await extractFrames(clips[ci], {
|
|
663
|
+
fps,
|
|
485
664
|
width,
|
|
486
665
|
height,
|
|
487
|
-
|
|
488
|
-
|
|
666
|
+
mimeType: options.mimeType,
|
|
667
|
+
quality: options.quality,
|
|
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
|
+
}
|
|
489
674
|
});
|
|
675
|
+
clipStatuses[ci].status = "encoding";
|
|
490
676
|
const blob = await backend.encode(frames, {
|
|
491
677
|
width,
|
|
492
678
|
height,
|
|
@@ -494,13 +680,18 @@ async function stitchClips(clips, backend, options) {
|
|
|
494
680
|
mimeType: options.mimeType ?? "video/mp4",
|
|
495
681
|
quality: options.quality ?? 0.92,
|
|
496
682
|
encoderOptions: options.encoderOptions,
|
|
497
|
-
|
|
498
|
-
|
|
683
|
+
signal,
|
|
684
|
+
onProgress: (p) => {
|
|
685
|
+
clipStatuses[ci].progress = 0.9 + p * 0.1;
|
|
686
|
+
emit((ci + 0.9 + p * 0.1) / clips.length);
|
|
687
|
+
}
|
|
499
688
|
});
|
|
689
|
+
clipStatuses[ci].status = "done";
|
|
690
|
+
clipStatuses[ci].progress = 1;
|
|
500
691
|
blobs.push(blob);
|
|
501
692
|
}
|
|
502
693
|
if (blobs.length === 1) {
|
|
503
|
-
|
|
694
|
+
emit(1);
|
|
504
695
|
return blobs[0];
|
|
505
696
|
}
|
|
506
697
|
return backend.concat(blobs, {
|
|
@@ -509,10 +700,96 @@ async function stitchClips(clips, backend, options) {
|
|
|
509
700
|
fps,
|
|
510
701
|
mimeType: options.mimeType ?? "video/mp4",
|
|
511
702
|
quality: options.quality ?? 0.92,
|
|
512
|
-
|
|
513
|
-
|
|
703
|
+
signal,
|
|
704
|
+
onProgress: (p) => emit((clips.length - 1 + p) / clips.length)
|
|
514
705
|
});
|
|
515
706
|
}
|
|
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
|
+
}
|
|
516
793
|
|
|
517
794
|
// src/index.ts
|
|
518
795
|
function createFrameWorker(config = {}) {
|