@pixerate/editor 0.89.0 → 0.91.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/image-editor/index.cjs +2770 -176
- package/dist/image-editor/index.cjs.map +1 -1
- package/dist/image-editor/index.d.cts +439 -8
- package/dist/image-editor/index.d.ts +439 -8
- package/dist/image-editor/index.js +2653 -59
- package/dist/image-editor/index.js.map +1 -1
- package/dist/video-editor/index.cjs +561 -69
- package/dist/video-editor/index.cjs.map +1 -1
- package/dist/video-editor/index.d.cts +285 -1
- package/dist/video-editor/index.d.ts +285 -1
- package/dist/video-editor/index.js +506 -14
- package/dist/video-editor/index.js.map +1 -1
- package/package.json +1 -1
- package/skills/pixerate-video-editor/SKILL.md +13 -0
|
@@ -852,12 +852,33 @@ function parseVideoAsset(v, path = "asset") {
|
|
|
852
852
|
height: num(v.height, `${path}.height`, { optional: true, positive: true }),
|
|
853
853
|
hasAudio: bool(v.hasAudio, `${path}.hasAudio`),
|
|
854
854
|
poster: str(v.poster, `${path}.poster`, true),
|
|
855
|
-
transcript: parseVideoTranscriptWords(v.transcript, `${path}.transcript`)
|
|
855
|
+
transcript: parseVideoTranscriptWords(v.transcript, `${path}.transcript`),
|
|
856
|
+
proxies: parseVideoAssetProxies(v.proxies, `${path}.proxies`)
|
|
856
857
|
});
|
|
858
|
+
if (kind !== "video" && asset.proxies) fail(`${path}.proxies`, "are only for video assets");
|
|
857
859
|
if (kind === "image" && asset.transcript) fail(`${path}.transcript`, "is only for video and audio assets");
|
|
858
860
|
if (kind === "image" && asset.duration !== void 0) fail(`${path}.duration`, "is only for video and audio assets");
|
|
859
861
|
return asset;
|
|
860
862
|
}
|
|
863
|
+
function parseVideoAssetProxies(v, path) {
|
|
864
|
+
if (v === void 0) return void 0;
|
|
865
|
+
if (!Array.isArray(v)) return fail(path, "must be a list of { scale, src }");
|
|
866
|
+
const out = v.map((p, i) => {
|
|
867
|
+
const at = `${path}[${i}]`;
|
|
868
|
+
if (!isObject(p)) return fail(at, "must be an object { scale, src }");
|
|
869
|
+
const unknown = Object.keys(p).find((k) => !["scale", "src", "width", "height"].includes(k));
|
|
870
|
+
if (unknown) fail(`${at}.${unknown}`, "is not a proxy setting; use scale, src, width, height");
|
|
871
|
+
const scale = num(p.scale, `${at}.scale`, { positive: true });
|
|
872
|
+
if (scale >= 1) fail(`${at}.scale`, "must be less than 1 (0.5 is half size)");
|
|
873
|
+
return compact({ scale, src: str(p.src, `${at}.src`), width: num(p.width, `${at}.width`, { optional: true, positive: true }), height: num(p.height, `${at}.height`, { optional: true, positive: true }) });
|
|
874
|
+
});
|
|
875
|
+
const scales = /* @__PURE__ */ new Set();
|
|
876
|
+
for (const [i, p] of out.entries()) {
|
|
877
|
+
if (scales.has(p.scale)) fail(`${path}[${i}].scale`, `repeats ${p.scale}; give each proxy its own scale`);
|
|
878
|
+
scales.add(p.scale);
|
|
879
|
+
}
|
|
880
|
+
return out.length ? out : void 0;
|
|
881
|
+
}
|
|
861
882
|
function parseVideoClip(v, path = "clip") {
|
|
862
883
|
if (!isObject(v)) return fail(path, "must be an object");
|
|
863
884
|
const type = oneOf(v.type ?? "media", VIDEO_CLIP_TYPES, `${path}.type`, false);
|
|
@@ -3219,7 +3240,7 @@ var color = (c) => {
|
|
|
3219
3240
|
const h = hex[1].length === 3 ? [...hex[1]].map((x) => x + x).join("") : hex[1];
|
|
3220
3241
|
return `0x${h.slice(0, 6)}${h.length === 8 ? `@${(parseInt(h.slice(6), 16) / 255).toFixed(3)}` : ""}`;
|
|
3221
3242
|
};
|
|
3222
|
-
var
|
|
3243
|
+
var videoShellQuote = (s) => /^[\w@%+=:,./-]+$/.test(s) ? s : `'${s.replace(/'/g, `'\\''`)}'`;
|
|
3223
3244
|
function atempo(speed) {
|
|
3224
3245
|
const out = [];
|
|
3225
3246
|
let s = speed;
|
|
@@ -3515,7 +3536,7 @@ function buildFfmpeg(project, options, measure) {
|
|
|
3515
3536
|
const filterComplex = graph.join(";");
|
|
3516
3537
|
if (measure) {
|
|
3517
3538
|
const args2 = ["-hide_banner", "-nostats", ...inputs.flat(), "-filter_complex", filterComplex, "-map", "[aout]", "-f", "null", "-"];
|
|
3518
|
-
return { args: args2, command: ["ffmpeg", ...args2].map(
|
|
3539
|
+
return { args: args2, command: ["ffmpeg", ...args2].map(videoShellQuote).join(" "), filterComplex, duration: D, notes };
|
|
3519
3540
|
}
|
|
3520
3541
|
const args = [
|
|
3521
3542
|
"-y",
|
|
@@ -3536,7 +3557,333 @@ function buildFfmpeg(project, options, measure) {
|
|
|
3536
3557
|
sec(D),
|
|
3537
3558
|
options.output ?? "output.mp4"
|
|
3538
3559
|
];
|
|
3539
|
-
return { args, command: ["ffmpeg", ...args].map(
|
|
3560
|
+
return { args, command: ["ffmpeg", ...args].map(videoShellQuote).join(" "), filterComplex, duration: D, notes };
|
|
3561
|
+
}
|
|
3562
|
+
|
|
3563
|
+
// src/video-editor/proxy.ts
|
|
3564
|
+
var VIDEO_PROXY_SCALES = [0.5, 0.25];
|
|
3565
|
+
function videoAssetProxy(asset, scale) {
|
|
3566
|
+
if (!asset?.proxies?.length || !(scale > 0) || scale >= 1) return null;
|
|
3567
|
+
let best = null;
|
|
3568
|
+
for (const p of asset.proxies) if (p.scale >= scale - 1e-9 && p.scale < 1 && (!best || p.scale < best.scale)) best = p;
|
|
3569
|
+
return best;
|
|
3570
|
+
}
|
|
3571
|
+
var videoPreviewSrc = (asset, scale, fallback = asset?.src ?? "") => videoAssetProxy(asset, scale)?.src ?? fallback;
|
|
3572
|
+
function resolveVideoPreviewScale(scale, shown) {
|
|
3573
|
+
if (scale === void 0 || scale === "auto") return Math.min(1, Math.max(0, shown));
|
|
3574
|
+
if (scale === "full") return 1;
|
|
3575
|
+
return Math.min(1, Math.max(0, scale));
|
|
3576
|
+
}
|
|
3577
|
+
function videoProxyName(src, scale = 0.5) {
|
|
3578
|
+
const clean = src.replace(/[?#].*$/, "");
|
|
3579
|
+
const base = clean.replace(/\.[^./\\]+$/, "");
|
|
3580
|
+
return `${base}.proxy-${Math.round(scale * 100)}.mp4`;
|
|
3581
|
+
}
|
|
3582
|
+
function videoProxyFfmpeg(asset, options = {}) {
|
|
3583
|
+
if (asset.kind !== "video") throw new Error(`Asset "${asset.id}" is ${asset.kind === "image" ? "an image" : "audio"}; only video assets have proxies.`);
|
|
3584
|
+
const scale = options.scale ?? 0.5;
|
|
3585
|
+
if (!(scale > 0 && scale < 1)) throw new Error(`A proxy's scale must be between 0 and 1 (not including either), not ${scale}; 0.5 is half size.`);
|
|
3586
|
+
const gop = Math.max(1, Math.round(options.gop ?? 12));
|
|
3587
|
+
const output = options.output ?? videoProxyName(asset.src, scale);
|
|
3588
|
+
const src = options.resolve ? options.resolve(asset) : asset.src;
|
|
3589
|
+
const size = `scale=trunc(iw*${scale}/2)*2:trunc(ih*${scale}/2)*2`;
|
|
3590
|
+
const args = [
|
|
3591
|
+
"-y",
|
|
3592
|
+
"-i",
|
|
3593
|
+
src,
|
|
3594
|
+
"-vf",
|
|
3595
|
+
size,
|
|
3596
|
+
"-c:v",
|
|
3597
|
+
"libx264",
|
|
3598
|
+
"-preset",
|
|
3599
|
+
"veryfast",
|
|
3600
|
+
"-tune",
|
|
3601
|
+
"fastdecode",
|
|
3602
|
+
"-crf",
|
|
3603
|
+
String(options.crf ?? 28),
|
|
3604
|
+
"-g",
|
|
3605
|
+
String(gop),
|
|
3606
|
+
"-keyint_min",
|
|
3607
|
+
String(gop),
|
|
3608
|
+
"-sc_threshold",
|
|
3609
|
+
"0",
|
|
3610
|
+
"-bf",
|
|
3611
|
+
"0",
|
|
3612
|
+
"-pix_fmt",
|
|
3613
|
+
"yuv420p",
|
|
3614
|
+
...asset.hasAudio === false ? ["-an"] : ["-c:a", "aac", "-b:a", "128k"],
|
|
3615
|
+
"-movflags",
|
|
3616
|
+
"+faststart",
|
|
3617
|
+
output
|
|
3618
|
+
];
|
|
3619
|
+
const proxy = { scale, src: output, ...asset.width && asset.height ? { width: Math.trunc(asset.width * scale / 2) * 2, height: Math.trunc(asset.height * scale / 2) * 2 } : {} };
|
|
3620
|
+
return { asset: asset.id, args, command: ["ffmpeg", ...args].map(videoShellQuote).join(" "), proxy };
|
|
3621
|
+
}
|
|
3622
|
+
function videoProxyPlan(project, options = {}) {
|
|
3623
|
+
const scale = options.scale ?? 0.5;
|
|
3624
|
+
const wanted = options.assets ? new Set(options.assets) : null;
|
|
3625
|
+
if (options.assets) for (const id of options.assets) {
|
|
3626
|
+
const a = project.assets.find((x) => x.id === id);
|
|
3627
|
+
if (!a) throw new Error(`There is no asset "${id}"; the assets are ${project.assets.map((x) => `"${x.id}"`).join(", ") || "none"}.`);
|
|
3628
|
+
if (a.kind !== "video") throw new Error(`Asset "${id}" is ${a.kind === "image" ? "an image" : "audio"}; only video assets have proxies.`);
|
|
3629
|
+
}
|
|
3630
|
+
return project.assets.filter((a) => a.kind === "video" && (!wanted || wanted.has(a.id)) && !a.proxies?.some((p) => Math.abs(p.scale - scale) < 1e-9)).map((a) => videoProxyFfmpeg(a, { ...options, scale }));
|
|
3631
|
+
}
|
|
3632
|
+
var VIDEO_PREVIEW_SCALE_OPTIONS = [
|
|
3633
|
+
{ value: "auto", label: "Auto", scale: "auto" },
|
|
3634
|
+
{ value: "full", label: "Full", scale: "full" },
|
|
3635
|
+
...VIDEO_PROXY_SCALES.map((s) => ({ value: String(s), label: s === 0.5 ? "\xBD" : s === 0.25 ? "\xBC" : `${s * 100}%`, scale: s }))
|
|
3636
|
+
];
|
|
3637
|
+
var parseVideoPreviewScale = (value) => value === "full" ? "full" : Number(value) > 0 && Number(value) <= 1 ? Number(value) : "auto";
|
|
3638
|
+
var videoProjectHasProxies = (project) => project.assets.some((a) => !!a.proxies?.length);
|
|
3639
|
+
|
|
3640
|
+
// src/video-editor/frame-cache.ts
|
|
3641
|
+
var videoSourceFrame = (sourceTime, fps) => Math.max(0, Math.floor(sourceTime * fps / 1e3 + 1e-6));
|
|
3642
|
+
var videoSourceFrameTime = (frame, fps) => (frame + 0.5) * 1e3 / fps;
|
|
3643
|
+
var videoFrameCacheKey = (src, frame) => `${frame}@${src}`;
|
|
3644
|
+
function videoFrameRequestsAt(project, time, srcFor = (l) => l.src) {
|
|
3645
|
+
const out = [];
|
|
3646
|
+
for (const layer of videoFrameAt(project, time).layers) {
|
|
3647
|
+
if (layer.type !== "media" || layer.assetKind !== "video") continue;
|
|
3648
|
+
const src = srcFor(layer);
|
|
3649
|
+
if (!src) continue;
|
|
3650
|
+
const frame = videoSourceFrame(layer.sourceTime ?? 0, project.fps);
|
|
3651
|
+
out.push({ key: videoFrameCacheKey(src, frame), src, frame, sourceTime: videoSourceFrameTime(frame, project.fps), time });
|
|
3652
|
+
}
|
|
3653
|
+
return out;
|
|
3654
|
+
}
|
|
3655
|
+
function planVideoPrefetch(project, time, options = {}) {
|
|
3656
|
+
const window2 = options.window ?? 2e3;
|
|
3657
|
+
const dir = options.direction ?? 1;
|
|
3658
|
+
const step = 1e3 / project.fps;
|
|
3659
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3660
|
+
const out = [];
|
|
3661
|
+
for (let i = 0; i * step <= window2 + 1e-6; i++) {
|
|
3662
|
+
const t = time + dir * i * step;
|
|
3663
|
+
if (t < 0) break;
|
|
3664
|
+
for (const r of videoFrameRequestsAt(project, t, options.srcFor)) {
|
|
3665
|
+
if (seen.has(r.key)) continue;
|
|
3666
|
+
seen.add(r.key);
|
|
3667
|
+
out.push(r);
|
|
3668
|
+
}
|
|
3669
|
+
}
|
|
3670
|
+
return out;
|
|
3671
|
+
}
|
|
3672
|
+
function videoCacheEvictions(entries, options) {
|
|
3673
|
+
let total = entries.reduce((n2, e) => n2 + e.bytes, 0);
|
|
3674
|
+
if (total <= options.maxBytes) return [];
|
|
3675
|
+
const dir = options.direction ?? 1;
|
|
3676
|
+
const behind = options.behind ?? 2;
|
|
3677
|
+
const cost = (e) => {
|
|
3678
|
+
const d = (e.time - options.playhead) * dir;
|
|
3679
|
+
return d >= 0 ? d : -d * behind;
|
|
3680
|
+
};
|
|
3681
|
+
const out = [];
|
|
3682
|
+
for (const e of entries.filter((x) => !options.protect?.has(x.key)).sort((a, b) => cost(b) - cost(a))) {
|
|
3683
|
+
if (total <= options.maxBytes) break;
|
|
3684
|
+
out.push(e.key);
|
|
3685
|
+
total -= e.bytes;
|
|
3686
|
+
}
|
|
3687
|
+
return out;
|
|
3688
|
+
}
|
|
3689
|
+
var defaultCapture = async (source) => {
|
|
3690
|
+
if (typeof createImageBitmap !== "function") return null;
|
|
3691
|
+
const image = await createImageBitmap(source);
|
|
3692
|
+
return { image, width: image.width, height: image.height };
|
|
3693
|
+
};
|
|
3694
|
+
var VideoFrameCache = class {
|
|
3695
|
+
constructor(options = {}) {
|
|
3696
|
+
this.options = options;
|
|
3697
|
+
this.maxBytes = options.maxBytes ?? 512 * 1024 * 1024;
|
|
3698
|
+
this.prefetch = options.prefetch ?? 2e3;
|
|
3699
|
+
this.decode = options.decode !== void 0 ? options.decode : typeof document !== "undefined" ? createVideoElementDecoder() : null;
|
|
3700
|
+
}
|
|
3701
|
+
options;
|
|
3702
|
+
entries = /* @__PURE__ */ new Map();
|
|
3703
|
+
inFlight = /* @__PURE__ */ new Set();
|
|
3704
|
+
queue = [];
|
|
3705
|
+
protect = /* @__PURE__ */ new Set();
|
|
3706
|
+
listeners = /* @__PURE__ */ new Set();
|
|
3707
|
+
playhead = 0;
|
|
3708
|
+
direction = 1;
|
|
3709
|
+
hits = 0;
|
|
3710
|
+
misses = 0;
|
|
3711
|
+
bytes = 0;
|
|
3712
|
+
generation = 0;
|
|
3713
|
+
destroyed = false;
|
|
3714
|
+
srcFor;
|
|
3715
|
+
maxBytes;
|
|
3716
|
+
prefetch;
|
|
3717
|
+
decode;
|
|
3718
|
+
/** The cached frame of `src` showing at a source time (ms), on the project's `fps` grid. Counts a hit or a miss. */
|
|
3719
|
+
get(src, sourceTime, fps) {
|
|
3720
|
+
const entry = this.entries.get(videoFrameCacheKey(src, videoSourceFrame(sourceTime, fps)));
|
|
3721
|
+
if (entry) this.hits++;
|
|
3722
|
+
else this.misses++;
|
|
3723
|
+
return entry?.frame.image;
|
|
3724
|
+
}
|
|
3725
|
+
/** Whether a frame is cached, without counting it. */
|
|
3726
|
+
has(src, sourceTime, fps) {
|
|
3727
|
+
return this.entries.has(videoFrameCacheKey(src, videoSourceFrame(sourceTime, fps)));
|
|
3728
|
+
}
|
|
3729
|
+
/** Keeps a decoded frame (replacing any with the same key), then evicts to the budget. */
|
|
3730
|
+
put(request, frame) {
|
|
3731
|
+
if (this.destroyed) return void frame.image.close?.();
|
|
3732
|
+
const old = this.entries.get(request.key);
|
|
3733
|
+
if (old) this.drop(old);
|
|
3734
|
+
const bytes = Math.max(1, frame.width * frame.height * 4);
|
|
3735
|
+
this.entries.set(request.key, { key: request.key, time: request.time, bytes, frame });
|
|
3736
|
+
this.bytes += bytes;
|
|
3737
|
+
this.evict();
|
|
3738
|
+
this.emit();
|
|
3739
|
+
}
|
|
3740
|
+
/** Keeps what a ready media element shows now as the frame of `src` at `sourceTime` (async; does nothing if it is cached). */
|
|
3741
|
+
capture(src, sourceTime, fps, time, source) {
|
|
3742
|
+
const frame = videoSourceFrame(sourceTime, fps);
|
|
3743
|
+
const key = videoFrameCacheKey(src, frame);
|
|
3744
|
+
if (this.entries.has(key) || this.inFlight.has(key)) return;
|
|
3745
|
+
this.inFlight.add(key);
|
|
3746
|
+
void (this.options.capture ?? defaultCapture)(source).then((f) => {
|
|
3747
|
+
if (f) this.put({ key, time }, f);
|
|
3748
|
+
}).catch(() => {
|
|
3749
|
+
}).finally(() => this.inFlight.delete(key));
|
|
3750
|
+
}
|
|
3751
|
+
/**
|
|
3752
|
+
* Follows the playhead: protects the prefetch window, evicts to the budget, and queues the
|
|
3753
|
+
* window's missing frames for decoding (nearest first). `srcFor` gives the file a layer plays.
|
|
3754
|
+
*/
|
|
3755
|
+
update(project, time, options = {}) {
|
|
3756
|
+
if (this.destroyed) return;
|
|
3757
|
+
this.direction = options.direction ?? (time < this.playhead && !options.playing ? -1 : 1);
|
|
3758
|
+
this.playhead = time;
|
|
3759
|
+
this.srcFor = options.srcFor;
|
|
3760
|
+
const plan = this.prefetch > 0 ? planVideoPrefetch(project, time, { window: this.prefetch, direction: this.direction, srcFor: options.srcFor }) : videoFrameRequestsAt(project, time, options.srcFor);
|
|
3761
|
+
this.protect = new Set(plan.map((r) => r.key));
|
|
3762
|
+
for (const r of plan) {
|
|
3763
|
+
const e = this.entries.get(r.key);
|
|
3764
|
+
if (e) e.time = r.time;
|
|
3765
|
+
}
|
|
3766
|
+
this.evict();
|
|
3767
|
+
this.generation++;
|
|
3768
|
+
this.queue = this.decode ? plan.filter((r) => !this.entries.has(r.key) && !this.inFlight.has(r.key)) : [];
|
|
3769
|
+
this.pump();
|
|
3770
|
+
}
|
|
3771
|
+
/**
|
|
3772
|
+
* The stretches of the timeline whose video frames are all cached, between `from` and `to` (ms),
|
|
3773
|
+
* for the files the preview last played (its `srcFor`, e.g. proxies) unless `srcFor` is given.
|
|
3774
|
+
* `step` (ms, at least a frame) samples more coarsely.
|
|
3775
|
+
*/
|
|
3776
|
+
cachedRanges(project, options = {}) {
|
|
3777
|
+
const step = Math.max(1e3 / project.fps, options.step ?? 0);
|
|
3778
|
+
const from = Math.max(0, options.from ?? 0);
|
|
3779
|
+
const to = options.to ?? Math.max(...project.tracks.flatMap((t) => t.clips.map((c) => c.start + c.duration)), 0);
|
|
3780
|
+
const out = [];
|
|
3781
|
+
for (let t = from; t < to - 1e-6; t += step) {
|
|
3782
|
+
const requests = videoFrameRequestsAt(project, t, options.srcFor ?? this.srcFor);
|
|
3783
|
+
const cached = requests.length > 0 && requests.every((r) => this.entries.has(r.key));
|
|
3784
|
+
const last = out[out.length - 1];
|
|
3785
|
+
if (!cached) continue;
|
|
3786
|
+
const end = Math.min(to, t + step);
|
|
3787
|
+
if (last && Math.abs(last.end - t) < 1e-6) last.end = end;
|
|
3788
|
+
else out.push({ start: t, end });
|
|
3789
|
+
}
|
|
3790
|
+
return out.map((r) => ({ start: Math.round(r.start * 10) / 10, end: Math.round(r.end * 10) / 10 }));
|
|
3791
|
+
}
|
|
3792
|
+
stats() {
|
|
3793
|
+
return { frames: this.entries.size, bytes: this.bytes, maxBytes: this.maxBytes, hits: this.hits, misses: this.misses, pending: this.inFlight.size + this.queue.length, prefetch: this.prefetch };
|
|
3794
|
+
}
|
|
3795
|
+
/** Called when frames arrive or leave. */
|
|
3796
|
+
subscribe(listener) {
|
|
3797
|
+
this.listeners.add(listener);
|
|
3798
|
+
return () => {
|
|
3799
|
+
this.listeners.delete(listener);
|
|
3800
|
+
};
|
|
3801
|
+
}
|
|
3802
|
+
/** Drops every frame (closing them) and the queue. */
|
|
3803
|
+
clear() {
|
|
3804
|
+
for (const e of [...this.entries.values()]) this.drop(e);
|
|
3805
|
+
this.queue = [];
|
|
3806
|
+
this.generation++;
|
|
3807
|
+
this.emit();
|
|
3808
|
+
}
|
|
3809
|
+
/** Clears and stops decoding for good. */
|
|
3810
|
+
destroy() {
|
|
3811
|
+
this.clear();
|
|
3812
|
+
this.destroyed = true;
|
|
3813
|
+
this.listeners.clear();
|
|
3814
|
+
}
|
|
3815
|
+
pump() {
|
|
3816
|
+
const limit = Math.max(1, this.options.concurrency ?? 2);
|
|
3817
|
+
while (this.decode && this.inFlight.size < limit && this.queue.length) {
|
|
3818
|
+
const r = this.queue.shift();
|
|
3819
|
+
if (this.entries.has(r.key) || this.inFlight.has(r.key)) continue;
|
|
3820
|
+
if (this.bytes >= this.maxBytes && [...this.entries.keys()].every((k) => this.protect.has(k))) {
|
|
3821
|
+
this.queue = [];
|
|
3822
|
+
break;
|
|
3823
|
+
}
|
|
3824
|
+
const generation = this.generation;
|
|
3825
|
+
this.inFlight.add(r.key);
|
|
3826
|
+
this.decode(r.src, r.sourceTime).then(
|
|
3827
|
+
(frame) => {
|
|
3828
|
+
this.inFlight.delete(r.key);
|
|
3829
|
+
if (generation === this.generation || this.bytes + frame.width * frame.height * 4 <= this.maxBytes) this.put(r, frame);
|
|
3830
|
+
else frame.image.close?.();
|
|
3831
|
+
},
|
|
3832
|
+
() => this.inFlight.delete(r.key)
|
|
3833
|
+
).finally(() => this.pump());
|
|
3834
|
+
}
|
|
3835
|
+
}
|
|
3836
|
+
evict() {
|
|
3837
|
+
const drop = videoCacheEvictions([...this.entries.values()], { playhead: this.playhead, maxBytes: this.maxBytes, direction: this.direction, behind: this.options.behind, protect: this.protect });
|
|
3838
|
+
for (const key of drop) this.drop(this.entries.get(key));
|
|
3839
|
+
}
|
|
3840
|
+
drop(e) {
|
|
3841
|
+
this.entries.delete(e.key);
|
|
3842
|
+
this.bytes -= e.bytes;
|
|
3843
|
+
e.frame.image.close?.();
|
|
3844
|
+
}
|
|
3845
|
+
emit() {
|
|
3846
|
+
for (const l of [...this.listeners]) l();
|
|
3847
|
+
}
|
|
3848
|
+
};
|
|
3849
|
+
function createVideoElementDecoder(options = {}) {
|
|
3850
|
+
const videos = /* @__PURE__ */ new Map();
|
|
3851
|
+
const make = options.createVideo ?? (() => {
|
|
3852
|
+
const el = document.createElement("video");
|
|
3853
|
+
el.muted = true;
|
|
3854
|
+
el.preload = "auto";
|
|
3855
|
+
el.crossOrigin = "anonymous";
|
|
3856
|
+
el.playsInline = true;
|
|
3857
|
+
return el;
|
|
3858
|
+
});
|
|
3859
|
+
const timeout = options.timeout ?? 5e3;
|
|
3860
|
+
const once = (el, type) => new Promise((resolve, reject) => {
|
|
3861
|
+
const timer = setTimeout(() => reject(new Error(`The video did not ${type} within ${timeout} ms.`)), timeout);
|
|
3862
|
+
el.addEventListener(type, () => (clearTimeout(timer), resolve()), { once: true });
|
|
3863
|
+
el.addEventListener("error", () => (clearTimeout(timer), reject(new Error("The video could not be loaded."))), { once: true });
|
|
3864
|
+
});
|
|
3865
|
+
return (src, sourceTime) => {
|
|
3866
|
+
let v = videos.get(src);
|
|
3867
|
+
if (!v) {
|
|
3868
|
+
const el2 = make();
|
|
3869
|
+
el2.src = src;
|
|
3870
|
+
v = { el: el2, chain: el2.readyState >= 1 ? Promise.resolve() : once(el2, "loadedmetadata") };
|
|
3871
|
+
videos.set(src, v);
|
|
3872
|
+
}
|
|
3873
|
+
const { el } = v;
|
|
3874
|
+
const next = v.chain.then(async () => {
|
|
3875
|
+
const presented = "requestVideoFrameCallback" in el ? new Promise((r) => el.requestVideoFrameCallback(() => r())) : null;
|
|
3876
|
+
const seeked = once(el, "seeked");
|
|
3877
|
+
el.currentTime = sourceTime / 1e3;
|
|
3878
|
+
await seeked;
|
|
3879
|
+
if (presented) await Promise.race([presented, new Promise((r) => setTimeout(r, 100))]);
|
|
3880
|
+
const image = await createImageBitmap(el);
|
|
3881
|
+
return { image, width: image.width, height: image.height };
|
|
3882
|
+
});
|
|
3883
|
+
v.chain = next.catch(() => {
|
|
3884
|
+
});
|
|
3885
|
+
return next;
|
|
3886
|
+
};
|
|
3540
3887
|
}
|
|
3541
3888
|
|
|
3542
3889
|
// src/video-editor/preview.ts
|
|
@@ -3550,6 +3897,9 @@ var VideoPreviewEngine = class {
|
|
|
3550
3897
|
this.lutCache = options.luts !== void 0 ? options.luts : source.luts ?? null;
|
|
3551
3898
|
this.unsubscribeLuts = this.lutCache ? this.lutCache.subscribe(() => this.redraw()) : () => {
|
|
3552
3899
|
};
|
|
3900
|
+
this.frameCache = options.frameCache !== void 0 ? options.frameCache : source.frameCache ?? null;
|
|
3901
|
+
this.unsubscribeFrames = this.frameCache ? this.frameCache.subscribe(() => this.redraw()) : () => {
|
|
3902
|
+
};
|
|
3553
3903
|
const pixels = options.pixels !== void 0 ? options.pixels : source.previewPixels;
|
|
3554
3904
|
this.detachPixels = pixels ? pixels.attach(() => this.readPixels()) : () => {
|
|
3555
3905
|
};
|
|
@@ -3572,6 +3922,11 @@ var VideoPreviewEngine = class {
|
|
|
3572
3922
|
drawn = null;
|
|
3573
3923
|
complete = true;
|
|
3574
3924
|
lutCache;
|
|
3925
|
+
frameCache;
|
|
3926
|
+
unsubscribeFrames;
|
|
3927
|
+
srcFor = (layer) => layer.src;
|
|
3928
|
+
playing = false;
|
|
3929
|
+
fps = 30;
|
|
3575
3930
|
unsubscribeLuts;
|
|
3576
3931
|
scratchCanvas;
|
|
3577
3932
|
/** The canvas's pixels and the frame they show, or `null` when its context cannot be read. */
|
|
@@ -3622,8 +3977,15 @@ var VideoPreviewEngine = class {
|
|
|
3622
3977
|
/** Brings the media elements in step with the editor and draws the frame. */
|
|
3623
3978
|
update() {
|
|
3624
3979
|
if (this.destroyed) return;
|
|
3625
|
-
const
|
|
3980
|
+
const state = this.source.getState();
|
|
3981
|
+
const { project, time, playing } = state;
|
|
3626
3982
|
const frame = videoFrameAt(project, time);
|
|
3983
|
+
const scale = resolveVideoPreviewScale(this.options.previewScale ?? state.previewScale, this.resolutionFor(project.width));
|
|
3984
|
+
const assets = new Map(project.assets.map((a) => [a.id, a]));
|
|
3985
|
+
this.srcFor = (layer) => videoPreviewSrc(assets.get(layer.asset ?? ""), scale, layer.src);
|
|
3986
|
+
this.playing = playing;
|
|
3987
|
+
this.fps = project.fps;
|
|
3988
|
+
this.frameCache?.update(project, time, { playing, srcFor: this.srcFor });
|
|
3627
3989
|
const tolerance = (this.options.tolerance ?? 200) / 1e3;
|
|
3628
3990
|
const active = /* @__PURE__ */ new Set();
|
|
3629
3991
|
const sync = (clip, src, kind, sourceTime, speed, gain) => {
|
|
@@ -3650,7 +4012,7 @@ var VideoPreviewEngine = class {
|
|
|
3650
4012
|
for (const layer of frame.layers) {
|
|
3651
4013
|
if (layer.type !== "media" || layer.assetKind !== "video" || !layer.src) continue;
|
|
3652
4014
|
const sound = sounding.get(layer.clip);
|
|
3653
|
-
sync(layer.clip, layer
|
|
4015
|
+
sync(layer.clip, this.srcFor(layer), "video", layer.sourceTime ?? 0, sound?.speed ?? 1, sound ? sound.gain : null);
|
|
3654
4016
|
sounding.delete(layer.clip);
|
|
3655
4017
|
}
|
|
3656
4018
|
for (const a of sounding.values()) sync(a.clip, a.src, "audio", a.sourceTime, a.speed, a.gain);
|
|
@@ -3671,15 +4033,29 @@ var VideoPreviewEngine = class {
|
|
|
3671
4033
|
return img.complete ? img : null;
|
|
3672
4034
|
}
|
|
3673
4035
|
const entry = this.media.get(layer.clip);
|
|
3674
|
-
|
|
4036
|
+
const ready = !!entry && entry.media.readyState >= 2;
|
|
4037
|
+
const cache = this.frameCache;
|
|
4038
|
+
const src = entry?.src ?? this.srcFor(layer);
|
|
4039
|
+
const sourceTime = layer.sourceTime ?? 0;
|
|
4040
|
+
if (cache && src && (!this.playing || !ready || entry.media.seeking)) {
|
|
4041
|
+
const hit = cache.get(src, sourceTime, this.fps);
|
|
4042
|
+
if (hit) return hit;
|
|
4043
|
+
}
|
|
4044
|
+
if (!ready) return null;
|
|
3675
4045
|
if (entry.media.seeking) this.complete = false;
|
|
4046
|
+
else if (cache && !this.playing) cache.capture(entry.src, sourceTime, this.fps, this.source.getState().time, entry.media);
|
|
3676
4047
|
return entry.media;
|
|
3677
4048
|
};
|
|
4049
|
+
/** Canvas pixels per frame pixel: the option, else the displayed width at the device pixel ratio (at most 1). */
|
|
4050
|
+
resolutionFor(frameWidth) {
|
|
4051
|
+
if (this.options.resolution !== void 0) return this.options.resolution;
|
|
4052
|
+
const shown = this.canvas.clientWidth || frameWidth;
|
|
4053
|
+
const dpr = typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1;
|
|
4054
|
+
return Math.min(1, shown * dpr / frameWidth);
|
|
4055
|
+
}
|
|
3678
4056
|
draw(frame) {
|
|
3679
4057
|
if (!this.ctx) return;
|
|
3680
|
-
const
|
|
3681
|
-
const dpr = typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1;
|
|
3682
|
-
const resolution = this.options.resolution ?? Math.min(1, shown * dpr / frame.width);
|
|
4058
|
+
const resolution = this.resolutionFor(frame.width);
|
|
3683
4059
|
const w = Math.max(1, Math.round(frame.width * resolution));
|
|
3684
4060
|
const h = Math.max(1, Math.round(frame.height * resolution));
|
|
3685
4061
|
if (this.canvas.width !== w) this.canvas.width = w;
|
|
@@ -3738,6 +4114,7 @@ var VideoPreviewEngine = class {
|
|
|
3738
4114
|
this.destroyed = true;
|
|
3739
4115
|
this.detachPixels();
|
|
3740
4116
|
this.unsubscribeLuts();
|
|
4117
|
+
this.unsubscribeFrames();
|
|
3741
4118
|
this.filterHost?.remove();
|
|
3742
4119
|
this.filterHost = null;
|
|
3743
4120
|
this.filters.clear();
|
|
@@ -3757,10 +4134,13 @@ var VideoEditorController = class {
|
|
|
3757
4134
|
previewPixels = new VideoPreviewPixels();
|
|
3758
4135
|
/** Grades' LUTs, loaded on demand for the preview, SVG frames and agents. */
|
|
3759
4136
|
luts;
|
|
4137
|
+
/** The host's frame cache, if it gave one (the preview fills and reads it). */
|
|
4138
|
+
frameCache;
|
|
3760
4139
|
project;
|
|
3761
4140
|
issues;
|
|
3762
4141
|
selection = [];
|
|
3763
4142
|
readOnly;
|
|
4143
|
+
previewScale;
|
|
3764
4144
|
error = null;
|
|
3765
4145
|
revision = 0;
|
|
3766
4146
|
applying = false;
|
|
@@ -3772,9 +4152,11 @@ var VideoEditorController = class {
|
|
|
3772
4152
|
this.history = options.history ?? new HistoryManager();
|
|
3773
4153
|
this.audioPeaks = options.audioPeaks instanceof VideoAudioPeaksCache ? options.audioPeaks : new VideoAudioPeaksCache(options.audioPeaks);
|
|
3774
4154
|
this.luts = options.luts instanceof VideoLutCache ? options.luts : new VideoLutCache(options.luts);
|
|
4155
|
+
this.frameCache = options.frameCache ?? null;
|
|
3775
4156
|
this.project = options.project ? parseVideoProject(options.project) : createVideoProject();
|
|
3776
4157
|
this.issues = checkVideoProject(this.project);
|
|
3777
4158
|
this.readOnly = options.readOnly ?? false;
|
|
4159
|
+
this.previewScale = checkPreviewScale(options.previewScale ?? "auto");
|
|
3778
4160
|
this.onChange = options.onChange;
|
|
3779
4161
|
this.player = new AnimationPlayer({ clock: options.clock, duration: () => videoProjectDuration(this.project), loop: "none" });
|
|
3780
4162
|
this.state = this.computeState();
|
|
@@ -3824,6 +4206,7 @@ var VideoEditorController = class {
|
|
|
3824
4206
|
canRedo: h.canRedo,
|
|
3825
4207
|
undoLabel: h.undoName,
|
|
3826
4208
|
redoLabel: h.redoName,
|
|
4209
|
+
previewScale: this.previewScale,
|
|
3827
4210
|
revision: this.revision
|
|
3828
4211
|
};
|
|
3829
4212
|
}
|
|
@@ -3913,6 +4296,17 @@ var VideoEditorController = class {
|
|
|
3913
4296
|
this.readOnly = readOnly;
|
|
3914
4297
|
this.notify();
|
|
3915
4298
|
}
|
|
4299
|
+
/**
|
|
4300
|
+
* Sets how large a picture the preview plays: `full` (the originals), a fraction such as 0.5
|
|
4301
|
+
* (the smallest proxy at least that large, else the original) or `auto` (as large as the canvas
|
|
4302
|
+
* shows). A view setting, not part of the project: no undo step.
|
|
4303
|
+
*/
|
|
4304
|
+
setPreviewScale(scale) {
|
|
4305
|
+
const next = checkPreviewScale(scale);
|
|
4306
|
+
if (next === this.previewScale) return;
|
|
4307
|
+
this.previewScale = next;
|
|
4308
|
+
this.notify();
|
|
4309
|
+
}
|
|
3916
4310
|
/** Selects clips (unknown ids are dropped). `add` toggles them into the current selection. */
|
|
3917
4311
|
select(clips, options = {}) {
|
|
3918
4312
|
const known = clips?.filter((id) => findVideoClip(this.project, id)) ?? [];
|
|
@@ -3997,6 +4391,11 @@ var VideoEditorController = class {
|
|
|
3997
4391
|
return toVideoEdl(this.project, options);
|
|
3998
4392
|
}
|
|
3999
4393
|
};
|
|
4394
|
+
function checkPreviewScale(scale) {
|
|
4395
|
+
if (scale === "auto" || scale === "full") return scale;
|
|
4396
|
+
if (typeof scale !== "number" || !(scale > 0 && scale <= 1)) throw new Error(`A preview scale is "auto", "full" or a fraction above 0 up to 1 (0.5 is half size), not ${JSON.stringify(scale)}.`);
|
|
4397
|
+
return scale === 1 ? "full" : scale;
|
|
4398
|
+
}
|
|
4000
4399
|
|
|
4001
4400
|
// src/video-editor/ui.ts
|
|
4002
4401
|
var STEPS = [40, 100, 200, 500, 1e3, 2e3, 5e3, 1e4, 15e3, 3e4, 6e4, 12e4, 3e5, 6e5];
|
|
@@ -4466,7 +4865,7 @@ function createVideoEditorToolProvider(controller, options = {}) {
|
|
|
4466
4865
|
handler: () => {
|
|
4467
4866
|
const state = controller.getState();
|
|
4468
4867
|
const reason = readOnlyReason();
|
|
4469
|
-
return { ...describeVideoProject(state.project), playhead: state.time, selection: state.selection, issues: issueSummary(state.issues), editable: !reason, ...reason ? { readOnlyReason: reason } : {} };
|
|
4868
|
+
return { ...describeVideoProject(state.project), playhead: state.time, previewScale: state.previewScale, selection: state.selection, issues: issueSummary(state.issues), editable: !reason, ...reason ? { readOnlyReason: reason } : {} };
|
|
4470
4869
|
}
|
|
4471
4870
|
};
|
|
4472
4871
|
const getFrame = {
|
|
@@ -4634,7 +5033,7 @@ function createVideoEditorToolProvider(controller, options = {}) {
|
|
|
4634
5033
|
name: "update_video_asset",
|
|
4635
5034
|
title: "Change media details",
|
|
4636
5035
|
description: "Changes an asset's src, name, duration, size, sound or poster (e.g. relinking moved media). Clips that now run past its end are refused. Returns the asset.",
|
|
4637
|
-
inputSchema: object({ asset: ASSET_ID, set: object(ASSET_FIELDS, [], "Fields to replace."), remove: { type: "array", items: { type: "string", enum: ["name", "duration", "width", "height", "hasAudio", "poster", "transcript"] }, description: "Fields to clear (transcript removes its words)." } }, ["asset"]),
|
|
5036
|
+
inputSchema: object({ asset: ASSET_ID, set: object(ASSET_FIELDS, [], "Fields to replace."), remove: { type: "array", items: { type: "string", enum: ["name", "duration", "width", "height", "hasAudio", "poster", "transcript", "proxies"] }, description: "Fields to clear (transcript removes its words, proxies its proxies)." } }, ["asset"]),
|
|
4638
5037
|
annotations: { title: "Change media details", ...WRITE_IDEMPOTENT },
|
|
4639
5038
|
handler: ({ asset, set, remove }) => apply({ op: "update_asset", asset, set, remove }, "Change media", (r) => ({ asset: videoAssetOut(r.project.assets.find((a) => a.id === asset)) }))
|
|
4640
5039
|
};
|
|
@@ -4832,6 +5231,79 @@ function createVideoEditorToolProvider(controller, options = {}) {
|
|
|
4832
5231
|
return apply(videoLutEdits(p, withLut, null), "Remove LUT", (r) => ({ clips: clips.map((id) => ({ id, grade: findVideoClip(r.project, id)?.clip.grade ?? null })) }));
|
|
4833
5232
|
}
|
|
4834
5233
|
};
|
|
5234
|
+
const PROXY_SCALE = { type: "number", exclusiveMinimum: 0, exclusiveMaximum: 1, description: "Proxy size as a fraction of the original: 0.5 (half width and height, the default) or 0.25 for very large footage." };
|
|
5235
|
+
const planProxies = {
|
|
5236
|
+
name: "plan_video_proxies",
|
|
5237
|
+
title: "Plan proxy files",
|
|
5238
|
+
description: 'Returns the ffmpeg commands that make smaller proxy copies of video assets for smooth preview playback, without changing anything: one per video asset (or each of "assets") that has no proxy at that scale yet, each with { asset, args, command, proxy }. Proxies are scaled, H.264 with a keyframe every few frames so scrubbing is quick, and keep the sound. After the host runs a command, attach its "proxy" with set_video_proxies. The export always uses the originals.',
|
|
5239
|
+
inputSchema: object({
|
|
5240
|
+
scale: PROXY_SCALE,
|
|
5241
|
+
assets: { type: "array", items: ASSET_ID, minItems: 1, uniqueItems: true, description: "Only these video assets (default: every video asset without a proxy at this scale)." },
|
|
5242
|
+
gop: { type: "integer", minimum: 1, maximum: 300, description: "Frames between keyframes (default 12): fewer makes seeking cheaper and files larger." },
|
|
5243
|
+
crf: { type: "integer", minimum: 0, maximum: 51, description: "x264 quality, lower is better (default 28: proxies are for editing)." }
|
|
5244
|
+
}),
|
|
5245
|
+
annotations: { title: "Plan proxy files", ...READ },
|
|
5246
|
+
handler: ({ scale, assets, gop, crf }) => {
|
|
5247
|
+
const commands = videoProxyPlan(project(), { scale, assets, gop, crf });
|
|
5248
|
+
return { scale: scale ?? 0.5, commands, ...commands.length ? {} : { note: "Every video asset already has a proxy at this scale." } };
|
|
5249
|
+
}
|
|
5250
|
+
};
|
|
5251
|
+
const setProxies = {
|
|
5252
|
+
name: "set_video_proxies",
|
|
5253
|
+
title: "Attach proxies",
|
|
5254
|
+
description: "Sets a video asset's proxies, the smaller copies the preview plays instead of the original (one undo step); an empty list removes them. Each proxy must match the original's length and timing (plan_video_proxies makes such files). The export, EDL and source times always use the original. Returns the asset.",
|
|
5255
|
+
inputSchema: object(
|
|
5256
|
+
{
|
|
5257
|
+
asset: { ...ASSET_ID, description: "A video asset id from get_video_project." },
|
|
5258
|
+
proxies: {
|
|
5259
|
+
type: "array",
|
|
5260
|
+
maxItems: 8,
|
|
5261
|
+
description: "Its proxies, one per scale (empty removes them all).",
|
|
5262
|
+
items: object({ scale: PROXY_SCALE, src: { type: "string", minLength: 1, description: "Where the preview loads the proxy from (URL or path)." }, width: { type: "number", exclusiveMinimum: 0, description: "Its width in pixels." }, height: { type: "number", exclusiveMinimum: 0, description: "Its height in pixels." } }, ["scale", "src"], "One proxy.")
|
|
5263
|
+
}
|
|
5264
|
+
},
|
|
5265
|
+
["asset", "proxies"]
|
|
5266
|
+
),
|
|
5267
|
+
annotations: { title: "Attach proxies", ...WRITE_IDEMPOTENT },
|
|
5268
|
+
handler: ({ asset, proxies }) => {
|
|
5269
|
+
const a = project().assets.find((x) => x.id === asset);
|
|
5270
|
+
if (!a) throw new Error(`There is no asset "${asset}"; the assets are ${project().assets.map((x) => `"${x.id}"`).join(", ") || "none"}.`);
|
|
5271
|
+
if (a.kind !== "video") throw new Error(`Asset "${asset}" is ${a.kind === "image" ? "an image" : "audio"}; only video assets have proxies.`);
|
|
5272
|
+
const edit = proxies.length ? { op: "update_asset", asset, set: { proxies } } : { op: "update_asset", asset, remove: ["proxies"] };
|
|
5273
|
+
return apply(edit, proxies.length ? "Attach proxies" : "Remove proxies", (r) => ({ asset: videoAssetOut(r.project.assets.find((x) => x.id === asset)) }));
|
|
5274
|
+
}
|
|
5275
|
+
};
|
|
5276
|
+
const setPreviewScale = {
|
|
5277
|
+
name: "set_video_preview_scale",
|
|
5278
|
+
title: "Set the preview size",
|
|
5279
|
+
description: 'Sets how large a picture the preview plays, without changing the project (no undo step): "full" plays the originals, a fraction (0.5, 0.25) plays the smallest proxy at least that large (or the original where there is none), "auto" (the default) picks by the size the preview is shown at. Use a proxy scale when playback stutters on large footage. Returns the setting and, for each video asset, what the preview plays at a fixed scale.',
|
|
5280
|
+
inputSchema: object(
|
|
5281
|
+
{ scale: { anyOf: [{ type: "string", enum: ["auto", "full"] }, { type: "number", exclusiveMinimum: 0, maximum: 1 }], description: '"auto", "full", or a fraction above 0 up to 1 (0.5 is half size).' } },
|
|
5282
|
+
["scale"]
|
|
5283
|
+
),
|
|
5284
|
+
annotations: { title: "Set the preview size", ...WRITE_IDEMPOTENT },
|
|
5285
|
+
handler: ({ scale }) => {
|
|
5286
|
+
controller.setPreviewScale(scale);
|
|
5287
|
+
const s = controller.getState().previewScale;
|
|
5288
|
+
const fixed = s === "full" ? 1 : typeof s === "number" ? s : null;
|
|
5289
|
+
const plays = fixed === null ? void 0 : project().assets.filter((a) => a.kind === "video").map((a) => ({ asset: a.id, src: videoAssetProxy(a, fixed)?.src ?? a.src, proxy: !!videoAssetProxy(a, fixed) }));
|
|
5290
|
+
return { previewScale: s, ...plays ? { plays } : { note: "auto picks the smallest proxy at least as large as the preview canvas shows, so it depends on the preview's size." } };
|
|
5291
|
+
}
|
|
5292
|
+
};
|
|
5293
|
+
const previewCache = {
|
|
5294
|
+
name: "get_video_preview_cache",
|
|
5295
|
+
title: "Read the preview's frame cache",
|
|
5296
|
+
description: `Reports the preview's cache of decoded frames, without changing anything: whether the host has one, how many frames and bytes it holds against its budget, hits and misses, frames still decoding, how far ahead it prefetches (ms), and the stretches of the timeline between "from" and "to" whose frames are all cached (these play and scrub without waiting). Use it to tell whether stuttering is the cache filling, and before asking the person to review a passage. With no cache, says so.`,
|
|
5297
|
+
inputSchema: object({ from: MS("Start of the stretch to report cached ranges for (default 0)"), to: MS("End of it (default: the end of the project)") }),
|
|
5298
|
+
annotations: { title: "Read the preview's frame cache", ...READ },
|
|
5299
|
+
handler: ({ from, to }) => {
|
|
5300
|
+
const cache = controller.frameCache;
|
|
5301
|
+
if (!cache) return { enabled: false, reason: "The host has not given this editor a frame cache (VideoEditorController frameCache), so the preview decodes each frame as it plays." };
|
|
5302
|
+
if (from !== void 0 && to !== void 0 && !(to > from)) throw new Error(`"to" (${to} ms) must be after "from" (${from} ms).`);
|
|
5303
|
+
const ranges = cache.cachedRanges(project(), { from, to });
|
|
5304
|
+
return { enabled: true, ...cache.stats(), cachedRanges: ranges };
|
|
5305
|
+
}
|
|
5306
|
+
};
|
|
4835
5307
|
const TRANSCRIPT_TRACK = { ...TRACK_ID, description: "Track whose words to read (default: the first audio track with a transcribed clip, else the first video track with one)." };
|
|
4836
5308
|
const RANGE_OUT = (r) => ({ from: round(r.from), to: round(r.to), text: r.text });
|
|
4837
5309
|
const timelineWords = (track) => {
|
|
@@ -5273,8 +5745,8 @@ function createVideoEditorToolProvider(controller, options = {}) {
|
|
|
5273
5745
|
return { time: t, timecode: formatVideoTimecode(t, p.fps), width: pixels.width, height: pixels.height, ...summarizeVideoScopes(pixels, { bins, columns }) };
|
|
5274
5746
|
}
|
|
5275
5747
|
};
|
|
5276
|
-
const reads = [getProject, getFrame, getTranscript, findText, findFillers, findPauses, findGaps, checkProject, renderFrame, getScopes, exportFfmpeg, exportLoudnessMeasure, exportEdl, audioLevels, findSilences, seek];
|
|
5277
|
-
const writes = [setSettings, addAsset, updateAsset, removeAsset, addTrack, updateTrack, removeTrack, moveTrack, addClip, moveClip, trimClip, splitClip, deleteClips, updateClip, setTransition, setSpeedRamp, addFreeze, removeRamp, setTranscript, removeRanges, applyGradePreset, copyGrade, setLut, removeLut, setKeyframes, removeKeyframes, slipClip, rollEdit, slideClip, liftRange, extractRange, splitAt, closeGaps, addMarker, removeMarker, editVideo, undo, redo];
|
|
5748
|
+
const reads = [getProject, getFrame, getTranscript, findText, findFillers, findPauses, findGaps, checkProject, renderFrame, getScopes, previewCache, planProxies, exportFfmpeg, exportLoudnessMeasure, exportEdl, audioLevels, findSilences, seek, setPreviewScale];
|
|
5749
|
+
const writes = [setSettings, addAsset, updateAsset, removeAsset, setProxies, addTrack, updateTrack, removeTrack, moveTrack, addClip, moveClip, trimClip, splitClip, deleteClips, updateClip, setTransition, setSpeedRamp, addFreeze, removeRamp, setTranscript, removeRanges, applyGradePreset, copyGrade, setLut, removeLut, setKeyframes, removeKeyframes, slipClip, rollEdit, slideClip, liftRange, extractRange, splitAt, closeGaps, addMarker, removeMarker, editVideo, undo, redo];
|
|
5278
5750
|
return {
|
|
5279
5751
|
id: options.id ?? "video",
|
|
5280
5752
|
namespace: options.namespace,
|
|
@@ -5315,6 +5787,8 @@ export {
|
|
|
5315
5787
|
VIDEO_LUT_MAX_SIZE,
|
|
5316
5788
|
VIDEO_PEAK_BLOCK,
|
|
5317
5789
|
VIDEO_PLACEMENT_MODES,
|
|
5790
|
+
VIDEO_PREVIEW_SCALE_OPTIONS,
|
|
5791
|
+
VIDEO_PROXY_SCALES,
|
|
5318
5792
|
VIDEO_SCOPE_KINDS,
|
|
5319
5793
|
VIDEO_TIME_EPSILON,
|
|
5320
5794
|
VIDEO_TRANSFORM_PROPERTIES,
|
|
@@ -5324,6 +5798,7 @@ export {
|
|
|
5324
5798
|
VIDEO_WIPE_DIRECTIONS,
|
|
5325
5799
|
VideoAudioPeaksCache,
|
|
5326
5800
|
VideoEditorController,
|
|
5801
|
+
VideoFrameCache,
|
|
5327
5802
|
VideoLutCache,
|
|
5328
5803
|
VideoPreviewEngine,
|
|
5329
5804
|
VideoPreviewPixels,
|
|
@@ -5336,6 +5811,7 @@ export {
|
|
|
5336
5811
|
checkVideoProject,
|
|
5337
5812
|
checkVideoTimeRemap,
|
|
5338
5813
|
createVideoEditorToolProvider,
|
|
5814
|
+
createVideoElementDecoder,
|
|
5339
5815
|
createVideoProject,
|
|
5340
5816
|
decodeVideoAudioPeaks,
|
|
5341
5817
|
describeVideoProject,
|
|
@@ -5359,6 +5835,7 @@ export {
|
|
|
5359
5835
|
normalizeVideoWord,
|
|
5360
5836
|
parseVideoAnimatable,
|
|
5361
5837
|
parseVideoAsset,
|
|
5838
|
+
parseVideoAssetProxies,
|
|
5362
5839
|
parseVideoClip,
|
|
5363
5840
|
parseVideoClipAnimation,
|
|
5364
5841
|
parseVideoCubeLut,
|
|
@@ -5367,6 +5844,7 @@ export {
|
|
|
5367
5844
|
parseVideoKeyframes,
|
|
5368
5845
|
parseVideoLoudnessMeasurement,
|
|
5369
5846
|
parseVideoMarker,
|
|
5847
|
+
parseVideoPreviewScale,
|
|
5370
5848
|
parseVideoProject,
|
|
5371
5849
|
parseVideoTimeRemap,
|
|
5372
5850
|
parseVideoTrack,
|
|
@@ -5376,9 +5854,11 @@ export {
|
|
|
5376
5854
|
parseVideoTransition,
|
|
5377
5855
|
placeVideoRect,
|
|
5378
5856
|
planVideoClipDrag,
|
|
5857
|
+
planVideoPrefetch,
|
|
5379
5858
|
probeVideoMedia,
|
|
5380
5859
|
readVideoScopesPanel,
|
|
5381
5860
|
removeVideoKey,
|
|
5861
|
+
resolveVideoPreviewScale,
|
|
5382
5862
|
retimeVideoAnimatable,
|
|
5383
5863
|
setVideoKey,
|
|
5384
5864
|
snapToVideoFrame,
|
|
@@ -5394,10 +5874,12 @@ export {
|
|
|
5394
5874
|
videoAssetHasSound,
|
|
5395
5875
|
videoAssetKindOf,
|
|
5396
5876
|
videoAssetMap,
|
|
5877
|
+
videoAssetProxy,
|
|
5397
5878
|
videoAudioLevels,
|
|
5398
5879
|
videoAudioPeakRange,
|
|
5399
5880
|
videoAudioPeakSpans,
|
|
5400
5881
|
videoAudioPeaksDuration,
|
|
5882
|
+
videoCacheEvictions,
|
|
5401
5883
|
videoChroma,
|
|
5402
5884
|
videoClipAfter,
|
|
5403
5885
|
videoClipAnimated,
|
|
@@ -5428,7 +5910,9 @@ export {
|
|
|
5428
5910
|
videoExposureStats,
|
|
5429
5911
|
videoFadeCurveGain,
|
|
5430
5912
|
videoFrameAt,
|
|
5913
|
+
videoFrameCacheKey,
|
|
5431
5914
|
videoFrameDuration,
|
|
5915
|
+
videoFrameRequestsAt,
|
|
5432
5916
|
videoFrameToSvg,
|
|
5433
5917
|
videoFrames,
|
|
5434
5918
|
videoFreezeFrameEdit,
|
|
@@ -5462,13 +5946,21 @@ export {
|
|
|
5462
5946
|
videoLutToCube,
|
|
5463
5947
|
videoOverlap,
|
|
5464
5948
|
videoPeakDb,
|
|
5949
|
+
videoPreviewSrc,
|
|
5465
5950
|
videoProjectDuration,
|
|
5951
|
+
videoProjectHasProxies,
|
|
5466
5952
|
videoProjectSvg,
|
|
5953
|
+
videoProxyFfmpeg,
|
|
5954
|
+
videoProxyName,
|
|
5955
|
+
videoProxyPlan,
|
|
5467
5956
|
videoRemoveRangesEdits,
|
|
5468
5957
|
videoRulerTicks,
|
|
5469
5958
|
videoScopeImage,
|
|
5959
|
+
videoShellQuote,
|
|
5470
5960
|
videoSlowMotionRamp,
|
|
5471
5961
|
videoSnapPoints,
|
|
5962
|
+
videoSourceFrame,
|
|
5963
|
+
videoSourceFrameTime,
|
|
5472
5964
|
videoSpeedRampEdit,
|
|
5473
5965
|
videoSpeedRampKeys,
|
|
5474
5966
|
videoTextStyle,
|