UNPKG

@webav/av-cliper

Version:

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

1,805 lines (1,800 loc) 84.1 kB
import F from "@webav/mp4box.js"; import { workerTimer as ut, Log as C, autoReadStream as U, file2stream as Z, EventTool as N, recodemux as ft } from "@webav/internal-utils"; import { Log as Se } from "@webav/internal-utils"; import * as mt from "wave-resampler"; import { tmpfile as _, write as O } from "opfs-tools"; const pt = `#version 300 es layout (location = 0) in vec4 a_position; layout (location = 1) in vec2 a_texCoord; out vec2 v_texCoord; void main () { gl_Position = a_position; v_texCoord = a_texCoord; } `, wt = `#version 300 es precision mediump float; out vec4 FragColor; in vec2 v_texCoord; uniform sampler2D frameTexture; uniform vec3 keyColor; // 色度的相似度计算 uniform float similarity; // 透明度的平滑度计算 uniform float smoothness; // 降低绿幕饱和度,提高抠图准确度 uniform float spill; vec2 RGBtoUV(vec3 rgb) { return vec2( rgb.r * -0.169 + rgb.g * -0.331 + rgb.b * 0.5 + 0.5, rgb.r * 0.5 + rgb.g * -0.419 + rgb.b * -0.081 + 0.5 ); } void main() { // 获取当前像素的rgba值 vec4 rgba = texture(frameTexture, v_texCoord); // 计算当前像素与绿幕像素的色度差值 vec2 chromaVec = RGBtoUV(rgba.rgb) - RGBtoUV(keyColor); // 计算当前像素与绿幕像素的色度距离(向量长度), 越相像则色度距离越小 float chromaDist = sqrt(dot(chromaVec, chromaVec)); // 设置了一个相似度阈值,baseMask为负,则表明是绿幕,为正则表明不是绿幕 float baseMask = chromaDist - similarity; // 如果baseMask为负数,fullMask等于0;baseMask为正数,越大,则透明度越低 float fullMask = pow(clamp(baseMask / smoothness, 0., 1.), 1.5); rgba.a = fullMask; // 设置透明度 // 如果baseMask为负数,spillVal等于0;baseMask为整数,越小,饱和度越低 float spillVal = pow(clamp(baseMask / spill, 0., 1.), 1.5); float desat = clamp(rgba.r * 0.2126 + rgba.g * 0.7152 + rgba.b * 0.0722, 0., 1.); // 计算当前像素的灰度值 rgba.rgb = mix(vec3(desat, desat, desat), rgba.rgb, spillVal); FragColor = rgba; } `, yt = [-1, 1, -1, -1, 1, -1, 1, -1, 1, 1, -1, 1], gt = [0, 1, 0, 0, 1, 0, 1, 0, 1, 1, 0, 1]; function bt(s, t, e) { const i = G(s, s.VERTEX_SHADER, t), n = G(s, s.FRAGMENT_SHADER, e), a = s.createProgram(); if (s.attachShader(a, i), s.attachShader(a, n), s.linkProgram(a), !s.getProgramParameter(a, s.LINK_STATUS)) throw Error( s.getProgramInfoLog(a) ?? "Unable to initialize the shader program" ); return a; } function G(s, t, e) { const i = s.createShader(t); if (s.shaderSource(i, e), s.compileShader(i), !s.getShaderParameter(i, s.COMPILE_STATUS)) { const n = s.getShaderInfoLog(i); throw s.deleteShader(i), Error(n ?? "An error occurred compiling the shaders"); } return i; } function xt(s, t, e) { s.bindTexture(s.TEXTURE_2D, e), s.texImage2D(s.TEXTURE_2D, 0, s.RGBA, s.RGBA, s.UNSIGNED_BYTE, t), s.drawArrays(s.TRIANGLES, 0, 6); } function Ct(s) { const t = s.createTexture(); if (t == null) throw Error("Create WebGL texture error"); s.bindTexture(s.TEXTURE_2D, t); const e = 0, i = s.RGBA, n = 1, a = 1, r = 0, o = s.RGBA, c = s.UNSIGNED_BYTE, l = new Uint8Array([0, 0, 255, 255]); return s.texImage2D( s.TEXTURE_2D, e, i, n, a, r, o, c, l ), s.texParameteri(s.TEXTURE_2D, s.TEXTURE_MAG_FILTER, s.LINEAR), s.texParameteri(s.TEXTURE_2D, s.TEXTURE_MIN_FILTER, s.LINEAR), s.texParameteri(s.TEXTURE_2D, s.TEXTURE_WRAP_S, s.CLAMP_TO_EDGE), s.texParameteri(s.TEXTURE_2D, s.TEXTURE_WRAP_T, s.CLAMP_TO_EDGE), t; } function vt(s) { const t = "document" in globalThis ? globalThis.document.createElement("canvas") : new OffscreenCanvas(s.width, s.height); t.width = s.width, t.height = s.height; const e = t.getContext("webgl2", { premultipliedAlpha: !1, alpha: !0 }); if (e == null) throw Error("Cant create gl context"); const i = bt(e, pt, wt); e.useProgram(i), e.uniform3fv( e.getUniformLocation(i, "keyColor"), s.keyColor.map((c) => c / 255) ), e.uniform1f( e.getUniformLocation(i, "similarity"), s.similarity ), e.uniform1f( e.getUniformLocation(i, "smoothness"), s.smoothness ), e.uniform1f(e.getUniformLocation(i, "spill"), s.spill); const n = e.createBuffer(); e.bindBuffer(e.ARRAY_BUFFER, n), e.bufferData(e.ARRAY_BUFFER, new Float32Array(yt), e.STATIC_DRAW); const a = e.getAttribLocation(i, "a_position"); e.vertexAttribPointer( a, 2, e.FLOAT, !1, Float32Array.BYTES_PER_ELEMENT * 2, 0 ), e.enableVertexAttribArray(a); const r = e.createBuffer(); e.bindBuffer(e.ARRAY_BUFFER, r), e.bufferData( e.ARRAY_BUFFER, new Float32Array(gt), e.STATIC_DRAW ); const o = e.getAttribLocation(i, "a_texCoord"); return e.vertexAttribPointer( o, 2, e.FLOAT, !1, Float32Array.BYTES_PER_ELEMENT * 2, 0 ), e.enableVertexAttribArray(o), e.pixelStorei(e.UNPACK_FLIP_Y_WEBGL, 1), { cvs: t, gl: e }; } function St(s) { return s instanceof VideoFrame ? { width: s.codedWidth, height: s.codedHeight } : { width: s.width, height: s.height }; } function Tt(s) { const e = new OffscreenCanvas(1, 1).getContext("2d"); e.drawImage(s, 0, 0); const { data: [i, n, a] } = e.getImageData(0, 0, 1, 1); return [i, n, a]; } const we = (s) => { let t = null, e = null, i = s.keyColor, n = null; return async (a) => { if ((t == null || e == null || n == null) && (i == null && (i = Tt(a)), { cvs: t, gl: e } = vt({ ...St(a), keyColor: i, ...s }), n = Ct(e)), xt(e, a, n), globalThis.VideoFrame != null && a instanceof globalThis.VideoFrame) { const r = new VideoFrame(t, { alpha: "keep", timestamp: a.timestamp, duration: a.duration ?? void 0 }); return a.close(), r; } return createImageBitmap(t, { imageOrientation: a instanceof ImageBitmap ? "flipY" : "none" }); }; }; function At(s) { return document.createElement(s); } function Ft(s) { var t = "", e = new Uint8Array(s), i = e.byteLength; for (let n = 0; n < i; n++) t += String.fromCharCode(e[n]); return window.btoa(t); } async function kt(s, t, e = {}) { const i = At("pre"); i.style.cssText = `margin: 0; ${t}; position: fixed;`, i.textContent = s, document.body.appendChild(i), e.onCreated?.(i); const n = "TMP_FONT_NAME_" + crypto.randomUUID(); let a = null; e.font != null && (i.style.fontFamily = n, a = new FontFace(n, `url(${e.font.url})`), await a.load(), document.fonts.add(a), await document.fonts.ready); const { width: r, height: o } = i.getBoundingClientRect(); i.remove(), a != null && document.fonts.delete(a); const c = new Image(); c.width = r, c.height = o; const l = e.font == null ? "" : ` @font-face { font-family: '${n}'; src: url('data:font/woff2;base64,${Ft(await (await fetch(e.font.url)).arrayBuffer())}') format('woff2'); } `, h = ` <svg xmlns="http://www.w3.org/2000/svg" width="${r}" height="${o}"> <style> ${l} </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 c.src = `data:image/svg+xml;charset=utf-8,${h}`, await new Promise((u) => { c.onload = u; }), c; } async function ye(s, t, e = {}) { const i = await kt(s, t, e), n = new OffscreenCanvas(i.width, i.height); return n.getContext("2d")?.drawImage(i, 0, 0, i.width, i.height), await createImageBitmap(n); } function It(s) { const t = new Float32Array( s.map((i) => i.length).reduce((i, n) => i + n) ); let e = 0; for (const i of s) t.set(i, e), e += i.length; return t; } function tt(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(It); } function et(s) { if (s.format === "f32-planar") { const t = []; for (let e = 0; e < s.numberOfChannels; e += 1) { const i = s.allocationSize({ planeIndex: e }), n = new ArrayBuffer(i); s.copyTo(n, { planeIndex: e }), t.push(new Float32Array(n)); } return t; } else if (s.format === "f32") { const t = new ArrayBuffer(s.allocationSize({ planeIndex: 0 })); return s.copyTo(t, { planeIndex: 0 }), Et(new Float32Array(t), s.numberOfChannels); } else if (s.format === "s16") { const t = new ArrayBuffer(s.allocationSize({ planeIndex: 0 })); return s.copyTo(t, { planeIndex: 0 }), Rt(new Int16Array(t), s.numberOfChannels); } throw Error("Unsupported audio data format"); } function Rt(s, t) { const e = s.length / t, i = Array.from( { length: t }, () => new Float32Array(e) ); for (let n = 0; n < e; n++) for (let a = 0; a < t; a++) { const r = s[n * t + a]; i[a][n] = r / 32768; } return i; } function Et(s, t) { const e = s.length / t, i = Array.from( { length: t }, () => new Float32Array(e) ); for (let n = 0; n < e; n++) for (let a = 0; a < t; a++) i[a][n] = s[n * t + a]; return i; } function $(s) { return Array(s.numberOfChannels).fill(0).map((t, e) => s.getChannelData(e)); } async function Dt(s, t) { const e = { type: t, data: s }, i = new ImageDecoder(e); await Promise.all([i.completed, i.tracks.ready]); let n = i.tracks.selectedTrack?.frameCount ?? 1; const a = []; for (let r = 0; r < n; r += 1) a.push((await i.decode({ frameIndex: r })).image); return a; } function j(s) { const t = Math.max(...s.map((i) => i[0]?.length ?? 0)), e = new Float32Array(t * 2); for (let i = 0; i < t; i++) { let n = 0, a = 0; for (let r = 0; r < s.length; r++) { const o = s[r][0]?.[i] ?? 0, c = s[r][1]?.[i] ?? o; n += o, a += c; } e[i] = n, e[i + t] = a; } return e; } async function Pt(s, t, e) { const i = s.length, n = Array(e.chanCount).fill(0).map(() => new Float32Array(0)); if (i === 0) return n; const a = Math.max(...s.map((l) => l.length)); if (a === 0) return n; if (globalThis.OfflineAudioContext == null) return s.map( (l) => new Float32Array( mt.resample(l, t, e.rate, { method: "sinc", LPF: !1 }) ) ); const r = new globalThis.OfflineAudioContext( e.chanCount, a * e.rate / t, e.rate ), o = r.createBufferSource(), c = r.createBuffer(i, a, t); return s.forEach((l, h) => c.copyToChannel(l, h)), o.buffer = c, o.connect(r.destination), o.start(), $(await r.startRendering()); } function H(s) { return new Promise((t) => { const e = ut(() => { e(), t(); }, s); }); } function z(s, t, e) { const i = e - t, n = new Float32Array(i); let a = 0; for (; a < i; ) n[a] = s[(t + a) % s.length], a += 1; return n; } function it(s, t) { const e = Math.floor(s.length / t), i = new Float32Array(e); for (let n = 0; n < e; n++) { const a = n * t, r = Math.floor(a), o = a - r; r + 1 < s.length ? i[n] = s[r] * (1 - o) + s[r + 1] * o : i[n] = s[r]; } return i; } const x = { sampleRate: 48e3, channelCount: 2, codec: "mp4a.40.2" }; function W(s, t) { const e = t.videoTracks[0], i = {}; if (e != null) { const a = Mt(s.getTrackById(e.id))?.buffer, { descKey: r, type: o } = e.codec.startsWith("avc1") ? { descKey: "avcDecoderConfigRecord", type: "avc1" } : e.codec.startsWith("hvc1") ? { descKey: "hevcDecoderConfigRecord", type: "hvc1" } : { descKey: "", type: "" }; r !== "" && (i.videoTrackConf = { timescale: e.timescale, duration: e.duration, width: e.video.width, height: e.video.height, brands: t.brands, type: o, [r]: a }), i.videoDecoderConf = { codec: e.codec, codedHeight: e.video.height, codedWidth: e.video.width, description: a }; } const n = t.audioTracks[0]; if (n != null) { const a = Bt(s), r = a == null ? {} : _t(a); i.audioTrackConf = { timescale: n.timescale, samplerate: r.sampleRate ?? n.audio.sample_rate, channel_count: r.numberOfChannels ?? n.audio.channel_count, hdlr: "soun", type: n.codec.startsWith("mp4a") ? "mp4a" : n.codec, description: a }, i.audioDecoderConf = { codec: r.codec ?? x.codec, numberOfChannels: r.numberOfChannels ?? n.audio.channel_count, sampleRate: r.sampleRate ?? n.audio.sample_rate }; } return i; } function Mt(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 F.DataStream( void 0, 0, F.DataStream.BIG_ENDIAN ); return e.write(i), new Uint8Array(i.buffer.slice(8)); } } } function Bt(s, t = "mp4a") { return s.moov?.traks.map((i) => i.mdia.minf.stbl.stsd.entries).flat().find(({ type: i }) => i === t)?.esds; } function _t(s) { let t = "mp4a"; const e = s.esd.descs[0]; if (e == null) return {}; t += "." + e.oti.toString(16); const i = e.descs[0]; if (i == null) return t.endsWith("40") && (t += ".2"), { codec: t }; const n = (i.data[0] & 248) >> 3; t += "." + n; const [a, r] = i.data, o = ((a & 7) << 1) + (r >> 7), c = (r & 127) >> 3; return { codec: t, sampleRate: [ 96e3, 88200, 64e3, 48e3, 44100, 32e3, 24e3, 22050, 16e3, 12e3, 11025, 8e3, 7350 ][o], numberOfChannels: c }; } async function Ot(s, t, e) { const i = F.createFile(!1); i.onReady = (a) => { t({ mp4boxFile: i, info: a }); const r = a.videoTracks[0]?.id; r != null && i.setExtractionOptions(r, "video", { nbSamples: 100 }); const o = a.audioTracks[0]?.id; o != null && i.setExtractionOptions(o, "audio", { nbSamples: 100 }), i.start(); }, i.onSamples = e, await n(); async function n() { let a = 0; const r = 30 * 1024 * 1024; for (; ; ) { const o = await s.read(r, { at: a }); if (o.byteLength === 0) break; o.fileStart = a; const c = i.appendBuffer(o); if (c == null) break; a = c; } i.stop(); } } function zt(s) { if (s?.length !== 9) return {}; const t = new Int32Array(s.buffer), e = t[0] / 65536, i = t[1] / 65536, n = t[3] / 65536, a = t[4] / 65536, r = t[6] / 65536, o = t[7] / 65536, c = t[8] / (1 << 30), l = Math.sqrt(e * e + n * n), h = Math.sqrt(i * i + a * a), u = Math.atan2(n, e), d = u * 180 / Math.PI; return { scaleX: l, scaleY: h, rotationRad: u, rotationDeg: d, translateX: r, translateY: o, perspective: c }; } function Vt(s, t, e) { const i = (Math.round(e / 90) * 90 + 360) % 360; if (i === 0) return (c) => c; const n = i === 90 || i === 270 ? t : s, a = i === 90 || i === 270 ? s : t, r = new OffscreenCanvas(n, a), o = r.getContext("2d"); return o.translate(n / 2, a / 2), o.rotate(-i * Math.PI / 180), o.translate(-s / 2, -t / 2), (c) => { if (c == null) return null; o.drawImage(c, 0, 0); const l = new VideoFrame(r, { timestamp: c.timestamp, duration: c.duration ?? void 0 }); return c.close(), l; }; } let X = 0; function B(s) { return s.kind === "file" && s.createReader instanceof Function; } class I { #t = X++; #n = C.create(`MP4Clip id:${this.#t},`); ready; #e = !1; #s = { // 微秒 duration: 0, width: 0, height: 0, audioSampleRate: 0, audioChanCount: 0 }; get meta() { return { ...this.#s }; } #a; /** 存储视频头(box: ftyp, moov)的二进制数据 */ #r = []; /** * 提供视频头(box: ftyp, moov)的二进制数据 * 使用任意 mp4 demxer 解析即可获得详细的视频信息 * 单元测试包含使用 mp4box.js 解析示例代码 */ async getFileHeaderBinData() { await this.ready; const t = await this.#a.getOriginFile(); if (t == null) throw Error("MP4Clip localFile is not origin file"); return await new Blob( this.#r.map( ({ start: e, size: i }) => t.slice(e, e + i) ) ).arrayBuffer(); } /**存储视频平移旋转信息,目前只还原旋转 */ #i = { perspective: 1, rotationRad: 0, rotationDeg: 0, scaleX: 1, scaleY: 1, translateX: 0, translateY: 0 }; #o = (t) => t; #l = 1; #c = []; #d = []; #m = null; #u = null; #f = { video: null, audio: null }; #h = { audio: !0 }; constructor(t, e = {}) { if (!(t instanceof ReadableStream) && !B(t) && !Array.isArray(t.videoSamples)) throw Error("Illegal argument"); this.#h = { audio: !0, ...e }, this.#l = typeof e.audio == "object" && "volume" in e.audio ? e.audio.volume : 1; const i = async (n) => (await O(this.#a, n), this.#a); this.#a = B(t) ? t : "localFile" in t ? t.localFile : _(), this.ready = (t instanceof ReadableStream ? i(t).then( (n) => J(n, this.#h) ) : B(t) ? J(t, this.#h) : Promise.resolve(t)).then( async ({ videoSamples: n, audioSamples: a, decoderConf: r, headerBoxPos: o, parsedMatrix: c }) => { this.#c = n, this.#d = a, this.#f = r, this.#r = o, this.#i = c; const { videoFrameFinder: l, audioFrameFinder: h } = Ut( { video: r.video == null ? null : { ...r.video, hardwareAcceleration: this.#h.__unsafe_hardwareAcceleration__ }, audio: r.audio }, await this.#a.createReader(), n, a, this.#h.audio !== !1 ? this.#l : 0 ); this.#m = l, this.#u = h; const { codedWidth: u, codedHeight: d } = r.video ?? {}; return u && d && (this.#o = Vt( u, d, c.rotationDeg )), this.#s = Lt( r, n, a, c.rotationDeg ), this.#n.info("MP4Clip meta:", this.#s), { ...this.#s }; } ); } /** * 拦截 {@link MP4Clip.tick} 方法返回的数据,用于对图像、音频数据二次处理 * @param time 调用 tick 的时间 * @param tickRet tick 返回的数据 * * @see [移除视频绿幕背景](https://webav-tech.github.io/WebAV/demo/3_2-chromakey-video) */ tickInterceptor = async (t, e) => e; /** * 获取素材指定时刻的图像帧、音频数据 * @param time 微秒 */ async tick(t) { if (t >= this.#s.duration) return await this.tickInterceptor(t, { audio: await this.#u?.find(t) ?? [], state: "done" }); const [e, i] = await Promise.all([ this.#u?.find(t) ?? [], this.#m?.find(t).then(this.#o) ]); return i == null ? await this.tickInterceptor(t, { audio: e, state: "success" }) : await this.tickInterceptor(t, { video: i, audio: e, state: "success" }); } #p = new AbortController(); /** * 生成缩略图,默认每个关键帧生成一个 100px 宽度的缩略图。 * * @param imgWidth 缩略图宽度,默认 100 * @param opts Partial<ThumbnailOpts> * @returns Promise<Array<{ ts: number; img: Blob }>> */ async thumbnails(t = 100, e) { this.#p.abort(), this.#p = new AbortController(); const i = this.#p.signal; await this.ready; const n = "generate thumbnails aborted"; if (i.aborted) throw Error(n); const { width: a, height: r } = this.#s, o = Xt( t, Math.round(r * (t / a)), { quality: 0.1, type: "image/png" } ); return new Promise( async (c, l) => { let h = []; const u = this.#f.video; if (u == null || this.#c.length === 0) { d(); return; } i.addEventListener("abort", () => { l(Error(n)); }); async function d() { i.aborted || c( await Promise.all( h.map(async (p) => ({ ts: p.ts, img: await p.img })) ) ); } function y(p) { h.push({ ts: p.timestamp, img: o(p) }); } const { start: m = 0, end: f = this.#s.duration, step: w } = e ?? {}; if (w) { let p = m; const g = new nt( await this.#a.createReader(), this.#c, { ...u, hardwareAcceleration: this.#h.__unsafe_hardwareAcceleration__ } ); for (; p <= f && !i.aborted; ) { const b = await g.find(p); b && y(b), p += w; } g.destroy(), d(); } else await Jt( this.#c, this.#a, u, i, { start: m, end: f }, (p, g) => { p != null && y(p), g && d(); } ); } ); } async split(t) { if (await this.ready, t <= 0 || t >= this.#s.duration) throw Error('"time" out of bounds'); const [e, i] = Yt( this.#c, t ), [n, a] = Gt( this.#d, t ), r = new I( { localFile: this.#a, videoSamples: e ?? [], audioSamples: n ?? [], decoderConf: this.#f, headerBoxPos: this.#r, parsedMatrix: this.#i }, this.#h ), o = new I( { localFile: this.#a, videoSamples: i ?? [], audioSamples: a ?? [], decoderConf: this.#f, headerBoxPos: this.#r, parsedMatrix: this.#i }, this.#h ); return await Promise.all([r.ready, o.ready]), [r, o]; } async clone() { await this.ready; const t = new I( { localFile: this.#a, videoSamples: [...this.#c], audioSamples: [...this.#d], decoderConf: this.#f, headerBoxPos: this.#r, parsedMatrix: this.#i }, this.#h ); return await t.ready, t.tickInterceptor = this.tickInterceptor, t; } /** * 拆分 MP4Clip 为仅包含视频轨道和音频轨道的 MP4Clip * @returns Mp4CLip[] */ async splitTrack() { await this.ready; const t = []; if (this.#c.length > 0) { const e = new I( { localFile: this.#a, videoSamples: [...this.#c], audioSamples: [], decoderConf: { video: this.#f.video, audio: null }, headerBoxPos: this.#r, parsedMatrix: this.#i }, this.#h ); await e.ready, e.tickInterceptor = this.tickInterceptor, t.push(e); } if (this.#d.length > 0) { const e = new I( { localFile: this.#a, videoSamples: [], audioSamples: [...this.#d], decoderConf: { audio: this.#f.audio, video: null }, headerBoxPos: this.#r, parsedMatrix: this.#i }, this.#h ); await e.ready, e.tickInterceptor = this.tickInterceptor, t.push(e); } return t; } destroy() { this.#e || (this.#n.info("MP4Clip destroy"), this.#e = !0, this.#m?.destroy(), this.#u?.destroy()); } } function Lt(s, t, e, i) { const n = { duration: 0, width: 0, height: 0, audioSampleRate: 0, audioChanCount: 0 }; if (s.video != null && t.length > 0) { n.width = s.video.codedWidth ?? 0, n.height = s.video.codedHeight ?? 0; const o = (Math.round(i / 90) * 90 + 360) % 360; (o === 90 || o === 270) && ([n.width, n.height] = [n.height, n.width]); } s.audio != null && e.length > 0 && (n.audioSampleRate = x.sampleRate, n.audioChanCount = x.channelCount); let a = 0, r = 0; if (t.length > 0) for (let o = t.length - 1; o >= 0; o--) { const c = t[o]; if (!c.deleted) { a = c.cts + c.duration; break; } } if (e.length > 0) { const o = e.at(-1); r = o.cts + o.duration; } return n.duration = Math.max(a, r), n; } function Ut(s, t, e, i, n) { return { audioFrameFinder: n === 0 || s.audio == null || i.length === 0 ? null : new $t( t, i, s.audio, { volume: n, targetSampleRate: x.sampleRate } ), videoFrameFinder: s.video == null || e.length === 0 ? null : new nt( t, e, s.video ) }; } async function J(s, t = {}) { let e = null; const i = { video: null, audio: null }; let n = [], a = [], r = []; const o = { perspective: 1, rotationRad: 0, rotationDeg: 0, scaleX: 1, scaleY: 1, translateX: 0, translateY: 0 }; let c = -1, l = -1; const h = await s.createReader(); await Ot( h, async (d) => { e = d.info; const y = d.mp4boxFile.ftyp; r.push({ start: y.start, size: y.size }); const m = d.mp4boxFile.moov; r.push({ start: m.start, size: m.size }), Object.assign(o, zt(e.videoTracks[0]?.matrix)); let { videoDecoderConf: f, audioDecoderConf: w } = W( d.mp4boxFile, d.info ); if (i.video = f ?? null, i.audio = w ?? null, f == null && w == null && C.error("MP4Clip no video and audio track"), w != null) { const { supported: p } = await AudioDecoder.isConfigSupported(w); p || C.error(`MP4Clip audio codec is not supported: ${w.codec}`); } if (f != null) { const { supported: p } = await VideoDecoder.isConfigSupported(f); p || C.error(`MP4Clip video codec is not supported: ${f.codec}`); } C.info( "mp4BoxFile moov ready", { ...d.info, tracks: null, videoTracks: null, audioTracks: null }, i ); }, (d, y, m) => { if (y === "video") { c === -1 && (c = m[0].dts); for (const f of m) n.push(Q(f, c, "video")); } else if (y === "audio" && t.audio) { l === -1 && (l = m[0].dts); for (const f of m) a.push(Q(f, l, "audio")); } } ), await h.close(); const u = n.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 L(n), C.info("mp4 stream parsed"), { videoSamples: n, audioSamples: a, decoderConf: i, headerBoxPos: r, parsedMatrix: o }; } function Q(s, t = 0, e) { let i = s.offset; const n = e === "video" && s.is_sync ? jt(s.data, s.description.type) : -1; let a = s.size; return n > 0 && (i += n, a -= n), { ...s, is_idr: n >= 0, offset: i, size: a, cts: (s.cts - t) / s.timescale * 1e6, dts: (s.dts - t) / s.timescale * 1e6, duration: s.duration / s.timescale * 1e6, timescale: 1e6, // 音频数据量可控,直接保存在内存中 data: e === "video" ? null : s.data }; } class nt { constructor(t, e, i) { this.localFileReader = t, this.samples = e, this.conf = i; } #t = null; #n = 0; #e = { abort: !1, st: performance.now() }; find = async (t) => { (this.#t == null || this.#t.state === "closed" || t <= this.#n || t - this.#n > 3e6) && this.#h(t), this.#e.abort = !0, this.#n = t, this.#e = { abort: !1, st: performance.now() }; const e = await this.#m(t, this.#t, this.#e); return this.#c = 0, e; }; // fix VideoFrame duration is null #s = 0; #a = !1; #r = 0; #i = []; #o = 0; #l = 0; #c = 0; #d = !1; #m = async (t, e, i) => { if (e == null || e.state === "closed" || i.abort) return null; if (this.#i.length > 0) { const n = this.#i[0]; return t < n.timestamp ? null : (this.#i.shift(), t > n.timestamp + (n.duration ?? 0) ? (n.close(), await this.#m(t, e, i)) : (!this.#d && this.#i.length < 10 && this.#f(e).catch((a) => { throw this.#d = !0, this.#h(t), a; }), n)); } if (this.#u || this.#o < this.#l && e.decodeQueueSize > 0) { if (performance.now() - i.st > 6e3) throw Error( `MP4Clip.tick video timeout, ${JSON.stringify(this.#p())}` ); this.#c += 1, await H(15); } else { if (this.#r >= this.samples.length) return null; try { await this.#f(e); } catch (n) { throw this.#h(t), n; } } return await this.#m(t, e, i); }; #u = !1; #f = async (t) => { if (this.#u || t.decodeQueueSize > 600) return; let e = this.#r + 1; if (e > this.samples.length) return; this.#u = !0; let i = !1; for (; e < this.samples.length; e++) { const n = this.samples[e]; if (!i && !n.deleted && (i = !0), n.is_idr) break; } if (i) { const n = this.samples.slice(this.#r, e); if (n[0]?.is_idr !== !0) C.warn("First sample not idr frame"); else { const a = performance.now(), r = await st(n, this.localFileReader), o = performance.now() - a; if (o > 1e3) { const c = n[0], l = n.at(-1), h = l.offset + l.size - c.offset; C.warn( `Read video samples time cost: ${Math.round(o)}ms, file chunk size: ${h}` ); } if (t.state === "closed") return; this.#s = r[0]?.duration ?? 0, V(t, r, { onDecodingError: (c) => { if (this.#a) throw c; this.#o === 0 && (this.#a = !0, C.warn("Downgrade to software decode"), this.#h()); } }), this.#l += r.length; } } this.#r = e, this.#u = !1; }; #h = (t) => { if (this.#u = !1, this.#i.forEach((i) => i.close()), this.#i = [], t == null || t === 0) this.#r = 0; else { let i = 0; for (let n = 0; n < this.samples.length; n++) { const a = this.samples[n]; if (a.is_idr && (i = n), !(a.cts < t)) { this.#r = i; break; } } } this.#l = 0, this.#o = 0, this.#t?.state !== "closed" && this.#t?.close(); const e = { ...this.conf, ...this.#a ? { hardwareAcceleration: "prefer-software" } : {} }; this.#t = new VideoDecoder({ output: (i) => { if (this.#o += 1, i.timestamp === -1) { i.close(); return; } let n = i; i.duration == null && (n = new VideoFrame(i, { duration: this.#s }), i.close()), this.#i.push(n); }, error: (i) => { if (i.message.includes("Codec reclaimed due to inactivity")) { this.#t = null, C.warn(i.message); return; } const n = `VideoFinder VideoDecoder err: ${i.message}, config: ${JSON.stringify(e)}, state: ${JSON.stringify(this.#p())}`; throw C.error(n), Error(n); } }), this.#t.configure(e); }; #p = () => ({ time: this.#n, decState: this.#t?.state, decQSize: this.#t?.decodeQueueSize, decCusorIdx: this.#r, sampleLen: this.samples.length, inputCnt: this.#l, outputCnt: this.#o, cacheFrameLen: this.#i.length, softDeocde: this.#a, clipIdCnt: X, sleepCnt: this.#c, memInfo: at() }); destroy = () => { this.#t?.state !== "closed" && this.#t?.close(), this.#t = null, this.#e.abort = !0, this.#i.forEach((t) => t.close()), this.#i = [], this.localFileReader.close(); }; } function Nt(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; } class $t { constructor(t, e, i, n) { this.localFileReader = t, this.samples = e, this.conf = i, this.#t = n.volume, this.#n = n.targetSampleRate; } #t = 1; #n; #e = null; #s = { abort: !1, st: performance.now() }; find = async (t) => { const e = t <= this.#a || t - this.#a > 1e5; (this.#e == null || this.#e.state === "closed" || e) && this.#d(), e && (this.#a = t, this.#r = Nt(t, this.samples)), this.#s.abort = !0; const i = t - this.#a; this.#a = t, this.#s = { abort: !1, st: performance.now() }; const n = await this.#l( Math.ceil(i * (this.#n / 1e6)), this.#e, this.#s ); return this.#o = 0, n; }; #a = 0; #r = 0; #i = { frameCnt: 0, data: [] }; #o = 0; #l = async (t, e = null, i) => { if (e == null || i.abort || e.state === "closed" || t === 0) return []; const n = this.#i.frameCnt - t; if (n > 0) return n < x.sampleRate / 10 && this.#c(e), K(this.#i, t); if (e.decoding) { if (performance.now() - i.st > 3e3) throw i.abort = !0, Error( `MP4Clip.tick audio timeout, ${JSON.stringify(this.#m())}` ); this.#o += 1, await H(15); } else { if (this.#r >= this.samples.length - 1) return K(this.#i, this.#i.frameCnt); this.#c(e); } return this.#l(t, e, i); }; #c = (t) => { if (t.decodeQueueSize > 10) return; const i = []; let n = this.#r; for (; n < this.samples.length; ) { const a = this.samples[n]; if (n += 1, !a.deleted && (i.push(a), i.length >= 10)) break; } this.#r = n, t.decode( i.map( (a) => new EncodedAudioChunk({ type: "key", timestamp: a.cts, duration: a.duration, data: a.data }) ) ); }; #d = () => { this.#a = 0, this.#r = 0, this.#i = { frameCnt: 0, data: [] }, this.#e?.close(), this.#e = Ht( this.conf, { resampleRate: x.sampleRate, volume: this.#t }, (t) => { this.#i.data.push(t), this.#i.frameCnt += t[0].length; } ); }; #m = () => ({ time: this.#a, decState: this.#e?.state, decQSize: this.#e?.decodeQueueSize, decCusorIdx: this.#r, sampleLen: this.samples.length, pcmLen: this.#i.frameCnt, clipIdCnt: X, sleepCnt: this.#o, memInfo: at() }); destroy = () => { this.#e = null, this.#s.abort = !0, this.#i = { frameCnt: 0, data: [] }, this.localFileReader.close(); }; } function Ht(s, t, e) { let i = 0, n = 0; const a = (h) => { if (n += 1, h.length !== 0) { if (t.volume !== 1) for (const u of h) for (let d = 0; d < u.length; d++) u[d] *= t.volume; h.length === 1 && (h = [h[0], h[0]]), e(h); } }, r = Wt(a), o = t.resampleRate !== s.sampleRate; let c = new AudioDecoder({ output: (h) => { const u = et(h); o ? r( () => Pt(u, h.sampleRate, { rate: t.resampleRate, chanCount: h.numberOfChannels }) ) : a(u), h.close(); }, error: (h) => { h.message.includes("Codec reclaimed due to inactivity") || l("MP4Clip AudioDecoder err", h); } }); c.configure(s); function l(h, u) { const d = `${h}: ${u.message}, state: ${JSON.stringify( { qSize: c.decodeQueueSize, state: c.state, inputCnt: i, outputCnt: n } )}`; throw C.error(d), Error(d); } return { decode(h) { i += h.length; try { for (const u of h) c.decode(u); } catch (u) { l("decode audio chunk error", u); } }, close() { c.state !== "closed" && c.close(); }, get decoding() { return i > n && c.decodeQueueSize > 0; }, get state() { return c.state; }, get decodeQueueSize() { return c.decodeQueueSize; } }; } function Wt(s) { const t = []; let e = 0; function i(r, o) { t[o] = r, n(); } function n() { const r = t[e]; r != null && (s(r), e += 1, n()); } let a = 0; return (r) => { const o = a; a += 1, r().then((c) => i(c, o)).catch((c) => i(c, o)); }; } function K(s, t) { const e = [new Float32Array(t), new Float32Array(t)]; let i = 0, n = 0; for (; n < s.data.length; ) { const [a, r] = s.data[n]; if (i + a.length > t) { const o = t - i; e[0].set(a.subarray(0, o), i), e[1].set(r.subarray(0, o), i), s.data[n][0] = a.subarray(o, a.length), s.data[n][1] = r.subarray(o, r.length); break; } else e[0].set(a, i), e[1].set(r, i), i += a.length, n++; } return s.data = s.data.slice(n), s.frameCnt -= t, e; } async function st(s, t) { const e = s[0], i = s.at(-1); if (i == null) return []; const n = i.offset + i.size - e.offset; if (n < 3e7) { const a = new Uint8Array( await t.read(n, { at: e.offset }) ); return s.map((r) => { const o = r.offset - e.offset; return new EncodedVideoChunk({ type: r.is_sync ? "key" : "delta", timestamp: r.cts, duration: r.duration, data: a.subarray(o, o + r.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 Xt(s, t, e) { const i = new OffscreenCanvas(s, t), n = i.getContext("2d"); return async (a) => (n.drawImage(a, 0, 0, s, t), a.close(), await i.convertToBlob(e)); } function Yt(s, t) { if (s.length === 0) return []; let e = 0, i = 0, n = -1; for (let c = 0; c < s.length; c++) { const l = s[c]; if (n === -1 && t < l.cts && (n = c - 1), l.is_idr) if (n === -1) e = c; else { i = c; break; } } const a = s[n]; if (a == null) throw Error("Not found video sample by time"); const r = s.slice(0, i === 0 ? s.length : i).map((c) => ({ ...c })); for (let c = e; c < r.length; c++) { const l = r[c]; t < l.cts && (l.deleted = !0, l.cts = -1); } L(r); const o = s.slice(a.is_idr ? n : e).map((c) => ({ ...c, cts: c.cts - t })); for (const c of o) c.cts < 0 && (c.deleted = !0, c.cts = -1); return L(o), [r, o]; } function Gt(s, t) { if (s.length === 0) return [void 0, void 0]; if (s[0].cts >= t) return [void 0, s.map((r) => ({ ...r }))]; if (s[s.length - 1].cts < t) return [s.map((r) => ({ ...r })), void 0]; let i = -1; for (let r = 0; r < s.length; r++) { const o = s[r]; if (!(t > o.cts)) { i = r; break; } } if (i === -1) throw Error("Not found audio sample by time"); const n = s.slice(0, i).map((r) => ({ ...r })), a = s.slice(i).map((r) => ({ ...r, cts: r.cts - t })); return [n, a]; } function V(s, t, e) { if (s.state === "configured") { for (let i = 0; i < t.length; i++) s.decode(t[i]); s.flush().catch((i) => { if (!(i instanceof Error)) throw i; if (i.message.includes("Decoding error") && e.onDecodingError != null) { e.onDecodingError(i); return; } if (!i.message.includes("Aborted due to close")) throw i; }); } } function jt(s, t) { if (t !== "avc1" && t !== "hvc1") return 0; const e = new DataView(s.buffer); for (let i = 0; i < s.byteLength - 4; ) { if (t === "avc1") { const n = e.getUint8(i + 4) & 31; if (n === 5 || n === 7 || n === 8) return i; } else if (t === "hvc1") { const n = e.getUint8(i + 4) >> 1 & 63; if (n === 19 || n === 20 || n === 32 || n === 33 || n === 34) return i; } i += e.getUint32(i) + 4; } return -1; } async function Jt(s, t, e, i, n, a) { const r = await t.createReader(), o = await st( s.filter( (h) => !h.deleted && h.is_sync && h.cts >= n.start && h.cts <= n.end ), r ); if (o.length === 0 || i.aborted) { a(null, !0); return; } let c = 0; V(l(), o, { onDecodingError: (h) => { C.warn("thumbnailsByKeyFrame", h), c === 0 ? V(l(!0), o, { onDecodingError: (u) => { r.close(), C.error("thumbnailsByKeyFrame retry soft deocde", u); } }) : (a(null, !0), r.close()); } }); function l(h = !1) { const u = { ...e, ...h ? { hardwareAcceleration: "prefer-software" } : {} }, d = new VideoDecoder({ output: (y) => { c += 1; const m = c === o.length; a(y, m), m && (r.close(), d.state !== "closed" && d.close()); }, error: (y) => { const m = `thumbnails decoder error: ${y.message}, config: ${JSON.stringify(u)}, state: ${JSON.stringify( { qSize: d.decodeQueueSize, state: d.state, outputCnt: c, inputCnt: o.length } )}`; throw C.error(m), Error(m); } }); return i.addEventListener("abort", () => { r.close(), d.state !== "closed" && d.close(); }), d.configure(u), d; } } function L(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 at() { 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 {}; } } class R { ready; #t = { // 微秒 duration: 0, width: 0, height: 0 }; /** * ⚠️ 静态图片的 duration 为 Infinity * * 使用 Sprite 包装时需要将它的 duration 设置为有限数 * */ get meta() { return { ...this.#t }; } #n = null; #e = []; /** * 静态图片可使用流、ImageBitmap 初始化 * * 动图需要使用 VideoFrame[] 或提供图片类型 */ constructor(t) { const e = (i) => (this.#n = i, this.#t.width = i.width, this.#t.height = i.height, this.#t.duration = 1 / 0, { ...this.#t }); 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)) { this.#e = t; const i = this.#e[0]; if (i == null) throw Error("The frame count must be greater than 0"); this.#t = { width: i.displayWidth, height: i.displayHeight, duration: this.#e.reduce( (n, a) => n + (a.duration ?? 0), 0 ) }, this.ready = Promise.resolve({ ...this.#t, duration: 1 / 0 }); } else if ("type" in t) this.ready = this.#s( t.stream, t.type ).then(() => ({ width: this.#t.width, height: this.#t.height, duration: 1 / 0 })); else throw Error("Illegal arguments"); } async #s(t, e) { this.#e = await Dt(t, e); const i = this.#e[0]; if (i == null) throw Error("No frame available in gif"); this.#t = { duration: this.#e.reduce((n, a) => n + (a.duration ?? 0), 0), width: i.codedWidth, height: i.codedHeight }, C.info("ImgClip ready:", this.#t); } tickInterceptor = async (t, e) => e; async tick(t) { if (this.#n != null) return await this.tickInterceptor(t, { video: await createImageBitmap(this.#n), state: "success" }); const e = t % this.#t.duration; return await this.tickInterceptor(t, { video: (this.#e.find( (i) => e >= i.timestamp && e <= i.timestamp + (i.duration ?? 0) ) ?? this.#e[0]).clone(), state: "success" }); } async split(t) { if (await this.ready, this.#n != null) return [ new R(await createImageBitmap(this.#n)), new R(await createImageBitmap(this.#n)) ]; let e = -1; for (let a = 0; a < this.#e.length; a++) { const r = this.#e[a]; if (!(t > r.timestamp)) { e = a; break; } } if (e === -1) throw Error("Not found frame by time"); const i = this.#e.slice(0, e).map((a) => new VideoFrame(a)), n = this.#e.slice(e).map( (a) => new VideoFrame(a, { timestamp: a.timestamp - t }) ); return [new R(i), new R(n)]; } async clone() { await this.ready; const t = this.#n == null ? this.#e.map((i) => i.clone()) : await createImageBitmap(this.#n), e = new R(t); return e.tickInterceptor = this.tickInterceptor, e; } destroy() { C.info("ImgClip destroy"), this.#n?.close(), this.#e.forEach((t) => t.close()); } } class A { static ctx = null; ready; #t = { // 微秒 duration: 0, width: 0, height: 0 }; /** * 音频元信息 * * ⚠️ 注意,这里是转换后(标准化)的元信息,非原始音频元信息 */ get meta() { return { ...this.#t, sampleRate: x.sampleRate, chanCount: 2 }; } // 使用类型断言来避免 ArrayBufferLike 和 ArrayBuffer 的类型兼容性问题 #n = new Float32Array(); #e = new Float32Array(); /** * 获取音频素材完整的 PCM 数据 */ getPCMData() { return [this.#n, this.#e]; } #s; /** * * @param dataSource 音频文件流 * @param opts 音频配置,控制音量、是否循环 */ constructor(t, e = {}) { this.#s = { loop: !1, volume: 1, ...e }, this.ready = this.#a(t).then(() => ({ // audio 没有宽高,无需绘制 width: 0, height: 0, duration: e.loop ? 1 / 0 : this.#t.duration })); } async #a(t) { A.ctx == null && (A.ctx = new AudioContext({ sampleRate: x.sampleRate })); const e = performance.now(), i = t instanceof ReadableStream ? await Kt(t, A.ctx) : t; C.info("Audio clip decoded complete:", performance.now() - e); const n = this.#s.volume; if (n !== 1) for (const a of i) for (let r = 0; r < a.length; r += 1) a[r] *= n; this.#t.duration = i[0].length / x.sampleRate * 1e6, this.#n = i[0], this.#e = i[1] ?? this.#n, C.info( "Audio clip convert to AudioData, time:", performance.now() - e ); } /** * 拦截 {@link AudioClip.tick} 方法返回的数据,用于对音频数据二次处理 * @param time 调用 tick 的时间 * @param tickRet tick 返回的数据 * * @see [移除视频绿幕背景](https://webav-tech.github.io/WebAV/demo/3_2-chromakey-video) */ tickInterceptor = async (t, e) => e; // 微秒 #r = 0; #i = 0; /** * 返回上次与当前时刻差对应的音频 PCM 数据; * * 若差值超过 3s 或当前时间小于上次时间,则重置状态 * @example * tick(0) // => [] * tick(1e6) // => [leftChanPCM(1s), rightChanPCM(1s)] * */ async tick(t) { if (!this.#s.loop && t >= this.#t.duration) return await this.tickInterceptor(t, { audio: [], state: "done" }); const e = t - this.#r; if (t < this.#r || e > 3e6) return this.#r = t, this.#i = Math.ceil( this.#r / 1e6 * x.sampleRate ), await this.tickInterceptor(t, { audio: [new Float32Array(0), new Float32Array(0)], state: "success" }); this.#r = t; const i = Math.ceil( e / 1e6 * x.sampleRate ), n = this.#i + i, a = this.#s.loop ? [ z(this.#n, this.#i, n), z(this.#e, this.#i, n) ] : [ this.#n.slice(this.#i, n), this.#e.slice(this.#i, n) ]; return this.#i = n, await this.tickInterceptor(t, { audio: a, state: "success" }); } /** * 按指定时间切割,返回前后两个音频素材 * @param time 时间,单位微秒 */ async split(t) { await this.ready; const e = Math.ceil(t / 1e6 * x.sampleRate), i = new A( this.getPCMData().map((a) => a.slice(0, e)), this.#s ), n = new A( this.getPCMData().map((a) => a.slice(e)), this.#s ); return [i, n]; } async clone() { await this.ready; const t = new A(this.getPCMData(), this.#s); return await t.ready, t; } /** * 销毁实例,释放资源 */ destroy() { this.#n = new Float32Array(0), this.#e = new Float32Array(0), C.info("---- audioclip destroy ----"); } static concatAudioClip = Qt; } async function Qt(s, t) { const e = []; for (const i of s) await i.ready, e.push(i.getPCMData()); return new A(tt(e), t); } async function Kt(s, t) { const e = await new Response(s).arrayBuffer(); return $(await t.decodeAudioData(e)); } class rt { static ctx = null; ready; #t = { // 微秒 duration: 0, width: 0, height: 0 }; get meta() { return { ...this.#t }; } #n = () => { }; /** * 实时流的音轨 */ audioTrack; #e = null; #s; constructor(t) { this.#s = t, this.audioTrack = t.getAudioTracks()[0] ?? null, this.#t.duration = 1 / 0; const e = t.getVideoTracks()[0]; e != null ? (e.contentHint = "motion", this.ready = new Promise((i) => { this.#n = qt(e, (n) => { this.#t.width = n.width, this.#t.height = n.height, this.#e = n, i(this.meta); }); })) : this.ready = Promise.resolve(this.meta); } async tick() { return { video: this.#e == null ? null : await createImageBitmap(this.#e), audio: [], state: "success" }; } async split() { return [await this.clone(), await this.clone()]; } async clone() { return new rt(this.#s.clone()); } destroy() { this.#s.getTracks().forEach((t) => t.stop()), this.#n(); } } function qt(s, t) { let e = !1, i; return U( new MediaStreamTrackProcessor({ track: s }).readable, { onChunk: async (n) => { if (!e) { const { displayHeight: a, displayWidth: r } = n, o = r ?? 0, c = a ?? 0, l = new OffscreenCanvas(o, c); i = l.getContext("2d"), t(l), e = !0; } i.drawImage(n, 0, 0), n.close(); }, onDone: async () => { } } ); } class E { ready; #t = []; #n = { width: 0, height: 0, duration: 0 }; get meta() { return { ...this.#n }; } #e = { 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" }; #s; #a; #r = null; #i = 0; #o = 0; constructor(t, e) { if (this.#t = Array.isArray(t) ? t : Zt(t).map(({ start: h, end: u, text: d }) => ({ start: h * 1e6, end: u * 1e6, text: d })), this.#t.length === 0) throw Error("No subtitles content"); this.#e = Object.assign(this.#e, e), this.#o = e.textBgColor == null ? 0 : (e.fontSize ?? 50) * 0.2; const { fontSize: i, fontFamily: n, fontWeight: a, fontStyle: r, videoWidth: o, videoHeight: c, letterSpacing: l } = this.#e; this.#i = i + this.#o * 2, this.#s = new OffscreenCanvas(o, c), this.#a = this.#s.getContext("2d"), this.#a.font = `${r} ${a} ${i}px ${n}`, this.#a.textAlign = "center", this.#a.textBaseline = "top", this.#a.letterSpacing = l ?? "0px", this.#n = {