From b1281ea286f5355055698c06da3e3b8db9e7f9ab Mon Sep 17 00:00:00 2001 From: dumbmoron Date: Mon, 19 Aug 2024 18:03:46 +0000 Subject: [PATCH] web/libav: move encoding to separate file, add webcodecs bridge --- pnpm-lock.yaml | 18 ++ web/package.json | 1 + web/src/lib/libav/{libav.ts => encode.ts} | 288 ++++------------------ web/src/lib/libav/remux.ts | 211 ++++++++++++++++ web/src/lib/libav/webcodecs.ts | 113 +++++++++ web/src/lib/types/libav.ts | 5 +- web/src/routes/convert/+page.svelte | 2 +- web/src/routes/remux/+page.svelte | 3 +- 8 files changed, 397 insertions(+), 244 deletions(-) rename web/src/lib/libav/{libav.ts => encode.ts} (59%) create mode 100644 web/src/lib/libav/remux.ts create mode 100644 web/src/lib/libav/webcodecs.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6709382e..633f1501 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -97,6 +97,9 @@ importers: '@imput/libav.js-remux-cli': specifier: ^5.7.6 version: 5.7.6 + '@imput/libavjs-webcodecs-polyfill': + specifier: ^0.5.1 + version: 0.5.1 '@imput/version-info': specifier: workspace:^ version: link:../packages/version-info @@ -109,6 +112,9 @@ importers: libavjs-webcodecs-bridge: specifier: ^0.1.0 version: 0.1.0 + libavjs-webcodecs-polyfill: + specifier: link:/home/j/libavjs-webcodecs-polyfill + version: link:../../libavjs-webcodecs-polyfill mime: specifier: ^4.0.4 version: 4.0.4 @@ -541,6 +547,9 @@ packages: '@imput/libav.js-remux-cli@5.7.6': resolution: {integrity: sha512-ofSSLjRF9RfZ3QMBlb7fhxso8p8xlDqU4qX8eCJCukCB15g7iBShthCyGVnYz+3lLoFu9klbvVal9bEncBj/FQ==} + '@imput/libavjs-webcodecs-polyfill@0.5.1': + resolution: {integrity: sha512-uQ5vawG/4ppLKDumkg8BjnvqKWzoKXxt7cj0lvTpB9ph1hiuzjNx8wmwMcXbeTVAzRcts+GtCTPpSF9rFosYBg==} + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -795,6 +804,9 @@ packages: resolution: {integrity: sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==} engines: {node: ^18.18.0 || >=20.0.0} + '@ungap/global-this@0.4.4': + resolution: {integrity: sha512-mHkm6FvepJECMNthFuIgpAEFmPOk71UyXuIxYfjytvFTnSDBIz7jmViO+LfHI/AjrazWije0PnSP3+/NlwzqtA==} + '@ungap/structured-clone@1.2.0': resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} @@ -2513,6 +2525,10 @@ snapshots: '@imput/libav.js-remux-cli@5.7.6': {} + '@imput/libavjs-webcodecs-polyfill@0.5.1': + dependencies: + '@ungap/global-this': 0.4.4 + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -2772,6 +2788,8 @@ snapshots: '@typescript-eslint/types': 7.18.0 eslint-visitor-keys: 3.4.3 + '@ungap/global-this@0.4.4': {} + '@ungap/structured-clone@1.2.0': {} '@vitejs/plugin-basic-ssl@1.1.0(vite@5.3.5(@types/node@20.14.14))': diff --git a/web/package.json b/web/package.json index 2d41e9e0..1fab9977 100644 --- a/web/package.json +++ b/web/package.json @@ -51,6 +51,7 @@ "@fontsource/ibm-plex-mono": "^5.0.13", "@imput/libav.js-encode-cli": "^5.7.6", "@imput/libav.js-remux-cli": "^5.7.6", + "@imput/libavjs-webcodecs-polyfill": "^0.5.1", "@imput/version-info": "workspace:^", "@tabler/icons-svelte": "3.6.0", "@vitejs/plugin-basic-ssl": "^1.1.0", diff --git a/web/src/lib/libav/libav.ts b/web/src/lib/libav/encode.ts similarity index 59% rename from web/src/lib/libav/libav.ts rename to web/src/lib/libav/encode.ts index a94c584d..a1415796 100644 --- a/web/src/lib/libav/libav.ts +++ b/web/src/lib/libav/encode.ts @@ -1,7 +1,5 @@ -import mime from "mime"; import LibAV, { type LibAV as LibAVInstance, type Packet, type Stream } from "@imput/libav.js-encode-cli"; -import type { Chunk, ChunkMetadata, Decoder, FFmpegProgressCallback, FFmpegProgressEvent, FFmpegProgressStatus, FileInfo, OutputStream, RenderingPipeline, RenderParams } from "../types/libav"; -import type { FfprobeData } from "fluent-ffmpeg"; +import type { Chunk, ChunkMetadata, Decoder, FFmpegProgressCallback, OutputStream, Pipeline, RenderingPipeline } from "../types/libav"; import * as LibAVWebCodecs from "libavjs-webcodecs-bridge"; import { BufferStream } from "./buffer-stream"; import { BufferStream } from "../buffer-stream"; @@ -31,7 +29,7 @@ export default class LibAVWrapper { base: '/_libav' }); - this.webcodecs = new WebCodecsWrapper(await this.libav); + this.webcodecs = new WebCodecsWrapper(this.libav); } } @@ -44,166 +42,18 @@ export default class LibAVWrapper { async #get() { if (!this.libav) throw new Error("LibAV wasn't initialized"); - if (!this.webcodecs) throw new Error("unreachable"); + const libav = await this.libav; + + if (!this.webcodecs) { + throw new Error("unreachable"); + } return { - libav: await this.libav, + libav, webcodecs: this.webcodecs }; } - async probe(blob: Blob) { - const { libav } = await this.#get(); - - await libav.mkreadaheadfile('input', blob); - - try { - await libav.ffprobe([ - '-v', 'quiet', - '-print_format', 'json', - '-show_format', - '-show_streams', - 'input', - '-o', 'output.json' - ]); - - const copy = await libav.readFile('output.json'); - const text = new TextDecoder().decode(copy); - await libav.unlink('output.json'); - - return JSON.parse(text) as FfprobeData; - } finally { - await libav.unlinkreadaheadfile('input'); - } - } - - static getExtensionFromType(blob: Blob) { - const extensions = mime.getAllExtensions(blob.type); - const overrides = ['mp3', 'mov']; - - if (!extensions) - return; - - for (const override of overrides) - if (extensions?.has(override)) - return override; - - return [...extensions][0]; - } - - async remux({ blob, output, args }: RenderParams) { - const { libav } = await this.#get(); - - const inputKind = blob.type.split("/")[0]; - const inputExtension = LibAVWrapper.getExtensionFromType(blob); - - if (inputKind !== "video" && inputKind !== "audio") return; - if (!inputExtension) return; - - const input: FileInfo = { - kind: inputKind, - extension: inputExtension, - } - - if (!output) output = input; - - output.type = mime.getType(output.extension); - if (!output.type) return; - - const outputName = `output.${output.extension}`; - - try { - await libav.mkreadaheadfile("input", blob); - - // https://github.com/Yahweasel/libav.js/blob/7d359f69/docs/IO.md#block-writer-devices - await libav.mkwriterdev(outputName); - await libav.mkwriterdev('progress.txt'); - - const MB = 1024 * 1024; - const chunks: Uint8Array[] = []; - const chunkSize = Math.min(512 * MB, blob.size); - - // since we expect the output file to be roughly the same size - // as the original, preallocate its size for the output - for (let toAllocate = blob.size; toAllocate > 0; toAllocate -= chunkSize) { - chunks.push(new Uint8Array(chunkSize)); - } - - let actualSize = 0; - libav.onwrite = (name, pos, data) => { - if (name === 'progress.txt') { - try { - return this.#emitProgress(data); - } catch(e) { - console.error(e); - } - } else if (name !== outputName) return; - - const writeEnd = pos + data.length; - if (writeEnd > chunkSize * chunks.length) { - chunks.push(new Uint8Array(chunkSize)); - } - - const chunkIndex = pos / chunkSize | 0; - const offset = pos - (chunkSize * chunkIndex); - - if (offset + data.length > chunkSize) { - chunks[chunkIndex].set( - data.subarray(0, chunkSize - offset), offset - ); - chunks[chunkIndex + 1].set( - data.subarray(chunkSize - offset), 0 - ); - } else { - chunks[chunkIndex].set(data, offset); - } - - actualSize = Math.max(writeEnd, actualSize); - }; - - await libav.ffmpeg([ - '-nostdin', '-y', - '-loglevel', 'error', - '-progress', 'progress.txt', - '-threads', this.concurrency.toString(), - '-i', 'input', - ...args, - outputName - ]); - - // if we didn't need as much space as we allocated for some reason, - // shrink the buffers so that we don't inflate the file with zeroes - const outputView: Uint8Array[] = []; - - for (let i = 0; i < chunks.length; ++i) { - outputView.push( - chunks[i].subarray( - 0, Math.min(chunkSize, actualSize) - ) - ); - - actualSize -= chunkSize; - if (actualSize <= 0) { - break; - } - } - - const renderBlob = new Blob( - outputView, - { type: output.type } - ); - - if (renderBlob.size === 0) return; - return renderBlob; - } finally { - try { - await libav.unlink(outputName); - await libav.unlink('progress.txt'); - await libav.unlinkreadaheadfile("input"); - } catch { /* catch & ignore */ } - } - } - async transcode(blob: Blob) { const { libav } = await this.#get(); let fmtctx; @@ -216,6 +66,12 @@ export default class LibAVWrapper { const pipes: RenderingPipeline[] = []; const output_streams: OutputStream[] = []; for (const stream of streams) { + if (stream.codec_id === 61) { + pipes.push(null); + output_streams.push(null); + continue; + } + const { pipe, stream: ostream @@ -246,6 +102,8 @@ export default class LibAVWrapper { async #decodeStreams(fmt_ctx: number, pipes: RenderingPipeline[], streams: Stream[]) { for await (const { index, packet } of this.#demux(fmt_ctx)) { + if (pipes[index] === null) continue; + const { decoder } = pipes[index]; this.#decodePacket(decoder.instance, packet, streams[index]); @@ -264,16 +122,18 @@ export default class LibAVWrapper { } } - for (const { decoder } of pipes) { - await decoder.instance.flush(); - decoder.instance.close(); - decoder.output.push(null); + for (const pipe of pipes) { + if (pipe !== null) { + await pipe.decoder.instance.flush(); + pipe.decoder.instance.close(); + pipe.decoder.output.push(null); + } } } async #encodeStream( - frames: RenderingPipeline['decoder']['output'], - { instance: encoder, output }: RenderingPipeline['encoder'] + frames: Pipeline['decoder']['output'], + { instance: encoder, output }: Pipeline['encoder'] ) { const reader = frames.getReader(); @@ -312,7 +172,9 @@ export default class LibAVWrapper { async #encodeStreams(pipes: RenderingPipeline[]) { return Promise.all( - pipes.map( + pipes + .filter(p => p !== null) + .map( ({ decoder, encoder }) => { return this.#encodeStream(decoder.output, encoder); } @@ -321,6 +183,7 @@ export default class LibAVWrapper { } async #processChunk({ chunk, metadata }: { chunk: Chunk, metadata: ChunkMetadata }, ostream: OutputStream, index: number) { + if (ostream === null) return; const { libav } = await this.#get(); let convertToPacket; @@ -343,7 +206,8 @@ export default class LibAVWrapper { const starterPackets = [], readers: ReadableStreamDefaultReader[] = []; for (let i = 0; i < ostreams.length; ++i) { - readers[i] = pipes[i].encoder.output.getReader(); + if (pipes[i] === null) continue; + readers[i] = pipes[i]!.encoder.output.getReader(); const { done, value } = await readers[i].read(); if (done) throw "this should not happen"; @@ -373,7 +237,7 @@ export default class LibAVWrapper { device: true, open: true, codecpars: true - }, ostreams + }, ostreams.filter(a => a !== null) ); await libav.avformat_write_header(output_ctx, 0); @@ -453,18 +317,18 @@ export default class LibAVWrapper { } async #createEncoder(stream: Stream, codec: string) { - const { libav } = await this.#get(); + const { libav, webcodecs } = await this.#get(); - let streamToConfig, configToStream, Encoder; + let streamToConfig, configToStream, initEncoder; if (stream.codec_type === libav.AVMEDIA_TYPE_VIDEO) { streamToConfig = LibAVWebCodecs.videoStreamToConfig; configToStream = LibAVWebCodecs.configToVideoStream; - Encoder = VideoEncoder; + initEncoder = webcodecs!.initVideoEncoder.bind(webcodecs); } else if (stream.codec_type === libav.AVMEDIA_TYPE_AUDIO) { streamToConfig = LibAVWebCodecs.audioStreamToConfig; configToStream = LibAVWebCodecs.configToAudioStream; - Encoder = AudioEncoder; + initEncoder = webcodecs.initAudioEncoder.bind(webcodecs); codec = 'mp4a.40.29'; } else throw "Unknown type: " + stream.codec_type; @@ -481,48 +345,41 @@ export default class LibAVWrapper { sampleRate: config.sampleRate }; - let { supported } = await Encoder.isConfigSupported(encoderConfig); - if (!supported) { - throw "cannot encode " + codec; - } const output = new BufferStream< { chunk: Chunk, metadata: ChunkMetadata } >(); - const encoder = new Encoder({ + const encoder = await initEncoder(encoderConfig, { output: (chunk, metadata = {}) => { output.push({ chunk, metadata }) }, error: console.error }); - encoder.configure(encoderConfig); + if (!encoder) { + throw "cannot encode " + codec; + } - const c2s = await configToStream(libav, encoderConfig); - - // FIXME: figure out a proper way to handle timescale - // (preferrably without killing self) - c2s[1] = 1; - c2s[2] = 60000; + const encoderStream = await configToStream(libav, encoderConfig); return { pipe: { instance: encoder, output }, - stream: c2s + stream: encoderStream }; } async #createDecoder(stream: Stream) { - const { libav } = await this.#get(); + const { libav, webcodecs } = await this.#get(); let streamToConfig, initDecoder; if (stream.codec_type === libav.AVMEDIA_TYPE_VIDEO) { streamToConfig = LibAVWebCodecs.videoStreamToConfig; - initDecoder = this.webcodecs.initVideoDecoder; + initDecoder = webcodecs.initVideoDecoder.bind(webcodecs); } else if (stream.codec_type === libav.AVMEDIA_TYPE_AUDIO) { streamToConfig = LibAVWebCodecs.audioStreamToConfig; - initDecoder = this.webcodecs.initAudioDecoder; + initDecoder = webcodecs.initAudioDecoder.bind(webcodecs); } else throw "Unknown type: " + stream.codec_type; const config = await streamToConfig(libav, stream); @@ -531,65 +388,16 @@ export default class LibAVWrapper { throw "could not make decoder config"; } - let { supported } = await Decoder.isConfigSupported(config); - if (!supported) { - throw "cannot decode " + config.codec; - } - const output = new BufferStream(); - const decoder = new Decoder({ + const decoder = await initDecoder(config, { output: frame => output.push(frame), error: console.error }); - decoder.configure(config); - return { instance: decoder, output } - } - - #emitProgress(data: Uint8Array | Int8Array) { - if (!this.onProgress) return; - - const copy = new Uint8Array(data); - const text = new TextDecoder().decode(copy); - const entries = Object.fromEntries( - text.split('\n') - .filter(a => a) - .map(a => a.split('=', )) - ); - - const status: FFmpegProgressStatus = (() => { - const { progress } = entries; - - if (progress === 'continue' || progress === 'end') { - return progress; - } - - return "unknown"; - })(); - - const tryNumber = (str: string, transform?: (n: number) => number) => { - if (str) { - const num = Number(str); - if (!isNaN(num)) { - if (transform) - return transform(num); - else - return num; - } - } + if (!decoder) { + throw "cannot decode " + config.codec; } - const progress: FFmpegProgressEvent = { - status, - frame: tryNumber(entries.frame), - fps: tryNumber(entries.fps), - total_size: tryNumber(entries.total_size), - dup_frames: tryNumber(entries.dup_frames), - drop_frames: tryNumber(entries.drop_frames), - speed: tryNumber(entries.speed?.trim()?.replace('x', '')), - out_time_sec: tryNumber(entries.out_time_us, n => Math.floor(n / 1e6)) - }; - - this.onProgress(progress); + return { instance: decoder, output } } } diff --git a/web/src/lib/libav/remux.ts b/web/src/lib/libav/remux.ts new file mode 100644 index 00000000..c3371a5e --- /dev/null +++ b/web/src/lib/libav/remux.ts @@ -0,0 +1,211 @@ +import mime from "mime"; +import LibAV, { type LibAV as LibAVInstance } from "@imput/libav.js-remux-cli"; +import type { FFmpegProgressCallback, FFmpegProgressEvent, FFmpegProgressStatus, FileInfo, RenderParams } from "../types/libav"; +import type { FfprobeData } from "fluent-ffmpeg"; + +export default class LibAVWrapper { + libav: Promise | null; + concurrency: number; + onProgress?: FFmpegProgressCallback; + + constructor(onProgress?: FFmpegProgressCallback) { + this.libav = null; + this.concurrency = Math.min(4, navigator.hardwareConcurrency); + this.onProgress = onProgress; + } + + async init() { + if (!this.libav) { + this.libav = LibAV.LibAV({ + yesthreads: true, + base: '/_libav' + }); + } + } + + async #get() { + if (!this.libav) throw new Error("LibAV wasn't initialized"); + + return { + libav: await this.libav + }; + } + + async probe(blob: Blob) { + const { libav } = await this.#get(); + + const OUT_FILE = 'output.json'; + await libav.mkreadaheadfile('input', blob); + await libav.mkwriterdev(OUT_FILE); + + let writtenData = new Uint8Array(0); + + libav.onwrite = (name, pos, data) => { + if (name !== OUT_FILE) return; + + const newLen = Math.max(pos + data.length, writtenData.length); + if (newLen > writtenData.length) { + const newData = new Uint8Array(newLen); + newData.set(writtenData); + writtenData = newData; + } + writtenData.set(data, pos); + }; + + await libav.ffprobe([ + '-v', 'quiet', + '-print_format', 'json', + '-show_format', + '-show_streams', + 'input', + '-o', OUT_FILE + ]); + + await libav.unlink(OUT_FILE); + await libav.unlinkreadaheadfile('input'); + + const copy = new Uint8Array(writtenData); + const text = new TextDecoder().decode(copy); + return JSON.parse(text) as FfprobeData; + } + + static getExtensionFromType(blob: Blob) { + const extensions = mime.getAllExtensions(blob.type); + const overrides = ['mp3', 'mov']; + + if (!extensions) + return; + + for (const override of overrides) + if (extensions?.has(override)) + return override; + + return [...extensions][0]; + } + + async remux({ blob, output, args }: RenderParams) { + const { libav } = await this.#get(); + + const inputKind = blob.type.split("/")[0]; + const inputExtension = LibAVWrapper.getExtensionFromType(blob); + + if (inputKind !== "video" && inputKind !== "audio") return; + if (!inputExtension) return; + + const input: FileInfo = { + kind: inputKind, + extension: inputExtension, + } + + if (!output) output = input; + + output.type = mime.getType(output.extension); + if (!output.type) return; + + const outputName = `output.${output.extension}`; + + await libav.mkreadaheadfile("input", blob); + + // https://github.com/Yahweasel/libav.js/blob/7d359f69/docs/IO.md#block-writer-devices + await libav.mkwriterdev(outputName); + await libav.mkwriterdev('progress.txt'); + + // since we expect the output file to be roughly the same size + // as the original, preallocate its size for the output + let writtenData = new Uint8Array(blob.size), actualSize = 0; + + libav.onwrite = (name, pos, data) => { + if (name === 'progress.txt') { + try { + return this.#emitProgress(data); + } catch(e) { + console.error(e); + } + } else if (name !== outputName) return; + + actualSize = Math.max(pos + data.length, actualSize); + const newLen = Math.max(pos + data.length, writtenData.length); + if (newLen > writtenData.length) { + const newData = new Uint8Array(newLen); + newData.set(writtenData); + writtenData = newData; + } + writtenData.set(data, pos); + }; + + await libav.ffmpeg([ + '-nostdin', '-y', + '-loglevel', 'error', + '-progress', 'progress.txt', + '-threads', this.concurrency.toString(), + '-i', 'input', + ...args, + outputName + ]); + + await libav.unlink(outputName); + await libav.unlink('progress.txt'); + await libav.unlinkreadaheadfile("input"); + + // if we didn't need as much space as we allocated for some reason, + // shrink the buffer so that we don't inflate the file with zeros + if (writtenData.length > actualSize) { + writtenData = writtenData.slice(0, actualSize); + } + + const renderBlob = new Blob( + [ writtenData ], + { type: output.type } + ); + + if (renderBlob.size === 0) return; + return renderBlob; + } + + #emitProgress(data: Uint8Array | Int8Array) { + if (!this.onProgress) return; + + const copy = new Uint8Array(data); + const text = new TextDecoder().decode(copy); + const entries = Object.fromEntries( + text.split('\n') + .filter(a => a) + .map(a => a.split('=', )) + ); + + const status: FFmpegProgressStatus = (() => { + const { progress } = entries; + + if (progress === 'continue' || progress === 'end') { + return progress; + } + + return "unknown"; + })(); + + const tryNumber = (str: string, transform?: (n: number) => number) => { + if (str) { + const num = Number(str); + if (!isNaN(num)) { + if (transform) + return transform(num); + else + return num; + } + } + } + + const progress: FFmpegProgressEvent = { + status, + frame: tryNumber(entries.frame), + fps: tryNumber(entries.fps), + total_size: tryNumber(entries.total_size), + dup_frames: tryNumber(entries.dup_frames), + drop_frames: tryNumber(entries.drop_frames), + speed: tryNumber(entries.speed?.trim()?.replace('x', '')), + out_time_sec: tryNumber(entries.out_time_us, n => Math.floor(n / 1e6)) + }; + + this.onProgress(progress); + } +} \ No newline at end of file diff --git a/web/src/lib/libav/webcodecs.ts b/web/src/lib/libav/webcodecs.ts new file mode 100644 index 00000000..91fffecb --- /dev/null +++ b/web/src/lib/libav/webcodecs.ts @@ -0,0 +1,113 @@ +import type { LibAV } from "@imput/libav.js-encode-cli"; +import * as LibAVPolyfill from "@imput/libavjs-webcodecs-polyfill"; + +const has = (obj: T, key: string) => { + return key in obj && typeof (obj as Record)[key] !== 'undefined'; +} + +export default class WebCodecsWrapper { + #libav: Promise; + #ready: Promise | undefined; + + constructor(libav: Promise) { + this.#libav = libav; + } + + async #load() { + if (typeof this.#ready === 'undefined') { + this.#ready = LibAVPolyfill.load({ + polyfill: false, + LibAV: await this.#libav + }); + } + + await this.#ready; + } + async #getDecoder(config: VideoDecoderConfig | AudioDecoderConfig) { + if (has(config, 'numberOfChannels') && has(config, 'sampleRate')) { + const audioConfig = config as AudioDecoderConfig; + + if ('AudioDecoder' in window && await window.AudioDecoder.isConfigSupported(audioConfig)) + return window.AudioDecoder; + + await this.#load(); + if (await LibAVPolyfill.AudioDecoder.isConfigSupported(audioConfig)) + return LibAVPolyfill.AudioDecoder; + } else { + const videoConfig = config as VideoDecoderConfig; + if ('VideoDecoder' in window && await window.VideoDecoder.isConfigSupported(videoConfig)) + return window.VideoDecoder; + + await this.#load(); + if (await LibAVPolyfill.VideoDecoder.isConfigSupported(videoConfig)) + return LibAVPolyfill.VideoDecoder; + } + + return null; + } + + async #getEncoder(config: VideoEncoderConfig | AudioEncoderConfig) { + if (has(config, 'numberOfChannels') && has(config, 'sampleRate')) { + const audioConfig = config as AudioEncoderConfig; + if ('AudioEncoder' in window && await window.AudioEncoder.isConfigSupported(audioConfig)) + return window.AudioEncoder; + + await this.#load(); + if (await LibAVPolyfill.AudioEncoder.isConfigSupported(audioConfig)) + return LibAVPolyfill.AudioEncoder; + } else if (has(config, 'width') && has(config, 'height')) { + const videoConfig = config as VideoEncoderConfig; + if ('VideoEncoder' in window && await window.VideoEncoder.isConfigSupported(videoConfig)) + return window.VideoEncoder; + + await this.#load(); + if (await LibAVPolyfill.VideoEncoder.isConfigSupported(videoConfig)) + return LibAVPolyfill.VideoEncoder; + } else throw new Error("unreachable"); + + return null; + } + + // FIXME: this is nasty, but whatever + async initAudioEncoder(config: AudioEncoderConfig, init: AudioEncoderInit) { + console.log(this); + const Encoder = await this.#getEncoder(config) as typeof AudioEncoder | null; + if (Encoder === null) return null; + + const encoder = new Encoder(init); + encoder.configure(config); + + return encoder; + } + + async initAudioDecoder(config: AudioDecoderConfig, init: AudioDecoderInit) { + const Decoder = await this.#getDecoder(config) as typeof AudioDecoder | null; + if (Decoder === null) return null; + + const decoder = new Decoder(init); + decoder.configure(config); + + return decoder; + } + + async initVideoEncoder(config: VideoEncoderConfig, init: VideoEncoderInit) { + const Encoder = await this.#getEncoder(config) as typeof VideoEncoder | null; + if (Encoder === null) return null; + + const encoder = new Encoder(init); + encoder.configure(config); + + return encoder; + } + + async initVideoDecoder(config: VideoDecoderConfig, init: VideoDecoderInit) { + const Decoder = await this.#getDecoder(config) as typeof VideoDecoder | null; + if (Decoder === null) return null; + + const decoder = new Decoder(init); + decoder.configure(config); + + return decoder; + } + +} \ No newline at end of file diff --git a/web/src/lib/types/libav.ts b/web/src/lib/types/libav.ts index 797aa866..7e9c5216 100644 --- a/web/src/lib/types/libav.ts +++ b/web/src/lib/types/libav.ts @@ -62,5 +62,6 @@ export type VideoPipeline = { } } -export type RenderingPipeline = AudioPipeline | VideoPipeline; -export type OutputStream = [number, number, number]; +export type Pipeline = AudioPipeline | VideoPipeline; +export type RenderingPipeline = Pipeline | null; +export type OutputStream = [number, number, number] | null; diff --git a/web/src/routes/convert/+page.svelte b/web/src/routes/convert/+page.svelte index f54a899e..560e1d29 100644 --- a/web/src/routes/convert/+page.svelte +++ b/web/src/routes/convert/+page.svelte @@ -1,5 +1,5 @@