UNPKG

@webav/av-cliper

Version:

WebCodecs-based, combine video, audio, images, text, with animation support 基于 WebCodecs 合成 视频、音频、图片、文字,支持动画

1,667 lines (1,666 loc) 87.8 kB
var vi = Object.defineProperty; var Je = (s) => { throw TypeError(s); }; var Si = (s, t, e) => t in s ? vi(s, t, { enumerable: !0, configurable: !0, writable: !0, value: e }) : s[t] = e; var S = (s, t, e) => Si(s, typeof t != "symbol" ? t + "" : t, e), Fe = (s, t, e) => t.has(s) || Je("Cannot " + e); var n = (s, t, e) => (Fe(s, t, "read from private field"), e ? e.call(s) : t.get(s)), d = (s, t, e) => t.has(s) ? Je("Cannot add the same private member more than once") : t instanceof WeakSet ? t.add(s) : t.set(s, e), h = (s, t, e, i) => (Fe(s, t, "write to private field"), i ? i.call(s, e) : t.set(s, e), e), B = (s, t, e) => (Fe(s, t, "access private method"), e); import wt from "@webav/mp4box.js"; import { workerTimer as Ti, Log as A, autoReadStream as ze, file2stream as oi, EventTool as Ve, recodemux as Ai } from "@webav/internal-utils"; import { Log as _n } from "@webav/internal-utils"; import * as ki from "wave-resampler"; import { tmpfile as Ee, write as Pe } from "opfs-tools"; function Ii(s) { return document.createElement(s); } function Fi(s) { var t = "", e = new Uint8Array(s), i = e.byteLength; for (let r = 0; r < i; r++) t += String.fromCharCode(e[r]); return window.btoa(t); } async function Ri(s, t, e = {}) { var f; const i = Ii("pre"); i.style.cssText = `margin: 0; ${t}; position: fixed;`, i.textContent = s, document.body.appendChild(i), (f = e.onCreated) == null || f.call(e, i); const { width: r, height: a } = i.getBoundingClientRect(); i.remove(); const o = new Image(); o.width = r, o.height = a; const c = e.font == null ? "" : ` @font-face { font-family: '${e.font.name}'; src: url('data:font/woff2;base64,${Fi(await (await fetch(e.font.url)).arrayBuffer())}') format('woff2'); } `, l = ` <svg xmlns="http://www.w3.org/2000/svg" width="${r}" height="${a}"> <style> ${c} </style> <foreignObject width="100%" height="100%"> <div xmlns="http://www.w3.org/1999/xhtml">${i.outerHTML}</div> </foreignObject> </svg> `.replace(/\t/g, "").replace(/#/g, "%23"); return o.src = `data:image/svg+xml;charset=utf-8,${l}`, await new Promise((u) => { o.onload = u; }), o; } async function In(s, t, e = {}) { const i = await Ri(s, t, e), r = new OffscreenCanvas(i.width, i.height), a = r.getContext("2d"); return a == null || a.drawImage(i, 0, 0, i.width, i.height), await createImageBitmap(r); } function Ei(s) { const t = new Float32Array( s.map((i) => i.length).reduce((i, r) => i + r) ); let e = 0; for (const i of s) t.set(i, e), e += i.length; return t; } function Pi(s) { const t = []; for (let e = 0; e < s.length; e += 1) for (let i = 0; i < s[e].length; i += 1) t[i] == null && (t[i] = []), t[i].push(s[e][i]); return t.map(Ei); } function ci(s) { if (s.format === "f32-planar") { const t = []; for (let e = 0; e < s.numberOfChannels; e += 1) { const i = s.allocationSize({ planeIndex: e }), r = new ArrayBuffer(i); s.copyTo(r, { planeIndex: e }), t.push(new Float32Array(r)); } return t; } else if (s.format === "f32") { const t = new ArrayBuffer(s.allocationSize({ planeIndex: 0 })); return s.copyTo(t, { planeIndex: 0 }), Bi(new Float32Array(t), s.numberOfChannels); } else if (s.format === "s16") { const t = new ArrayBuffer(s.allocationSize({ planeIndex: 0 })); return s.copyTo(t, { planeIndex: 0 }), Di(new Int16Array(t), s.numberOfChannels); } throw Error("Unsupported audio data format"); } function Di(s, t) { const e = s.length / t, i = Array.from( { length: t }, () => new Float32Array(e) ); for (let r = 0; r < e; r++) for (let a = 0; a < t; a++) { const o = s[r * t + a]; i[a][r] = o / 32768; } return i; } function Bi(s, t) { const e = s.length / t, i = Array.from( { length: t }, () => new Float32Array(e) ); for (let r = 0; r < e; r++) for (let a = 0; a < t; a++) i[a][r] = s[r * t + a]; return i; } function Le(s) { return Array(s.numberOfChannels).fill(0).map((t, e) => s.getChannelData(e)); } async function _i(s, t) { var o; const e = { type: t, data: s }, i = new ImageDecoder(e); await Promise.all([i.completed, i.tracks.ready]); let r = ((o = i.tracks.selectedTrack) == null ? void 0 : o.frameCount) ?? 1; const a = []; for (let c = 0; c < r; c += 1) a.push((await i.decode({ frameIndex: c })).image); return a; } function Ye(s) { var i, r; const t = Math.max(...s.map((a) => { var o; return ((o = a[0]) == null ? void 0 : o.length) ?? 0; })), e = new Float32Array(t * 2); for (let a = 0; a < t; a++) { let o = 0, c = 0; for (let l = 0; l < s.length; l++) { const f = ((i = s[l][0]) == null ? void 0 : i[a]) ?? 0, u = ((r = s[l][1]) == null ? void 0 : r[a]) ?? f; o += f, c += u; } e[a] = o, e[a + t] = c; } return e; } async function Oi(s, t, e) { const i = s.length, r = Array(e.chanCount).fill(0).map(() => new Float32Array(0)); if (i === 0) return r; const a = Math.max(...s.map((f) => f.length)); if (a === 0) return r; if (globalThis.OfflineAudioContext == null) return s.map( (f) => new Float32Array( ki.resample(f, t, e.rate, { method: "sinc", LPF: !1 }) ) ); const o = new globalThis.OfflineAudioContext( e.chanCount, a * e.rate / t, e.rate ), c = o.createBufferSource(), l = o.createBuffer(i, a, t); return s.forEach((f, u) => l.copyToChannel(f, u)), c.buffer = l, c.connect(o.destination), c.start(), Le(await o.startRendering()); } function Ue(s) { return new Promise((t) => { const e = Ti(() => { e(), t(); }, s); }); } function De(s, t, e) { const i = e - t, r = new Float32Array(i); let a = 0; for (; a < i; ) r[a] = s[(t + a) % s.length], a += 1; return r; } function li(s, t) { const e = Math.floor(s.length / t), i = new Float32Array(e); for (let r = 0; r < e; r++) { const a = r * t, o = Math.floor(a), c = a - o; o + 1 < s.length ? i[r] = s[o] * (1 - c) + s[o + 1] * c : i[r] = s[o]; } return i; } const T = { sampleRate: 48e3, channelCount: 2, codec: "mp4a.40.2" }; function Ne(s, t) { const e = t.videoTracks[0], i = {}; if (e != null) { const a = Mi(s.getTrackById(e.id)).buffer, { descKey: o, type: c } = e.codec.startsWith("avc1") ? { descKey: "avcDecoderConfigRecord", type: "avc1" } : e.codec.startsWith("hvc1") ? { descKey: "hevcDecoderConfigRecord", type: "hvc1" } : { descKey: "", type: "" }; o !== "" && (i.videoTrackConf = { timescale: e.timescale, duration: e.duration, width: e.video.width, height: e.video.height, brands: t.brands, type: c, [o]: a }), i.videoDecoderConf = { codec: e.codec, codedHeight: e.video.height, codedWidth: e.video.width, description: a }; } const r = t.audioTracks[0]; if (r != null) { const a = je(s); i.audioTrackConf = { timescale: r.timescale, samplerate: r.audio.sample_rate, channel_count: r.audio.channel_count, hdlr: "soun", type: r.codec.startsWith("mp4a") ? "mp4a" : r.codec, description: je(s) }, i.audioDecoderConf = { codec: r.codec.startsWith("mp4a") ? T.codec : r.codec, numberOfChannels: r.audio.channel_count, sampleRate: r.audio.sample_rate, ...a == null ? {} : zi(a) }; } return i; } function Mi(s) { for (const t of s.mdia.minf.stbl.stsd.entries) { const e = t.avcC ?? t.hvcC ?? t.av1C ?? t.vpcC; if (e != null) { const i = new wt.DataStream( void 0, 0, wt.DataStream.BIG_ENDIAN ); return e.write(i), new Uint8Array(i.buffer.slice(8)); } } throw Error("avcC, hvcC, av1C or VPX not found"); } function je(s, t = "mp4a") { var i; const e = (i = s.moov) == null ? void 0 : i.traks.map((r) => r.mdia.minf.stbl.stsd.entries).flat().find(({ type: r }) => r === t); return e == null ? void 0 : e.esds; } function zi(s) { var c; const t = (c = s.esd.descs[0]) == null ? void 0 : c.descs[0]; if (t == null) return {}; const [e, i] = t.data, r = ((e & 7) << 1) + (i >> 7), a = (i & 127) >> 3; return { sampleRate: [ 96e3, 88200, 64e3, 48e3, 44100, 32e3, 24e3, 22050, 16e3, 12e3, 11025, 8e3, 7350 ][r], numberOfChannels: a }; } async function Vi(s, t, e) { const i = wt.createFile(!1); i.onReady = (a) => { var l, f; t({ mp4boxFile: i, info: a }); const o = (l = a.videoTracks[0]) == null ? void 0 : l.id; o != null && i.setExtractionOptions(o, "video", { nbSamples: 100 }); const c = (f = a.audioTracks[0]) == null ? void 0 : f.id; c != null && i.setExtractionOptions(c, "audio", { nbSamples: 100 }), i.start(); }, i.onSamples = e, await r(); async function r() { let a = 0; const o = 30 * 1024 * 1024; for (; ; ) { const c = await s.read(o, { at: a }); if (c.byteLength === 0) break; c.fileStart = a; const l = i.appendBuffer(c); if (l == null) break; a = l; } i.stop(); } } let $e = 0; function Re(s) { return s.kind === "file" && s.createReader instanceof Function; } var Ce, Yt, jt, V, R, H, Qt, L, rt, Ot, bt, W, D, Mt; const yt = class yt { constructor(t, e = {}) { d(this, Ce, $e++); d(this, Yt, A.create(`MP4Clip id:${n(this, Ce)},`)); S(this, "ready"); d(this, jt, !1); d(this, V, { // 微秒 duration: 0, width: 0, height: 0, audioSampleRate: 0, audioChanCount: 0 }); d(this, R); d(this, H, []); d(this, Qt, 1); d(this, L, []); d(this, rt, []); d(this, Ot, null); d(this, bt, null); d(this, W, { video: null, audio: null }); d(this, D, { audio: !0 }); /** * 拦截 {@link MP4Clip.tick} 方法返回的数据,用于对图像、音频数据二次处理 * @param time 调用 tick 的时间 * @param tickRet tick 返回的数据 * * @see [移除视频绿幕背景](https://webav-tech.github.io/WebAV/demo/3_2-chromakey-video) */ S(this, "tickInterceptor", async (t, e) => e); d(this, Mt, new AbortController()); if (!(t instanceof ReadableStream) && !Re(t) && !Array.isArray(t.videoSamples)) throw Error("Illegal argument"); h(this, D, { audio: !0, ...e }), h(this, Qt, typeof e.audio == "object" && "volume" in e.audio ? e.audio.volume : 1); const i = async (r) => (await Pe(n(this, R), r), n(this, R)); h(this, R, Re(t) ? t : "localFile" in t ? t.localFile : Ee()), this.ready = (t instanceof ReadableStream ? i(t).then( (r) => Ke(r, n(this, D)) ) : Re(t) ? Ke(t, n(this, D)) : Promise.resolve(t)).then( async ({ videoSamples: r, audioSamples: a, decoderConf: o, headerBoxPos: c }) => { h(this, L, r), h(this, rt, a), h(this, W, o), h(this, H, c); const { videoFrameFinder: l, audioFrameFinder: f } = Ui( { video: o.video == null ? null : { ...o.video, hardwareAcceleration: n(this, D).__unsafe_hardwareAcceleration__ }, audio: o.audio }, await n(this, R).createReader(), r, a, n(this, D).audio !== !1 ? n(this, Qt) : 0 ); return h(this, Ot, l), h(this, bt, f), h(this, V, Li(o, r, a)), n(this, Yt).info("MP4Clip meta:", n(this, V)), { ...n(this, V) }; } ); } get meta() { return { ...n(this, V) }; } /** * 提供视频头(box: ftyp, moov)的二进制数据 * 使用任意 mp4 demxer 解析即可获得详细的视频信息 * 单元测试包含使用 mp4box.js 解析示例代码 */ async getFileHeaderBinData() { await this.ready; const t = await n(this, R).getOriginFile(); if (t == null) throw Error("MP4Clip localFile is not origin file"); return await new Blob( n(this, H).map( ({ start: e, size: i }) => t.slice(e, e + i) ) ).arrayBuffer(); } /** * 获取素材指定时刻的图像帧、音频数据 * @param time 微秒 */ async tick(t) { var r, a, o; if (t >= n(this, V).duration) return await this.tickInterceptor(t, { audio: await ((r = n(this, bt)) == null ? void 0 : r.find(t)) ?? [], state: "done" }); const [e, i] = await Promise.all([ ((a = n(this, bt)) == null ? void 0 : a.find(t)) ?? [], (o = n(this, Ot)) == null ? void 0 : o.find(t) ]); return i == null ? await this.tickInterceptor(t, { audio: e, state: "success" }) : await this.tickInterceptor(t, { video: i, audio: e, state: "success" }); } /** * 生成缩略图,默认每个关键帧生成一个 100px 宽度的缩略图。 * * @param imgWidth 缩略图宽度,默认 100 * @param opts Partial<ThumbnailOpts> * @returns Promise<Array<{ ts: number; img: Blob }>> */ async thumbnails(t = 100, e) { n(this, Mt).abort(), h(this, Mt, new AbortController()); const i = n(this, Mt).signal; await this.ready; const r = "generate thumbnails aborted"; if (i.aborted) throw Error(r); const { width: a, height: o } = n(this, V), c = Xi( t, Math.round(o * (t / a)), { quality: 0.1, type: "image/png" } ); return new Promise( async (l, f) => { let u = []; const w = n(this, W).video; if (w == null || n(this, L).length === 0) { p(); return; } i.addEventListener("abort", () => { f(Error(r)); }); async function p() { i.aborted || l( await Promise.all( u.map(async (x) => ({ ts: x.ts, img: await x.img })) ) ); } function b(x) { u.push({ ts: x.timestamp, img: c(x) }); } const { start: m = 0, end: y = n(this, V).duration, step: g } = e ?? {}; if (g) { let x = m; const C = new hi( await n(this, R).createReader(), n(this, L), { ...w, hardwareAcceleration: n(this, D).__unsafe_hardwareAcceleration__ } ); for (; x <= y && !i.aborted; ) { const v = await C.find(x); v && b(v), x += g; } C.destroy(), p(); } else await ji( n(this, L), n(this, R), w, i, { start: m, end: y }, (x, C) => { x != null && b(x), C && p(); } ); } ); } async split(t) { if (await this.ready, t <= 0 || t >= n(this, V).duration) throw Error('"time" out of bounds'); const [e, i] = Gi( n(this, L), t ), [r, a] = Ji( n(this, rt), t ), o = new yt( { localFile: n(this, R), videoSamples: e ?? [], audioSamples: r ?? [], decoderConf: n(this, W), headerBoxPos: n(this, H) }, n(this, D) ), c = new yt( { localFile: n(this, R), videoSamples: i ?? [], audioSamples: a ?? [], decoderConf: n(this, W), headerBoxPos: n(this, H) }, n(this, D) ); return await Promise.all([o.ready, c.ready]), [o, c]; } async clone() { await this.ready; const t = new yt( { localFile: n(this, R), videoSamples: [...n(this, L)], audioSamples: [...n(this, rt)], decoderConf: n(this, W), headerBoxPos: n(this, H) }, n(this, D) ); return await t.ready, t.tickInterceptor = this.tickInterceptor, t; } /** * 拆分 MP4Clip 为仅包含视频轨道和音频轨道的 MP4Clip * @returns Mp4CLip[] */ async splitTrack() { await this.ready; const t = []; if (n(this, L).length > 0) { const e = new yt( { localFile: n(this, R), videoSamples: [...n(this, L)], audioSamples: [], decoderConf: { video: n(this, W).video, audio: null }, headerBoxPos: n(this, H) }, n(this, D) ); await e.ready, e.tickInterceptor = this.tickInterceptor, t.push(e); } if (n(this, rt).length > 0) { const e = new yt( { localFile: n(this, R), videoSamples: [], audioSamples: [...n(this, rt)], decoderConf: { audio: n(this, W).audio, video: null }, headerBoxPos: n(this, H) }, n(this, D) ); await e.ready, e.tickInterceptor = this.tickInterceptor, t.push(e); } return t; } destroy() { var t, e; n(this, jt) || (n(this, Yt).info("MP4Clip destroy"), h(this, jt, !0), (t = n(this, Ot)) == null || t.destroy(), (e = n(this, bt)) == null || e.destroy()); } }; Ce = new WeakMap(), Yt = new WeakMap(), jt = new WeakMap(), V = new WeakMap(), R = new WeakMap(), H = new WeakMap(), Qt = new WeakMap(), L = new WeakMap(), rt = new WeakMap(), Ot = new WeakMap(), bt = new WeakMap(), W = new WeakMap(), D = new WeakMap(), Mt = new WeakMap(); let Qe = yt; function Li(s, t, e) { const i = { duration: 0, width: 0, height: 0, audioSampleRate: 0, audioChanCount: 0 }; s.video != null && t.length > 0 && (i.width = s.video.codedWidth ?? 0, i.height = s.video.codedHeight ?? 0), s.audio != null && e.length > 0 && (i.audioSampleRate = T.sampleRate, i.audioChanCount = T.channelCount); let r = 0, a = 0; if (t.length > 0) for (let o = t.length - 1; o >= 0; o--) { const c = t[o]; if (!c.deleted) { r = c.cts + c.duration; break; } } if (e.length > 0) { const o = e.at(-1); a = o.cts + o.duration; } return i.duration = Math.max(r, a), i; } function Ui(s, t, e, i, r) { return { audioFrameFinder: r === 0 || s.audio == null || i.length === 0 ? null : new $i( t, i, s.audio, { volume: r, targetSampleRate: T.sampleRate } ), videoFrameFinder: s.video == null || e.length === 0 ? null : new hi( t, e, s.video ) }; } async function Ke(s, t = {}) { let e = null; const i = { video: null, audio: null }; let r = [], a = [], o = [], c = -1, l = -1; const f = await s.createReader(); await Vi( f, (p) => { e = p.info; const b = p.mp4boxFile.ftyp; o.push({ start: b.start, size: b.size }); const m = p.mp4boxFile.moov; o.push({ start: m.start, size: m.size }); let { videoDecoderConf: y, audioDecoderConf: g } = Ne( p.mp4boxFile, p.info ); i.video = y ?? null, i.audio = g ?? null, y == null && g == null && A.error("MP4Clip no video and audio track"), A.info( "mp4BoxFile moov ready", { ...p.info, tracks: null, videoTracks: null, audioTracks: null }, i ); }, (p, b, m) => { if (b === "video") { c === -1 && (c = m[0].dts); for (const y of m) r.push(w(y, c, "video")); } else if (b === "audio" && t.audio) { l === -1 && (l = m[0].dts); for (const y of m) a.push(w(y, l, "audio")); } } ), await f.close(); const u = r.at(-1) ?? a.at(-1); if (e == null) throw Error("MP4Clip stream is done, but not emit ready"); if (u == null) throw Error("MP4Clip stream not contain any sample"); return _e(r), A.info("mp4 stream parsed"), { videoSamples: r, audioSamples: a, decoderConf: i, headerBoxPos: o }; function w(p, b = 0, m) { const y = m === "video" && p.is_sync ? Yi(p.data, p.description.type) : -1; let g = p.offset, x = p.size; return y >= 0 && (g += y, x -= y), { ...p, is_idr: y >= 0, offset: g, size: x, cts: (p.cts - b) / p.timescale * 1e6, dts: (p.dts - b) / p.timescale * 1e6, duration: p.duration / p.timescale * 1e6, timescale: 1e6, // 音频数据量可控,直接保存在内存中 data: m === "video" ? null : p.data }; } } var F, xt, Ct, Kt, vt, X, _, at, St, zt, qt, Vt, ot, Zt, Tt, te; class hi { constructor(t, e, i) { d(this, F, null); d(this, xt, 0); d(this, Ct, { abort: !1, st: performance.now() }); S(this, "find", async (t) => { (n(this, F) == null || n(this, F).state === "closed" || t <= n(this, xt) || t - n(this, xt) > 3e6) && n(this, Tt).call(this, t), n(this, Ct).abort = !0, h(this, xt, t), h(this, Ct, { abort: !1, st: performance.now() }); const e = await n(this, Vt).call(this, t, n(this, F), n(this, Ct)); return h(this, zt, 0), e; }); // fix VideoFrame duration is null d(this, Kt, 0); d(this, vt, !1); d(this, X, 0); d(this, _, []); d(this, at, 0); d(this, St, 0); d(this, zt, 0); d(this, qt, !1); d(this, Vt, async (t, e, i) => { if (e == null || e.state === "closed" || i.abort) return null; if (n(this, _).length > 0) { const r = n(this, _)[0]; return t < r.timestamp ? null : (n(this, _).shift(), t > r.timestamp + (r.duration ?? 0) ? (r.close(), await n(this, Vt).call(this, t, e, i)) : (!n(this, qt) && n(this, _).length < 10 && n(this, Zt).call(this, e).catch((a) => { throw h(this, qt, !0), n(this, Tt).call(this, t), a; }), r)); } if (n(this, ot) || n(this, at) < n(this, St) && e.decodeQueueSize > 0) { if (performance.now() - i.st > 6e3) throw Error( `MP4Clip.tick video timeout, ${JSON.stringify(n(this, te).call(this))}` ); h(this, zt, n(this, zt) + 1), await Ue(15); } else { if (n(this, X) >= this.samples.length) return null; try { await n(this, Zt).call(this, e); } catch (r) { throw n(this, Tt).call(this, t), r; } } return await n(this, Vt).call(this, t, e, i); }); d(this, ot, !1); d(this, Zt, async (t) => { var r, a; if (n(this, ot) || t.decodeQueueSize > 600) return; let e = n(this, X) + 1; if (e > this.samples.length) return; h(this, ot, !0); let i = !1; for (; e < this.samples.length; e++) { const o = this.samples[e]; if (!i && !o.deleted && (i = !0), o.is_idr) break; } if (i) { const o = this.samples.slice(n(this, X), e); if (((r = o[0]) == null ? void 0 : r.is_idr) !== !0) A.warn("First sample not idr frame"); else { const c = performance.now(), l = await di(o, this.localFileReader), f = performance.now() - c; if (f > 1e3) { const u = o[0], w = o.at(-1), p = w.offset + w.size - u.offset; A.warn( `Read video samples time cost: ${Math.round(f)}ms, file chunk size: ${p}` ); } if (t.state === "closed") return; h(this, Kt, ((a = l[0]) == null ? void 0 : a.duration) ?? 0), Be(t, l, { onDecodingError: (u) => { if (n(this, vt)) throw u; n(this, at) === 0 && (h(this, vt, !0), A.warn("Downgrade to software decode"), n(this, Tt).call(this)); } }), h(this, St, n(this, St) + l.length); } } h(this, X, e), h(this, ot, !1); }); d(this, Tt, (t) => { var i, r; if (h(this, ot, !1), n(this, _).forEach((a) => a.close()), h(this, _, []), t == null || t === 0) h(this, X, 0); else { let a = 0; for (let o = 0; o < this.samples.length; o++) { const c = this.samples[o]; if (c.is_idr && (a = o), !(c.cts < t)) { h(this, X, a); break; } } } h(this, St, 0), h(this, at, 0), ((i = n(this, F)) == null ? void 0 : i.state) !== "closed" && ((r = n(this, F)) == null || r.close()); const e = { ...this.conf, ...n(this, vt) ? { hardwareAcceleration: "prefer-software" } : {} }; h(this, F, new VideoDecoder({ output: (a) => { if (h(this, at, n(this, at) + 1), a.timestamp === -1) { a.close(); return; } let o = a; a.duration == null && (o = new VideoFrame(a, { duration: n(this, Kt) }), a.close()), n(this, _).push(o); }, error: (a) => { if (a.message.includes("Codec reclaimed due to inactivity")) { h(this, F, null), A.warn(a.message); return; } const o = `VideoFinder VideoDecoder err: ${a.message}, config: ${JSON.stringify(e)}, state: ${JSON.stringify(n(this, te).call(this))}`; throw A.error(o), Error(o); } })), n(this, F).configure(e); }); d(this, te, () => { var t, e; return { time: n(this, xt), decState: (t = n(this, F)) == null ? void 0 : t.state, decQSize: (e = n(this, F)) == null ? void 0 : e.decodeQueueSize, decCusorIdx: n(this, X), sampleLen: this.samples.length, inputCnt: n(this, St), outputCnt: n(this, at), cacheFrameLen: n(this, _).length, softDeocde: n(this, vt), clipIdCnt: $e, sleepCnt: n(this, zt), memInfo: ui() }; }); S(this, "destroy", () => { var t, e; ((t = n(this, F)) == null ? void 0 : t.state) !== "closed" && ((e = n(this, F)) == null || e.close()), h(this, F, null), n(this, Ct).abort = !0, n(this, _).forEach((i) => i.close()), h(this, _, []), this.localFileReader.close(); }); this.localFileReader = t, this.samples = e, this.conf = i; } } F = new WeakMap(), xt = new WeakMap(), Ct = new WeakMap(), Kt = new WeakMap(), vt = new WeakMap(), X = new WeakMap(), _ = new WeakMap(), at = new WeakMap(), St = new WeakMap(), zt = new WeakMap(), qt = new WeakMap(), Vt = new WeakMap(), ot = new WeakMap(), Zt = new WeakMap(), Tt = new WeakMap(), te = new WeakMap(); function Ni(s, t) { for (let e = 0; e < t.length; e++) { const i = t[e]; if (s >= i.cts && s < i.cts + i.duration) return e; if (i.cts > s) break; } return 0; } var ee, ie, U, At, G, et, M, Lt, ne, se, ve, Se; class $i { constructor(t, e, i, r) { d(this, ee, 1); d(this, ie); d(this, U, null); d(this, At, { abort: !1, st: performance.now() }); S(this, "find", async (t) => { const e = t <= n(this, G) || t - n(this, G) > 1e5; (n(this, U) == null || n(this, U).state === "closed" || e) && n(this, ve).call(this), e && (h(this, G, t), h(this, et, Ni(t, this.samples))), n(this, At).abort = !0; const i = t - n(this, G); h(this, G, t), h(this, At, { abort: !1, st: performance.now() }); const r = await n(this, ne).call(this, Math.ceil(i * (n(this, ie) / 1e6)), n(this, U), n(this, At)); return h(this, Lt, 0), r; }); d(this, G, 0); d(this, et, 0); d(this, M, { frameCnt: 0, data: [] }); d(this, Lt, 0); d(this, ne, async (t, e = null, i) => { if (e == null || i.abort || e.state === "closed" || t === 0) return []; const r = n(this, M).frameCnt - t; if (r > 0) return r < T.sampleRate / 10 && n(this, se).call(this, e), qe(n(this, M), t); if (e.decoding) { if (performance.now() - i.st > 3e3) throw i.abort = !0, Error( `MP4Clip.tick audio timeout, ${JSON.stringify(n(this, Se).call(this))}` ); h(this, Lt, n(this, Lt) + 1), await Ue(15); } else { if (n(this, et) >= this.samples.length - 1) return qe(n(this, M), n(this, M).frameCnt); n(this, se).call(this, e); } return n(this, ne).call(this, t, e, i); }); d(this, se, (t) => { if (t.decodeQueueSize > 10) return; const i = []; let r = n(this, et); for (; r < this.samples.length; ) { const a = this.samples[r]; if (r += 1, !a.deleted && (i.push(a), i.length >= 10)) break; } h(this, et, r), t.decode( i.map( (a) => new EncodedAudioChunk({ type: "key", timestamp: a.cts, duration: a.duration, data: a.data }) ) ); }); d(this, ve, () => { var t; h(this, G, 0), h(this, et, 0), h(this, M, { frameCnt: 0, data: [] }), (t = n(this, U)) == null || t.close(), h(this, U, Hi( this.conf, { resampleRate: T.sampleRate, volume: n(this, ee) }, (e) => { n(this, M).data.push(e), n(this, M).frameCnt += e[0].length; } )); }); d(this, Se, () => { var t, e; return { time: n(this, G), decState: (t = n(this, U)) == null ? void 0 : t.state, decQSize: (e = n(this, U)) == null ? void 0 : e.decodeQueueSize, decCusorIdx: n(this, et), sampleLen: this.samples.length, pcmLen: n(this, M).frameCnt, clipIdCnt: $e, sleepCnt: n(this, Lt), memInfo: ui() }; }); S(this, "destroy", () => { h(this, U, null), n(this, At).abort = !0, h(this, M, { frameCnt: 0, data: [] }), this.localFileReader.close(); }); this.localFileReader = t, this.samples = e, this.conf = i, h(this, ee, r.volume), h(this, ie, r.targetSampleRate); } } ee = new WeakMap(), ie = new WeakMap(), U = new WeakMap(), At = new WeakMap(), G = new WeakMap(), et = new WeakMap(), M = new WeakMap(), Lt = new WeakMap(), ne = new WeakMap(), se = new WeakMap(), ve = new WeakMap(), Se = new WeakMap(); function Hi(s, t, e) { let i = 0, r = 0; const a = (u) => { if (r += 1, u.length !== 0) { if (t.volume !== 1) for (const w of u) for (let p = 0; p < w.length; p++) w[p] *= t.volume; u.length === 1 && (u = [u[0], u[0]]), e(u); } }, o = Wi(a), c = t.resampleRate !== s.sampleRate; let l = new AudioDecoder({ output: (u) => { const w = ci(u); c ? o( () => Oi(w, u.sampleRate, { rate: t.resampleRate, chanCount: u.numberOfChannels }) ) : a(w), u.close(); }, error: (u) => { u.message.includes("Codec reclaimed due to inactivity") || f("MP4Clip AudioDecoder err", u); } }); l.configure(s); function f(u, w) { const p = `${u}: ${w.message}, state: ${JSON.stringify( { qSize: l.decodeQueueSize, state: l.state, inputCnt: i, outputCnt: r } )}`; throw A.error(p), Error(p); } return { decode(u) { i += u.length; try { for (const w of u) l.decode(w); } catch (w) { f("decode audio chunk error", w); } }, close() { l.state !== "closed" && l.close(); }, get decoding() { return i > r && l.decodeQueueSize > 0; }, get state() { return l.state; }, get decodeQueueSize() { return l.decodeQueueSize; } }; } function Wi(s) { const t = []; let e = 0; function i(o, c) { t[c] = o, r(); } function r() { const o = t[e]; o != null && (s(o), e += 1, r()); } let a = 0; return (o) => { const c = a; a += 1, o().then((l) => i(l, c)).catch((l) => i(l, c)); }; } function qe(s, t) { const e = [new Float32Array(t), new Float32Array(t)]; let i = 0, r = 0; for (; r < s.data.length; ) { const [a, o] = s.data[r]; if (i + a.length > t) { const c = t - i; e[0].set(a.subarray(0, c), i), e[1].set(o.subarray(0, c), i), s.data[r][0] = a.subarray(c, a.length), s.data[r][1] = o.subarray(c, o.length); break; } else e[0].set(a, i), e[1].set(o, i), i += a.length, r++; } return s.data = s.data.slice(r), s.frameCnt -= t, e; } async function di(s, t) { const e = s[0], i = s.at(-1); if (i == null) return []; const r = i.offset + i.size - e.offset; if (r < 3e7) { const a = new Uint8Array( await t.read(r, { at: e.offset }) ); return s.map((o) => { const c = o.offset - e.offset; return new EncodedVideoChunk({ type: o.is_sync ? "key" : "delta", timestamp: o.cts, duration: o.duration, data: a.subarray(c, c + o.size) }); }); } return await Promise.all( s.map(async (a) => new EncodedVideoChunk({ type: a.is_sync ? "key" : "delta", timestamp: a.cts, duration: a.duration, data: await t.read(a.size, { at: a.offset }) })) ); } function Xi(s, t, e) { const i = new OffscreenCanvas(s, t), r = i.getContext("2d"); return async (a) => (r.drawImage(a, 0, 0, s, t), a.close(), await i.convertToBlob(e)); } function Gi(s, t) { if (s.length === 0) return []; let e = 0, i = 0, r = -1; for (let l = 0; l < s.length; l++) { const f = s[l]; if (r === -1 && t < f.cts && (r = l - 1), f.is_idr) if (r === -1) e = l; else { i = l; break; } } const a = s[r]; if (a == null) throw Error("Not found video sample by time"); const o = s.slice(0, i === 0 ? s.length : i).map((l) => ({ ...l })); for (let l = e; l < o.length; l++) { const f = o[l]; t < f.cts && (f.deleted = !0, f.cts = -1); } _e(o); const c = s.slice(a.is_idr ? r : e).map((l) => ({ ...l, cts: l.cts - t })); for (const l of c) l.cts < 0 && (l.deleted = !0, l.cts = -1); return _e(c), [o, c]; } function Ji(s, t) { if (s.length === 0) return []; let e = -1; for (let a = 0; a < s.length; a++) { const o = s[a]; if (!(t > o.cts)) { e = a; break; } } if (e === -1) throw Error("Not found audio sample by time"); const i = s.slice(0, e).map((a) => ({ ...a })), r = s.slice(e).map((a) => ({ ...a, cts: a.cts - t })); return [i, r]; } function Be(s, t, e) { let i = 0; if (s.state === "configured") { for (; i < t.length; i++) s.decode(t[i]); s.flush().catch((r) => { if (!(r instanceof Error)) throw r; if (r.message.includes("Decoding error") && e.onDecodingError != null) { e.onDecodingError(r); return; } if (!r.message.includes("Aborted due to close")) throw r; }); } } function Yi(s, t) { if (t !== "avc1" && t !== "hvc1") return 0; const e = new DataView(s.buffer); let i = 0; for (; i < s.byteLength - 4; ) { if (t === "avc1" && (e.getUint8(i + 4) & 31) === 5) return i; if (t === "hvc1") { const r = e.getUint8(i + 4) >> 1 & 63; if (r === 19 || r === 20) return i; } i += e.getUint32(i) + 4; } return -1; } async function ji(s, t, e, i, r, a) { const o = await t.createReader(), c = await di( s.filter( (u) => !u.deleted && u.is_sync && u.cts >= r.start && u.cts <= r.end ), o ); if (c.length === 0 || i.aborted) return; let l = 0; Be(f(), c, { onDecodingError: (u) => { A.warn("thumbnailsByKeyFrame", u), l === 0 ? Be(f(!0), c, { onDecodingError: (w) => { o.close(), A.error("thumbnailsByKeyFrame retry soft deocde", w); } }) : (a(null, !0), o.close()); } }); function f(u = !1) { const w = { ...e, ...u ? { hardwareAcceleration: "prefer-software" } : {} }, p = new VideoDecoder({ output: (b) => { l += 1; const m = l === c.length; a(b, m), m && (o.close(), p.state !== "closed" && p.close()); }, error: (b) => { const m = `thumbnails decoder error: ${b.message}, config: ${JSON.stringify(w)}, state: ${JSON.stringify( { qSize: p.decodeQueueSize, state: p.state, outputCnt: l, inputCnt: c.length } )}`; throw A.error(m), Error(m); } }); return i.addEventListener("abort", () => { o.close(), p.state !== "closed" && p.close(); }), p.configure(w), p; } } function _e(s) { let t = 0, e = null; for (const i of s) if (!i.deleted) { if (i.is_sync && (t += 1), t >= 2) break; (e == null || i.cts < e.cts) && (e = i); } e != null && e.cts < 2e5 && (e.duration += e.cts, e.cts = 0); } function ui() { try { const s = performance.memory; return { jsHeapSizeLimit: s.jsHeapSizeLimit, totalJSHeapSize: s.totalJSHeapSize, usedJSHeapSize: s.usedJSHeapSize, percentUsed: (s.usedJSHeapSize / s.jsHeapSizeLimit).toFixed(3), percentTotal: (s.totalJSHeapSize / s.jsHeapSizeLimit).toFixed(3) }; } catch { return {}; } } var E, z, I, Te, fi; const gt = class gt { /** * 静态图片可使用流、ImageBitmap 初始化 * * 动图需要使用 VideoFrame[] 或提供图片类型 */ constructor(t) { d(this, Te); S(this, "ready"); d(this, E, { // 微秒 duration: 0, width: 0, height: 0 }); d(this, z, null); d(this, I, []); S(this, "tickInterceptor", async (t, e) => e); const e = (i) => (h(this, z, i), n(this, E).width = i.width, n(this, E).height = i.height, n(this, E).duration = 1 / 0, { ...n(this, E) }); if (t instanceof ReadableStream) this.ready = new Response(t).blob().then((i) => createImageBitmap(i)).then(e); else if (t instanceof ImageBitmap) this.ready = Promise.resolve(e(t)); else if (Array.isArray(t) && t.every((i) => i instanceof VideoFrame)) { h(this, I, t); const i = n(this, I)[0]; if (i == null) throw Error("The frame count must be greater than 0"); h(this, E, { width: i.displayWidth, height: i.displayHeight, duration: n(this, I).reduce( (r, a) => r + (a.duration ?? 0), 0 ) }), this.ready = Promise.resolve({ ...n(this, E), duration: 1 / 0 }); } else if ("type" in t) this.ready = B(this, Te, fi).call(this, t.stream, t.type).then(() => ({ width: n(this, E).width, height: n(this, E).height, duration: 1 / 0 })); else throw Error("Illegal arguments"); } /** * ⚠️ 静态图片的 duration 为 Infinity * * 使用 Sprite 包装时需要将它的 duration 设置为有限数 * */ get meta() { return { ...n(this, E) }; } async tick(t) { if (n(this, z) != null) return await this.tickInterceptor(t, { video: await createImageBitmap(n(this, z)), state: "success" }); const e = t % n(this, E).duration; return await this.tickInterceptor(t, { video: (n(this, I).find( (i) => e >= i.timestamp && e <= i.timestamp + (i.duration ?? 0) ) ?? n(this, I)[0]).clone(), state: "success" }); } async split(t) { if (await this.ready, n(this, z) != null) return [ new gt(await createImageBitmap(n(this, z))), new gt(await createImageBitmap(n(this, z))) ]; let e = -1; for (let a = 0; a < n(this, I).length; a++) { const o = n(this, I)[a]; if (!(t > o.timestamp)) { e = a; break; } } if (e === -1) throw Error("Not found frame by time"); const i = n(this, I).slice(0, e).map((a) => new VideoFrame(a)), r = n(this, I).slice(e).map( (a) => new VideoFrame(a, { timestamp: a.timestamp - t }) ); return [new gt(i), new gt(r)]; } async clone() { await this.ready; const t = n(this, z) == null ? n(this, I).map((e) => e.clone()) : await createImageBitmap(n(this, z)); return new gt(t); } destroy() { var t; A.info("ImgClip destroy"), (t = n(this, z)) == null || t.close(), n(this, I).forEach((e) => e.close()); } }; E = new WeakMap(), z = new WeakMap(), I = new WeakMap(), Te = new WeakSet(), fi = async function(t, e) { h(this, I, await _i(t, e)); const i = n(this, I)[0]; if (i == null) throw Error("No frame available in gif"); h(this, E, { duration: n(this, I).reduce((r, a) => r + (a.duration ?? 0), 0), width: i.codedWidth, height: i.codedHeight }), A.info("ImgClip ready:", n(this, E)); }; let Ze = gt; var kt, it, ct, J, Ae, mi, lt, Y; const tt = class tt { /** * * @param dataSource 音频文件流 * @param opts 音频配置,控制音量、是否循环 */ constructor(t, e = {}) { d(this, Ae); S(this, "ready"); d(this, kt, { // 微秒 duration: 0, width: 0, height: 0 }); d(this, it, new Float32Array()); d(this, ct, new Float32Array()); d(this, J); /** * 拦截 {@link AudioClip.tick} 方法返回的数据,用于对音频数据二次处理 * @param time 调用 tick 的时间 * @param tickRet tick 返回的数据 * * @see [移除视频绿幕背景](https://webav-tech.github.io/WebAV/demo/3_2-chromakey-video) */ S(this, "tickInterceptor", async (t, e) => e); // 微秒 d(this, lt, 0); d(this, Y, 0); h(this, J, { loop: !1, volume: 1, ...e }), this.ready = B(this, Ae, mi).call(this, t).then(() => ({ // audio 没有宽高,无需绘制 width: 0, height: 0, duration: e.loop ? 1 / 0 : n(this, kt).duration })); } /** * 音频元信息 * * ⚠️ 注意,这里是转换后(标准化)的元信息,非原始音频元信息 */ get meta() { return { ...n(this, kt), sampleRate: T.sampleRate, chanCount: 2 }; } /** * 获取音频素材完整的 PCM 数据 */ getPCMData() { return [n(this, it), n(this, ct)]; } /** * 返回上次与当前时刻差对应的音频 PCM 数据; * * 若差值超过 3s 或当前时间小于上次时间,则重置状态 * @example * tick(0) // => [] * tick(1e6) // => [leftChanPCM(1s), rightChanPCM(1s)] * */ async tick(t) { if (!n(this, J).loop && t >= n(this, kt).duration) return await this.tickInterceptor(t, { audio: [], state: "done" }); const e = t - n(this, lt); if (t < n(this, lt) || e > 3e6) return h(this, lt, t), h(this, Y, Math.ceil( n(this, lt) / 1e6 * T.sampleRate )), await this.tickInterceptor(t, { audio: [new Float32Array(0), new Float32Array(0)], state: "success" }); h(this, lt, t); const i = Math.ceil( e / 1e6 * T.sampleRate ), r = n(this, Y) + i, a = n(this, J).loop ? [ De(n(this, it), n(this, Y), r), De(n(this, ct), n(this, Y), r) ] : [ n(this, it).slice(n(this, Y), r), n(this, ct).slice(n(this, Y), r) ]; return h(this, Y, r), await this.tickInterceptor(t, { audio: a, state: "success" }); } /** * 按指定时间切割,返回前后两个音频素材 * @param time 时间,单位微秒 */ async split(t) { await this.ready; const e = Math.ceil(t / 1e6 * T.sampleRate), i = new tt( this.getPCMData().map((a) => a.slice(0, e)), n(this, J) ), r = new tt( this.getPCMData().map((a) => a.slice(e)), n(this, J) ); return [i, r]; } async clone() { await this.ready; const t = new tt(this.getPCMData(), n(this, J)); return await t.ready, t; } /** * 销毁实例,释放资源 */ destroy() { h(this, it, new Float32Array(0)), h(this, ct, new Float32Array(0)), A.info("---- audioclip destroy ----"); } }; kt = new WeakMap(), it = new WeakMap(), ct = new WeakMap(), J = new WeakMap(), Ae = new WeakSet(), mi = async function(t) { tt.ctx == null && (tt.ctx = new AudioContext({ sampleRate: T.sampleRate })); const e = performance.now(), i = t instanceof ReadableStream ? await Qi(t, tt.ctx) : t; A.info("Audio clip decoded complete:", performance.now() - e); const r = n(this, J).volume; if (r !== 1) for (const a of i) for (let o = 0; o < a.length; o += 1) a[o] *= r; n(this, kt).duration = i[0].length / T.sampleRate * 1e6, h(this, it, i[0]), h(this, ct, i[1] ?? n(this, it)), A.info( "Audio clip convert to AudioData, time:", performance.now() - e ); }, lt = new WeakMap(), Y = new WeakMap(), S(tt, "ctx", null); let ti = tt; async function Qi(s, t) { const e = await new Response(s).arrayBuffer(); return Le(await t.decodeAudioData(e)); } var It, re, Ut, Nt; const ke = class ke { constructor(t) { S(this, "ready"); d(this, It, { // 微秒 duration: 0, width: 0, height: 0 }); d(this, re, () => { }); /** * 实时流的音轨 */ S(this, "audioTrack"); d(this, Ut, null); d(this, Nt); h(this, Nt, t), this.audioTrack = t.getAudioTracks()[0] ?? null, n(this, It).duration = 1 / 0; const e = t.getVideoTracks()[0]; e != null ? (e.contentHint = "motion", this.ready = new Promise((i) => { h(this, re, Ki(e, (r) => { n(this, It).width = r.width, n(this, It).height = r.height, h(this, Ut, r), i(this.meta); })); })) : this.ready = Promise.resolve(this.meta); } get meta() { return { ...n(this, It) }; } async tick() { return { video: n(this, Ut) == null ? null : await createImageBitmap(n(this, Ut)), audio: [], state: "success" }; } async split() { return [await this.clone(), await this.clone()]; } async clone() { return new ke(n(this, Nt).clone()); } destroy() { n(this, Nt).getTracks().forEach((t) => t.stop()), n(this, re).call(this); } }; It = new WeakMap(), re = new WeakMap(), Ut = new WeakMap(), Nt = new WeakMap(), S(ke, "ctx", null); let ei = ke; function Ki(s, t) { let e = !1, i; return ze( new MediaStreamTrackProcessor({ track: s }).readable, { onChunk: async (r) => { if (!e) { const { displayHeight: a, displayWidth: o } = r, c = o ?? 0, l = a ?? 0, f = new OffscreenCanvas(c, l); i = f.getContext("2d"), t(f), e = !0; } i.drawImage(r, 0, 0), r.close(); }, onDone: async () => { } } ); } var P, ae, j, Q, K, O, nt, st, Ie, pi; const Jt = class Jt { constructor(t, e) { d(this, Ie); S(this, "ready"); d(this, P, []); d(this, ae, { width: 0, height: 0, duration: 0 }); d(this, j, { color: "#FFF", textBgColor: null, type: "srt", fontSize: 30, letterSpacing: null, bottomOffset: 30, fontFamily: "Noto Sans SC", strokeStyle: "#000", lineWidth: null, lineCap: null, lineJoin: null, textShadow: { offsetX: 2, offsetY: 2, blur: 4, color: "#000" }, videoWidth: 1280, videoHeight: 720, fontWeight: "normal", fontStyle: "normal" }); d(this, Q); d(this, K); d(this, O, null); d(this, nt, 0); d(this, st, 0); var u; if (h(this, P, Array.isArray(t) ? t : qi(t).map(({ start: w, end: p, text: b }) => ({ start: w * 1e6, end: p * 1e6, text: b }))), n(this, P).length === 0) throw Error("No subtitles content"); h(this, j, Object.assign(n(this, j), e)), h(this, st, e.textBgColor == null ? 0 : (e.fontSize ?? 50) * 0.2); const { fontSize: i, fontFamily: r, fontWeight: a, fontStyle: o, videoWidth: c, videoHeight: l, letterSpacing: f } = n(this, j); h(this, nt, i + n(this, st) * 2), h(this, Q, new OffscreenCanvas(c, l)), h(this, K, n(this, Q).getContext("2d")), n(this, K).font = `${o} ${a} ${i}px ${r}`, n(this, K).textAlign = "center", n(this, K).textBaseline = "top", n(this, K).letterSpacing = f ?? "0px", h(this, ae, { width: c, height: l, duration: ((u = n(this, P).at(-1)) == null ? void 0 : u.end) ?? 0 }), this.ready = Promise.resolve(this.meta); } get meta() { return { ...n(this, ae) }; } /** * @see {@link IClip.tick} */ async tick(t) { var a, o; if (n(this, O) != null && t >= n(this, O).timestamp && t <= n(this, O).timestamp + (n(this, O).duration ?? 0)) return { video: n(this, O).clone(), state: "success" }; let e = 0; for (; e < n(this, P).length && !(t <= n(this, P)[e].end); e += 1) ; const i = n(this, P)[e] ?? n(this, P).at(-1); if (t > i.end) return { state: "done" }; if (t < i.start) { n(this, K).clearRect(0, 0, n(this, Q).width, n(this, Q).height); const c = new VideoFrame(n(this, Q), { timestamp: t, // 直到下个字幕出现的时机 duration: i.start - t }); return (a = n(this, O)) == null || a.close(), h(this, O, c), { video: c.clone(), state: "success" }; } B(this, Ie, pi).call(this, i.text); const r = new VideoFrame(n(this, Q), { timestamp: t, duration: i.end - t }); return (o = n(this, O)) == null || o.close(), h(this, O, r), { video: r.clone(), state: "success" }; } /** * @see {@link IClip.split} */ async split(t) { await this.ready; let e = -1; for (let c = 0; c < n(this, P).length; c++) { const l = n(this, P)[c]; if (!(t > l.start)) { e = c; break; } } if (e === -1) throw Error("Not found subtitle by time"); const i = n(this, P).slice(0, e).map((c) => ({ ...c })); let r = i.at(-1), a = null; r != null && r.end > t && (a = { start: 0, end: r.end - t, text: r.text }, r.end = t); const o = n(this, P).slice(e).map((c) => ({ ...c, start: c.start - t, end: c.end - t })); return a != null && o.unshift(a), [ new Jt(i, n(this, j)), new Jt(o, n(this, j)) ]; } /** * @see {@link IClip.clone} */ async clone() { return new Jt(n(this, P).slice(0), n(this, j)); } /** * @see {@link IClip.destroy} */ destroy() { var t; (t = n(this, O)) == null || t.close(); } }; P = new WeakMap(), ae = new WeakMap(), j = new WeakMap(), Q = new WeakMap(), K = new WeakMap(), O = new WeakMap(), nt = new WeakMap(), st = new WeakMap(), Ie = new WeakSet(), pi = function(t) { const e = t.split(` `).reverse().map((g) => g.trim()), { width: i, height: r } = n(this, Q), { color: a, fontSize: o, textBgColor: c, textShadow: l, strokeStyle: f, lineWidth: u, lineCap: w, lineJoin: p, bottomOffset: b } = n(this, j), m = n(this, K); m.clearRect(0, 0, i, r), m.globalAlpha = 0.6; let y = b; for (const g of e) { const x = m.measureText(g), C = i / 2; c != null && (m.shadowOffsetX = 0, m.shadowOffsetY = 0, m.shadowBlur = 0, m.fillStyle = c, m.globalAlpha = 0.5, m.fillRect( C - x.actualBoundingBoxLeft - n(this, st), r - y - n(this, nt),