UNPKG

transavormer

Version:

Transform (transcode, transmux, etc) audio/video formats using libav.js and WebCodecs

284 lines (255 loc) 9.8 kB
/* * Copyright (c) 2024 Yahweasel * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ import type * as LibAVT from "@libav.js/types"; import * as ifs from "./interfaces"; import * as norm from "./normalizer"; /** * Frontend for libav filters. */ export class LAFilter implements ifs.LibAVFrameStream { constructor( /** * @private * libav.js instance. */ private _libav: LibAVT.LibAV, /** * @private * Initializers (for filtergraph descriptions). */ private _init: ifs.InitLAFilter, /** * @private * Demuxed input. */ private _rawInputP: Promise<ifs.FrameStreamAny> ) { this.ptr = <false> !!_init.ptr; this.stream = new ReadableStream({}); this.streams = Promise.resolve([]); } /** * @private * Filters must be initialized. */ private async _initialize() { const la = this._libav; const rawInput = await this._rawInputP; let input: ifs.LibAVFrameStreamAny; if (rawInput.streamType !== "libav-frame") { // Need to convert the frames to libav first input = await norm.FrameNormalizer.build( la, { type: "frame-normalizer", ptr: true, input: rawInput }, this._rawInputP ); } else { input = <ifs.LibAVFrameStreamAny> rawInput; } this.streams = input.streams; const streams = await input.streams; const frameStream = input.stream.getReader(); const frame = await la.av_frame_alloc(); const destructors: (()=>Promise<unknown>)[] = []; async function cleanup() { for (const destructor of destructors) await destructor(); } destructors.push(async () => { await la.av_frame_free_js(frame); }); // Filters, per stream let filters: Record<number, [number, number, number]> = {}; // Create the stream this.stream = new ReadableStream({ pull: async (controller) => { while (true) { // Get some data to encode const inFrameData = await frameStream.read(); const eof = inFrameData.done; const inFrames = inFrameData.value || []; if (!eof && inFrames.length === 0) { /* Empty array indicates a seek. Dump current data to * reinitialize our filters. */ for (const idx in filters) { const filter = filters[idx]; await la.avfilter_graph_free_js(filter[0]); } filters = {}; controller.enqueue([]); break; } // Group frames by stream const framesByStr: Record< number, (number|LibAVT.Frame)[] > = {}; for (const frame of inFrames) { const streamIndex = frame.streamIndex; framesByStr[streamIndex] = framesByStr[streamIndex] || []; framesByStr[streamIndex].push(frame.frame); } let ret: ifs.LibAVStreamFrame[] = []; // Filter for (let i = 0; i < streams.length; i++) { if (!framesByStr[i]) continue; const frames = framesByStr[i]; if (!filters[i]) { let descr: string; let ios: Partial<LibAVT.FilterIOSettings>; if (streams[i].codec_type === la.AVMEDIA_TYPE_VIDEO) { descr = this._init.videoFilters || "null"; ios = this._init.videoIOSettings || {}; } else { descr = this._init.audioFilters || "aresample"; ios = this._init.audioIOSettings || {}; } filters[i] = await mkFilter( la, descr, ios, streams[i], <any> frames[0] ); destructors.push(async () => { await la.avfilter_graph_free_js(filters[i][0]); }); } const [, bufferSrc, bufferSink] = filters[i]; const ffs = await la.ff_filter_multi( bufferSrc, bufferSink, frame, <LibAVT.Frame[]> frames, { fin: eof, copyoutFrame: <"default"> (this._init.ptr ? "ptr" : "default") } ); ret.push.apply(ret, ffs.map(x => ({ streamIndex: i, frame: x }))); } if (ret.length) { // FIXME: Make sure they're still in temporal order controller.enqueue(ret); } if (eof) { await cleanup(); controller.close(); } if (ret.length || eof) break; } } }); } async sendCommands(cmds: ifs.Command[]): Promise<ifs.CommandResult[]> { const input = await this._rawInputP; return input.sendCommands(cmds); } static async build( libav: LibAVT.LibAV, init: ifs.InitLAFilter, input: Promise<ifs.FrameStreamAny> ): Promise<ifs.LibAVFrameStream> static async build( libav: LibAVT.LibAV, init: ifs.InitLAFilterPtr, input: Promise<ifs.FrameStreamAny> ): Promise<ifs.LibAVFrameStreamPtr>; /** * Build a encoder. */ static async build( libav: LibAVT.LibAV, init: ifs.InitLAFilter | ifs.InitLAFilterPtr, input: Promise<ifs.FrameStreamAny> ): Promise<ifs.LibAVFrameStreamAny> { const ret = new LAFilter( libav, <ifs.InitLAFilter> init, input ); await ret._initialize(); return <any> ret; } component: ifs.Component = "la-filter"; streamType: "libav-frame" = "libav-frame"; ptr: false; /** * Stream of filtered frames. */ stream: ReadableStream<ifs.LibAVStreamFrame[]>; /** * Streams to which the frames belong. */ streams: Promise<ifs.StreamParameters[]>; } /** * @private * Make the given filter. */ async function mkFilter( la: LibAVT.LibAV, descr: string, ios: Partial<LibAVT.FilterIOSettings>, stream: LibAVT.CodecParameters, frameIn: number | LibAVT.Frame ) { let frame: LibAVT.Frame; if (typeof frameIn === "number") frame = await la.ff_copyout_frame(frameIn); else frame = frameIn; if (stream.codec_type === la.AVMEDIA_TYPE_VIDEO) { const oios: LibAVT.FilterIOSettings = { type: la.AVMEDIA_TYPE_VIDEO, width: frame.width, height: frame.height, pix_fmt: frame.format, time_base: frame.time_base_num ? [frame.time_base_num, frame.time_base_den] : [1, 1000000] }; Object.assign(oios, ios); return await la.ff_init_filter_graph( descr, { type: la.AVMEDIA_TYPE_VIDEO, width: frame.width, height: frame.height, pix_fmt: frame.format, time_base: frame.time_base_num ? [frame.time_base_num, frame.time_base_den] : void 0 }, oios ); } else { let channelLayout = 4; if (frame.channel_layout) channelLayout = frame.channel_layout; else if (frame.channels && frame.channels !== 1) channelLayout = (1 << frame.channels) - 1; const oios: LibAVT.FilterIOSettings = { sample_rate: frame.sample_rate, channel_layout: channelLayout, sample_fmt: frame.format, time_base: [1, frame.sample_rate!] }; Object.assign(oios, ios); return await la.ff_init_filter_graph( descr, { sample_rate: frame.sample_rate, channel_layout: channelLayout, sample_fmt: frame.format, time_base: frame.time_base_num ? [frame.time_base_num, frame.time_base_den] : void 0 }, oios ); } }