web/encode: refactor, split up into separately callable functions
This commit is contained in:
parent
f36088b48d
commit
64002345b5
8 changed files with 415 additions and 111 deletions
|
@ -112,6 +112,9 @@ importers:
|
|||
'@vitejs/plugin-basic-ssl':
|
||||
specifier: ^1.1.0
|
||||
version: 1.1.0(vite@5.3.5(@types/node@20.14.14))
|
||||
json-stringify-pretty-compact:
|
||||
specifier: ^4.0.0
|
||||
version: 4.0.0
|
||||
mime:
|
||||
specifier: ^4.0.4
|
||||
version: 4.0.4
|
||||
|
@ -1533,6 +1536,9 @@ packages:
|
|||
json-stable-stringify-without-jsonify@1.0.1:
|
||||
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
|
||||
|
||||
json-stringify-pretty-compact@4.0.0:
|
||||
resolution: {integrity: sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q==}
|
||||
|
||||
keyv@4.5.4:
|
||||
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
|
||||
|
||||
|
@ -3563,6 +3569,8 @@ snapshots:
|
|||
|
||||
json-stable-stringify-without-jsonify@1.0.1: {}
|
||||
|
||||
json-stringify-pretty-compact@4.0.0: {}
|
||||
|
||||
keyv@4.5.4:
|
||||
dependencies:
|
||||
json-buffer: 3.0.1
|
||||
|
|
|
@ -56,6 +56,7 @@
|
|||
"@imput/version-info": "workspace:^",
|
||||
"@tabler/icons-svelte": "3.6.0",
|
||||
"@vitejs/plugin-basic-ssl": "^1.1.0",
|
||||
"json-stringify-pretty-compact": "^4.0.0",
|
||||
"mime": "^4.0.4",
|
||||
"sveltekit-i18n": "^2.4.2",
|
||||
"ts-deepmerge": "^7.0.0",
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import LibAV, { type Packet, type Stream } from "@imput/libav.js-encode-cli";
|
||||
import type { Chunk, ChunkMetadata, Decoder, OutputStream, Pipeline, RenderingPipeline } from "../types/libav";
|
||||
import type { Chunk, ChunkMetadata, Decoder, DecoderPipeline, EncoderPipeline, OutputStream, Pipeline, RenderingPipeline, StreamInfo } from "../types/libav";
|
||||
import * as LibAVWebCodecs from "@imput/libavjs-webcodecs-bridge";
|
||||
import { BufferStream } from "../buffer-stream";
|
||||
import WebCodecsWrapper from "./webcodecs";
|
||||
|
@ -8,12 +8,22 @@ import {
|
|||
EncodedAudioChunk as PolyfilledEncodedAudioChunk,
|
||||
EncodedVideoChunk as PolyfilledEncodedVideoChunk
|
||||
} from "@imput/libavjs-webcodecs-polyfill";
|
||||
import type { AudioEncoderConfig, VideoEncoderConfig } from "@imput/libavjs-webcodecs-polyfill";
|
||||
import { probeAudio, probeVideo, type ProbeResult } from "./probe";
|
||||
|
||||
const QUEUE_THRESHOLD_MIN = 16;
|
||||
const QUEUE_THRESHOLD_MAX = 128;
|
||||
|
||||
export default class EncodeLibAV extends LibAVWrapper {
|
||||
#webcodecs: WebCodecsWrapper | null = null;
|
||||
#has_file = false;
|
||||
#fmt_ctx?: number;
|
||||
|
||||
#istreams?: Stream[];
|
||||
#decoders?: DecoderPipeline[];
|
||||
|
||||
#encoders?: EncoderPipeline[];
|
||||
#ostreams?: OutputStream[];
|
||||
|
||||
constructor() {
|
||||
super(LibAV);
|
||||
|
@ -25,6 +35,8 @@ export default class EncodeLibAV extends LibAVWrapper {
|
|||
this.#webcodecs = new WebCodecsWrapper(
|
||||
super.get().then(({ libav }) => libav)
|
||||
);
|
||||
|
||||
await this.#webcodecs.load();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -35,53 +47,207 @@ export default class EncodeLibAV extends LibAVWrapper {
|
|||
};
|
||||
}
|
||||
|
||||
async transcode(blob: Blob) {
|
||||
async cleanup() {
|
||||
const { libav } = await this.#get();
|
||||
|
||||
if (this.#has_file) {
|
||||
await libav.unlinkreadaheadfile('input');
|
||||
this.#has_file = false;
|
||||
}
|
||||
|
||||
if (this.#fmt_ctx) {
|
||||
await libav.avformat_close_input_js(this.#fmt_ctx);
|
||||
this.#fmt_ctx = undefined;
|
||||
this.#istreams = undefined;
|
||||
}
|
||||
|
||||
if (this.#encoders) {
|
||||
for (const encoder of this.#encoders) {
|
||||
try {
|
||||
encoder?.instance.close();
|
||||
} catch {}
|
||||
}
|
||||
this.#encoders = undefined;
|
||||
}
|
||||
|
||||
if (this.#decoders) {
|
||||
for (const decoder of this.#decoders) {
|
||||
try {
|
||||
decoder?.instance.close();
|
||||
} catch {}
|
||||
}
|
||||
this.#decoders = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async feed(blob: Blob) {
|
||||
if (this.#has_file) {
|
||||
throw "readahead file already exists";
|
||||
}
|
||||
|
||||
const { libav } = await this.#get();
|
||||
let fmtctx;
|
||||
|
||||
await libav.mkreadaheadfile('input', blob);
|
||||
this.#has_file = true;
|
||||
|
||||
try {
|
||||
const [ fmt_ctx, streams ] = await libav.ff_init_demuxer_file('input');
|
||||
fmtctx = fmt_ctx;
|
||||
|
||||
const pipes: RenderingPipeline[] = [];
|
||||
const output_streams: OutputStream[] = [];
|
||||
for (const stream of streams) {
|
||||
const {
|
||||
pipe,
|
||||
stream: ostream
|
||||
} = await this.#createEncoder(stream, 'mp4a.40.02');
|
||||
|
||||
pipes.push({
|
||||
decoder: await this.#createDecoder(stream),
|
||||
encoder: pipe
|
||||
} as RenderingPipeline);
|
||||
output_streams.push(ostream);
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
this.#decodeStreams(fmt_ctx, pipes, streams),
|
||||
this.#encodeStreams(pipes),
|
||||
this.#mux(pipes, output_streams)
|
||||
])
|
||||
this.#fmt_ctx = fmt_ctx;
|
||||
this.#istreams = streams;
|
||||
} catch(e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
await libav.unlinkreadaheadfile('input');
|
||||
await this.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
if (fmtctx) {
|
||||
await libav.avformat_close_input_js(fmtctx);
|
||||
async prep() {
|
||||
if (!this.#istreams || !this.#fmt_ctx) {
|
||||
throw "streams are not set up";
|
||||
} else if (this.#decoders) {
|
||||
throw "decoders are already set up";
|
||||
}
|
||||
|
||||
this.#decoders = Array(this.#istreams.length).fill(null);
|
||||
this.#encoders = Array(this.#istreams.length).fill(null);
|
||||
this.#ostreams = Array(this.#istreams.length).fill(null);
|
||||
|
||||
for (const idx in this.#istreams) {
|
||||
try {
|
||||
this.#decoders[idx] = await this.#createDecoder(
|
||||
this.#istreams[idx]
|
||||
)
|
||||
} catch(e) {
|
||||
console.error('could not make decoder', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async #decodeStreams(fmt_ctx: number, pipes: RenderingPipeline[], streams: Stream[]) {
|
||||
for await (const { index, packet } of this.#demux(fmt_ctx)) {
|
||||
async getStreamInfo(): Promise<StreamInfo[]> {
|
||||
if (!this.#istreams) {
|
||||
throw "input not configured";
|
||||
} else if (!this.#decoders) {
|
||||
throw "decoders not prepped";
|
||||
}
|
||||
|
||||
const { libav } = await this.#get();
|
||||
|
||||
return Promise.all(this.#istreams.map(
|
||||
async (stream, index) => {
|
||||
const codec = await libav.avcodec_get_name(stream.codec_id);
|
||||
|
||||
let type = 'unsupported';
|
||||
if (stream.codec_type === libav.AVMEDIA_TYPE_VIDEO) {
|
||||
type = 'video'
|
||||
} else if (stream.codec_type === libav.AVMEDIA_TYPE_AUDIO) {
|
||||
type = 'audio'
|
||||
}
|
||||
|
||||
const decoderConfig: VideoDecoderConfig | AudioDecoderConfig = await this.#streamToConfig(stream);
|
||||
const config = {
|
||||
...decoderConfig,
|
||||
width: 'codedWidth' in decoderConfig ? decoderConfig.codedWidth : undefined,
|
||||
height: 'codedHeight' in decoderConfig ? decoderConfig.codedHeight : undefined,
|
||||
};
|
||||
|
||||
let output: ProbeResult = {};
|
||||
if (type === 'video') {
|
||||
output = await probeVideo(config as globalThis.VideoEncoderConfig);
|
||||
} else if (type === 'audio') {
|
||||
output = await probeAudio(config as globalThis.AudioEncoderConfig)
|
||||
}
|
||||
|
||||
return {
|
||||
codec,
|
||||
type,
|
||||
supported: !!this.#decoders?.[index],
|
||||
output
|
||||
}
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
async configureEncoder(index: number, config: AudioEncoderConfig | VideoEncoderConfig | null) {
|
||||
if (!this.#istreams || !this.#ostreams || !this.#istreams[index])
|
||||
throw "stream does not exist or streams are not configured"
|
||||
else if (!this.#encoders)
|
||||
throw "decoders have not been set up yet";
|
||||
|
||||
const { libav, webcodecs } = await this.#get();
|
||||
const stream = this.#istreams[index];
|
||||
|
||||
let configToStream, initEncoder;
|
||||
|
||||
if (this.#encoders[index]) {
|
||||
await this.#encoders[index].instance.flush();
|
||||
this.#encoders[index].instance.close();
|
||||
this.#encoders[index] = null;
|
||||
}
|
||||
|
||||
if (config === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (stream.codec_type === libav.AVMEDIA_TYPE_VIDEO) {
|
||||
configToStream = LibAVWebCodecs.configToVideoStream;
|
||||
initEncoder = webcodecs.initVideoEncoder.bind(webcodecs);
|
||||
} else if (stream.codec_type === libav.AVMEDIA_TYPE_AUDIO) {
|
||||
configToStream = LibAVWebCodecs.configToAudioStream;
|
||||
initEncoder = webcodecs.initAudioEncoder.bind(webcodecs);
|
||||
} else throw "Unknown type: " + stream.codec_type;
|
||||
|
||||
const output = new BufferStream<
|
||||
{ chunk: Chunk, metadata: ChunkMetadata }
|
||||
>();
|
||||
|
||||
const encoder = await initEncoder(config as any /* fixme */, {
|
||||
output: (chunk, metadata = {}) => {
|
||||
output.push({ chunk, metadata })
|
||||
},
|
||||
error: console.error
|
||||
});
|
||||
|
||||
if (!encoder) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.#ostreams[index] = await configToStream(libav, config);
|
||||
this.#encoders[index] = { instance: encoder, output } as EncoderPipeline;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async work(formatName: string) {
|
||||
if (!this.#encoders || !this.#decoders)
|
||||
throw "not configured";
|
||||
|
||||
const pipes: RenderingPipeline[] = Array(this.#decoders.length).fill(null);
|
||||
for (let i = 0; i < this.#encoders.length; ++i) {
|
||||
if (this.#encoders[i] === null) continue;
|
||||
if (this.#decoders[i] === null) continue;
|
||||
|
||||
pipes[i] = {
|
||||
encoder: this.#encoders[i],
|
||||
decoder: this.#decoders[i]
|
||||
} as RenderingPipeline;
|
||||
}
|
||||
|
||||
const [,, blob] = await Promise.all([
|
||||
this.#decodeStreams(pipes),
|
||||
this.#encodeStreams(pipes),
|
||||
this.#mux(pipes, formatName)
|
||||
]);
|
||||
|
||||
return blob;
|
||||
}
|
||||
|
||||
async #decodeStreams(pipes: RenderingPipeline[]) {
|
||||
if (!this.#istreams) throw "istreams are not set up";
|
||||
|
||||
for await (const { index, packet } of this.#demux()) {
|
||||
if (pipes[index] === null) continue;
|
||||
|
||||
const { decoder } = pipes[index];
|
||||
|
||||
this.#decodePacket(decoder.instance, packet, streams[index]);
|
||||
this.#decodePacket(decoder.instance, packet, this.#istreams[index]);
|
||||
|
||||
let currentSize = decoder.instance.decodeQueueSize + decoder.output.queue.size;
|
||||
if (currentSize >= QUEUE_THRESHOLD_MAX) {
|
||||
|
@ -137,7 +303,6 @@ export default class EncodeLibAV extends LibAVWrapper {
|
|||
encoder as VideoEncoder
|
||||
);
|
||||
} else {
|
||||
console.log(value);
|
||||
WebCodecsWrapper.encodeAudio(
|
||||
value as AudioData,
|
||||
encoder as AudioEncoder
|
||||
|
@ -178,7 +343,10 @@ export default class EncodeLibAV extends LibAVWrapper {
|
|||
return await convertToPacket(libav, chunk, metadata, ostream, index);
|
||||
}
|
||||
|
||||
async #mux(pipes: RenderingPipeline[], ostreams: OutputStream[]) {
|
||||
async #mux(pipes: RenderingPipeline[], format_name: string) {
|
||||
if (!this.#ostreams) throw "ostreams not configured";
|
||||
|
||||
const ostreams = this.#ostreams;
|
||||
const { libav } = await this.#get();
|
||||
const write_pkt = await libav.av_packet_alloc();
|
||||
|
||||
|
@ -211,15 +379,15 @@ export default class EncodeLibAV extends LibAVWrapper {
|
|||
writtenData.set(data, pos);
|
||||
};
|
||||
|
||||
await libav.mkwriterdev("output.mp4");
|
||||
await libav.mkwriterdev("output");
|
||||
[ output_ctx,, writer_ctx ] = await libav.ff_init_muxer(
|
||||
{
|
||||
format_name: 'mp4',
|
||||
filename: 'output.mp4',
|
||||
format_name,
|
||||
filename: 'output',
|
||||
device: true,
|
||||
open: true,
|
||||
codecpars: true
|
||||
}, ostreams.filter(a => a !== null)
|
||||
}, this.#ostreams.filter(a => a !== null)
|
||||
);
|
||||
|
||||
await libav.avformat_write_header(output_ctx, 0);
|
||||
|
@ -245,25 +413,18 @@ export default class EncodeLibAV extends LibAVWrapper {
|
|||
await writePromise;
|
||||
await libav.av_write_trailer(output_ctx);
|
||||
|
||||
const renderBlob = new Blob(
|
||||
[ writtenData ],
|
||||
{ type: "video/mp4" }
|
||||
);
|
||||
|
||||
const url = URL.createObjectURL(renderBlob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'output.mp4';
|
||||
a.click();
|
||||
return new Blob([ writtenData ]);
|
||||
} finally {
|
||||
try {
|
||||
await libav.unlink('output.mp4');
|
||||
await libav.unlink('output');
|
||||
} catch {}
|
||||
|
||||
await libav.av_packet_free(write_pkt);
|
||||
if (output_ctx && writer_ctx) {
|
||||
await libav.ff_free_muxer(output_ctx, writer_ctx);
|
||||
}
|
||||
|
||||
await this.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -289,13 +450,14 @@ export default class EncodeLibAV extends LibAVWrapper {
|
|||
}
|
||||
}
|
||||
|
||||
async* #demux(fmt_ctx: number) {
|
||||
async* #demux() {
|
||||
if (!this.#fmt_ctx) throw "fmt_ctx is missing";
|
||||
const { libav } = await this.#get();
|
||||
const read_pkt = await libav.av_packet_alloc();
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const [ ret, packets ] = await libav.ff_read_frame_multi(fmt_ctx, read_pkt, { limit: 1 });
|
||||
const [ ret, packets ] = await libav.ff_read_frame_multi(this.#fmt_ctx, read_pkt, { limit: 1 });
|
||||
|
||||
if (ret !== -libav.EAGAIN &&
|
||||
ret !== 0 &&
|
||||
|
@ -317,80 +479,45 @@ export default class EncodeLibAV extends LibAVWrapper {
|
|||
}
|
||||
}
|
||||
|
||||
async #createEncoder(stream: Stream, codec: string) {
|
||||
const { libav, webcodecs } = await this.#get();
|
||||
#streamToConfig(stream: Stream) {
|
||||
let _streamToConfig;
|
||||
|
||||
let streamToConfig, configToStream, initEncoder;
|
||||
|
||||
if (stream.codec_type === libav.AVMEDIA_TYPE_VIDEO) {
|
||||
streamToConfig = LibAVWebCodecs.videoStreamToConfig;
|
||||
configToStream = LibAVWebCodecs.configToVideoStream;
|
||||
initEncoder = webcodecs.initVideoEncoder.bind(webcodecs);
|
||||
codec = 'hvc1.1.6.L123.B0'
|
||||
} else if (stream.codec_type === libav.AVMEDIA_TYPE_AUDIO) {
|
||||
streamToConfig = LibAVWebCodecs.audioStreamToConfig;
|
||||
configToStream = LibAVWebCodecs.configToAudioStream;
|
||||
initEncoder = webcodecs.initAudioEncoder.bind(webcodecs);
|
||||
if (stream.codec_type === LibAV.AVMEDIA_TYPE_VIDEO) {
|
||||
_streamToConfig = LibAVWebCodecs.videoStreamToConfig;
|
||||
} else if (stream.codec_type === LibAV.AVMEDIA_TYPE_AUDIO) {
|
||||
_streamToConfig = LibAVWebCodecs.audioStreamToConfig;
|
||||
} else throw "Unknown type: " + stream.codec_type;
|
||||
|
||||
const config = await streamToConfig(libav, stream, true);
|
||||
if (config === null) {
|
||||
throw "could not make encoder config";
|
||||
}
|
||||
return this.#get().then(
|
||||
({ libav }) => _streamToConfig(libav, stream, true)
|
||||
) as Promise<AudioDecoderConfig | VideoDecoderConfig>;
|
||||
}
|
||||
|
||||
const encoderConfig = {
|
||||
codec,
|
||||
width: config.codedWidth,
|
||||
height: config.codedHeight,
|
||||
numberOfChannels: config.numberOfChannels,
|
||||
sampleRate: config.sampleRate
|
||||
};
|
||||
|
||||
|
||||
const output = new BufferStream<
|
||||
{ chunk: Chunk, metadata: ChunkMetadata }
|
||||
>();
|
||||
|
||||
const encoder = await initEncoder(encoderConfig, {
|
||||
output: (chunk, metadata = {}) => {
|
||||
output.push({ chunk, metadata })
|
||||
},
|
||||
error: console.error
|
||||
});
|
||||
|
||||
if (!encoder) {
|
||||
throw "cannot encode " + codec;
|
||||
}
|
||||
|
||||
const encoderStream = await configToStream(libav, encoderConfig);
|
||||
|
||||
return {
|
||||
pipe: { instance: encoder, output },
|
||||
stream: encoderStream
|
||||
};
|
||||
streamIndexToConfig(index: number) {
|
||||
if (!this.#istreams || !this.#istreams[index])
|
||||
throw "invalid stream";
|
||||
return this.#streamToConfig(this.#istreams[index]);
|
||||
}
|
||||
|
||||
async #createDecoder(stream: Stream) {
|
||||
const { libav, webcodecs } = await this.#get();
|
||||
|
||||
let streamToConfig, initDecoder;
|
||||
let initDecoder;
|
||||
|
||||
if (stream.codec_type === libav.AVMEDIA_TYPE_VIDEO) {
|
||||
streamToConfig = LibAVWebCodecs.videoStreamToConfig;
|
||||
initDecoder = webcodecs.initVideoDecoder.bind(webcodecs);
|
||||
} else if (stream.codec_type === libav.AVMEDIA_TYPE_AUDIO) {
|
||||
streamToConfig = LibAVWebCodecs.audioStreamToConfig;
|
||||
initDecoder = webcodecs.initAudioDecoder.bind(webcodecs);
|
||||
} else throw "Unknown type: " + stream.codec_type;
|
||||
|
||||
const config = await streamToConfig(libav, stream, true);
|
||||
const config = await this.#streamToConfig(stream);
|
||||
|
||||
if (config === null) {
|
||||
throw "could not make decoder config";
|
||||
}
|
||||
|
||||
const output = new BufferStream<VideoFrame | AudioData>();
|
||||
const decoder = await initDecoder(config, {
|
||||
const decoder = await initDecoder(config as any /* fixme */, {
|
||||
output: frame => output.push(frame),
|
||||
error: console.error
|
||||
});
|
||||
|
@ -399,6 +526,6 @@ export default class EncodeLibAV extends LibAVWrapper {
|
|||
throw "cannot decode " + config.codec;
|
||||
}
|
||||
|
||||
return { instance: decoder, output }
|
||||
return { instance: decoder, output } as DecoderPipeline;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,6 +28,7 @@ export default class LibAVWrapper {
|
|||
if (this.#libav) {
|
||||
const libav = await this.#libav;
|
||||
libav.terminate();
|
||||
this.#libav = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -41,7 +42,7 @@ export default class LibAVWrapper {
|
|||
|
||||
static getExtensionFromType(blob: Blob) {
|
||||
const extensions = mime.getAllExtensions(blob.type);
|
||||
const overrides = ['mp3', 'mov'];
|
||||
const overrides = ['mp3', 'mov', 'opus'];
|
||||
|
||||
if (!extensions)
|
||||
return;
|
||||
|
|
101
web/src/lib/libav/probe.ts
Normal file
101
web/src/lib/libav/probe.ts
Normal file
|
@ -0,0 +1,101 @@
|
|||
import * as LibAVPolyfill from "@imput/libavjs-webcodecs-polyfill";
|
||||
|
||||
const AUDIO_CODECS = {
|
||||
aac: ['mp4a.40.02', 'mp4a.40.05', 'mp4a.40.29'],
|
||||
opus: ['opus'],
|
||||
mp3: ['mp3', 'mp4a.69', 'mp4a.6B'],
|
||||
flac: ['flac'],
|
||||
ogg: ['vorbis'],
|
||||
ulaw: ['ulaw'],
|
||||
alaw: ['alaw']
|
||||
};
|
||||
|
||||
const VIDEO_CODECS = {
|
||||
h264: ['avc1.42E01E', 'avc1.42E01F', 'avc1.4D401F', 'avc1.4D4028', 'avc1.640028', 'avc1.640029', 'avc1.64002A'],
|
||||
av1: ['av01.0.08M.08', 'av01.0.12M.08', 'av01.0.16M.08'],
|
||||
vp9: ['vp09.01.10.08', 'vp09.02.10.08'],
|
||||
hevc: ['hvc1.1.6.L93.B0', 'hvc1.1.6.L123.00', 'hvc1.1.6.L153.00', 'hvc1.2.4.L93.00', 'hvc1.2.4.L123.00']
|
||||
};
|
||||
|
||||
async function probeSingleAudio(config: AudioEncoderConfig) {
|
||||
try {
|
||||
if ('AudioEncoder' in window && window.AudioEncoder !== LibAVPolyfill.AudioEncoder) {
|
||||
const { supported } = await window.AudioEncoder.isConfigSupported(config);
|
||||
if (supported) {
|
||||
return { supported };
|
||||
}
|
||||
}
|
||||
} catch(e) { console.warn('audio native probe fail', e) }
|
||||
|
||||
try {
|
||||
const { supported } = await LibAVPolyfill.AudioEncoder.isConfigSupported(config);
|
||||
if (supported) {
|
||||
return { supported, slow: true }
|
||||
}
|
||||
} catch(e) { console.warn('audio polyfill probe fail', e) }
|
||||
|
||||
return { supported: false }
|
||||
}
|
||||
|
||||
async function probeSingleVideo(config: VideoEncoderConfig) {
|
||||
try {
|
||||
if ('VideoEncoder' in window && window.VideoEncoder !== LibAVPolyfill.VideoEncoder) {
|
||||
const { supported } = await window.VideoEncoder.isConfigSupported(config);
|
||||
if (supported) {
|
||||
return { supported };
|
||||
}
|
||||
}
|
||||
} catch(e) { console.warn('video native probe fail', e) }
|
||||
|
||||
try {
|
||||
const { supported } = await LibAVPolyfill.VideoEncoder.isConfigSupported(config);
|
||||
if (supported) {
|
||||
return { supported, slow: true }
|
||||
}
|
||||
} catch(e) { console.warn('video polyfill probe fail', e) }
|
||||
|
||||
return { supported: false }
|
||||
}
|
||||
|
||||
export type ProbeResult = {
|
||||
[name: string]: { supported: false } | {
|
||||
supported: true,
|
||||
codec: string,
|
||||
slow?: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export async function probeAudio(partial: Omit<AudioEncoderConfig, "codec">) {
|
||||
const result: ProbeResult = {};
|
||||
|
||||
for (const [ name, codecs ] of Object.entries(AUDIO_CODECS)) {
|
||||
result[name] = { supported: false };
|
||||
for (const codec of codecs) {
|
||||
const config = { ...partial, codec };
|
||||
const { supported, slow } = await probeSingleAudio(config);
|
||||
if (supported) {
|
||||
result[name] = { supported, slow, codec };
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function probeVideo(partial: Omit<VideoEncoderConfig, "codec">) {
|
||||
const result: ProbeResult = {};
|
||||
for (const [ name, codecs ] of Object.entries(VIDEO_CODECS)) {
|
||||
result[name] = { supported: false };
|
||||
for (const codec of codecs) {
|
||||
const config = { ...partial, codec };
|
||||
const { supported, slow } = await probeSingleVideo(config);
|
||||
if (supported) {
|
||||
result[name] = { supported, slow, codec };
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
|
@ -13,7 +13,7 @@ export default class WebCodecsWrapper {
|
|||
this.#libav = libav;
|
||||
}
|
||||
|
||||
async #load() {
|
||||
async load() {
|
||||
if (typeof this.#ready === 'undefined') {
|
||||
this.#ready = LibAVPolyfill.load({
|
||||
polyfill: true,
|
||||
|
@ -30,7 +30,7 @@ export default class WebCodecsWrapper {
|
|||
const audioConfig = config as AudioDecoderConfig;
|
||||
for (const source of [ window, LibAVPolyfill ]) {
|
||||
if (source === LibAVPolyfill) {
|
||||
await this.#load();
|
||||
await this.load();
|
||||
}
|
||||
|
||||
try {
|
||||
|
@ -45,7 +45,7 @@ export default class WebCodecsWrapper {
|
|||
const videoConfig = config as VideoDecoderConfig;
|
||||
for (const source of [ window, LibAVPolyfill ]) {
|
||||
if (source === LibAVPolyfill) {
|
||||
await this.#load();
|
||||
await this.load();
|
||||
}
|
||||
|
||||
try {
|
||||
|
@ -66,7 +66,7 @@ export default class WebCodecsWrapper {
|
|||
const audioConfig = config as AudioEncoderConfig;
|
||||
for (const source of [ window, LibAVPolyfill ]) {
|
||||
if (source === LibAVPolyfill) {
|
||||
await this.#load();
|
||||
await this.load();
|
||||
}
|
||||
|
||||
try {
|
||||
|
@ -81,7 +81,7 @@ export default class WebCodecsWrapper {
|
|||
const videoConfig = config as VideoEncoderConfig;
|
||||
for (const source of [ window, LibAVPolyfill ]) {
|
||||
if (source === LibAVPolyfill) {
|
||||
await this.#load();
|
||||
await this.load();
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { BufferStream } from "$lib/buffer-stream";
|
||||
import type { ProbeResult } from "$lib/libav/probe";
|
||||
|
||||
export type InputFileKind = "video" | "audio";
|
||||
|
||||
|
@ -69,5 +70,17 @@ export type VideoPipeline = {
|
|||
}
|
||||
|
||||
export type Pipeline = AudioPipeline | VideoPipeline;
|
||||
export type DecoderPipeline = AudioDecoderPipeline | VideoDecoderPipeline | null;
|
||||
export type EncoderPipeline = AudioEncoderPipeline | VideoEncoderPipeline | null;
|
||||
export type RenderingPipeline = Pipeline | null;
|
||||
export type OutputStream = [number, number, number] | null;
|
||||
export type ContainerConfiguration = {
|
||||
formatName: string
|
||||
};
|
||||
|
||||
export type StreamInfo = {
|
||||
codec: string,
|
||||
type: string,
|
||||
supported: boolean,
|
||||
output: ProbeResult
|
||||
};
|
|
@ -1,12 +1,15 @@
|
|||
<script lang="ts">
|
||||
import LibAVWrapper from "$lib/libav/encode";
|
||||
import { t } from "$lib/i18n/translations";
|
||||
import stringify from "json-stringify-pretty-compact";
|
||||
|
||||
import DropReceiver from "$components/misc/DropReceiver.svelte";
|
||||
import FileReceiver from "$components/misc/FileReceiver.svelte";
|
||||
import type { StreamInfo } from "$lib/types/libav";
|
||||
import { onDestroy } from "svelte";
|
||||
|
||||
let file: File | undefined;
|
||||
let streamInfo: StreamInfo[] | undefined;
|
||||
|
||||
const ff = new LibAVWrapper();
|
||||
ff.init();
|
||||
|
@ -14,6 +17,44 @@
|
|||
const render = async () => {
|
||||
if (!file) return;
|
||||
await ff.init();
|
||||
await ff.cleanup();
|
||||
await ff.feed(file);
|
||||
await ff.prep();
|
||||
streamInfo = await ff.getStreamInfo();
|
||||
console.log(streamInfo);
|
||||
|
||||
for (const stream_index in streamInfo) {
|
||||
const stream = streamInfo[stream_index];
|
||||
if (!stream.supported) continue;
|
||||
|
||||
const maybe_codec = Object.values(stream.output).find(a => a.supported && !a.slow);
|
||||
if (!maybe_codec || !maybe_codec.supported)
|
||||
throw "could not find valid codec";
|
||||
|
||||
const decoderConfig = await ff.streamIndexToConfig(+stream_index);
|
||||
const config = {
|
||||
...decoderConfig,
|
||||
width: 'codedWidth' in decoderConfig ? decoderConfig.codedWidth : undefined,
|
||||
height: 'codedHeight' in decoderConfig ? decoderConfig.codedHeight : undefined,
|
||||
};
|
||||
|
||||
await ff.configureEncoder(+stream_index, {
|
||||
...config,
|
||||
codec: maybe_codec.codec
|
||||
});
|
||||
}
|
||||
|
||||
const blob = new Blob(
|
||||
[ await ff.work('mp4') ],
|
||||
{ type: 'video/mp4' }
|
||||
);
|
||||
console.log('she=onika ate=burgers blob=', blob);
|
||||
|
||||
const pseudolink = document.createElement("a");
|
||||
pseudolink.href = URL.createObjectURL(blob);
|
||||
pseudolink.download = "video.mp4";
|
||||
pseudolink.click();
|
||||
};
|
||||
|
||||
onDestroy(async () => {
|
||||
await ff.cleanup();
|
||||
|
@ -44,6 +85,12 @@
|
|||
{$t("remux.description")}
|
||||
</div>
|
||||
</div>
|
||||
{#if streamInfo}
|
||||
<div class="codec-info">
|
||||
i am (hopefully) working. check console
|
||||
{ stringify(streamInfo) }
|
||||
</div>
|
||||
{/if}
|
||||
</DropReceiver>
|
||||
|
||||
<style>
|
||||
|
@ -69,4 +116,10 @@
|
|||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.codec-info {
|
||||
white-space: pre;
|
||||
font-family: 'Comic Sans MS', cursive;
|
||||
color: red;
|
||||
}
|
||||
</style>
|
||||
|
|
Loading…
Reference in a new issue