diff --git a/Makefile b/Makefile index 71489d6..f59e218 100644 --- a/Makefile +++ b/Makefile @@ -28,11 +28,8 @@ WEB_DEMUXER_ARGS = \ -O3 \ -s EXPORT_ES6=1 \ -s INVOKE_RUN=0 \ - -s ENVIRONMENT=worker \ - -s EXPORTED_RUNTIME_METHODS=cwrap,getValue,UTF8ToString \ - -s EXPORTED_FUNCTIONS=_free \ + -s ENVIRONMENT=worker \ -s ASYNCIFY \ - -s WASM_BIGINT \ -s ALLOW_MEMORY_GROWTH=1 clean: diff --git a/README.md b/README.md index 817e142..0191958 100644 --- a/README.md +++ b/README.md @@ -63,8 +63,6 @@ async function seek(file, time) { - [Seek Video Frame](https://foreversc.github.io/web-demuxer/#example-seek) | [code](https://github.com/ForeverSc/web-demuxer/blob/main/index.html#L96) - [Play Video](https://foreversc.github.io/web-demuxer/#example-play) | [code](https://github.com/ForeverSc/web-demuxer/blob/main/index.html#L123) -Here is the translation of the provided TypeScript API documentation into English: - ## API ```typescript new WebDemuxer(options: WebDemuxerOptions) diff --git a/index.html b/index.html index 81031f5..31c0877 100644 --- a/index.html +++ b/index.html @@ -121,13 +121,9 @@

Get All Video Packets

await demuxer.load(file); - const videoDecoderConfig = await demuxer.getVideoDecoderConfig().catch(e => { - console.error('get video decoder config error:', e); - }); + const videoDecoderConfig = await demuxer.getVideoDecoderConfig(); const videoChunk = await demuxer.seekEncodedVideoChunk(seekTime); - console.log("getVideoStream", await demuxer.getVideoStream()) - const decoder = new VideoDecoder({ output: (frame) => { const scale = Math.min(canvas.width / frame.displayWidth, canvas.height / frame.displayHeight); diff --git a/lib/web-demuxer/post.js b/lib/web-demuxer/post.js index 0c9ebef..2b8c143 100644 --- a/lib/web-demuxer/post.js +++ b/lib/web-demuxer/post.js @@ -253,6 +253,10 @@ function genSendAVPacket(messageId) { } } +function setAVLogLevel(level) { + Module.set_av_log_level(level); +} + // ============ Module Register ============ Module.getAVStream = getAVStream; Module.getAVStreams = getAVStreams; @@ -260,6 +264,7 @@ Module.getMediaInfo = getMediaInfo; Module.getAVPacket = getAVPacket; Module.getAVPackets = getAVPackets; Module.readAVPacket = readAVPacket; +Module.setAVLogLevel = setAVLogLevel; Module.onRuntimeInitialized = () => { self.postMessage({ type: "WASMRuntimeInitialized" }); diff --git a/lib/web-demuxer/web_demuxer.cpp b/lib/web-demuxer/web_demuxer.cpp index b5f7682..4c997e0 100644 --- a/lib/web-demuxer/web_demuxer.cpp +++ b/lib/web-demuxer/web_demuxer.cpp @@ -228,8 +228,6 @@ void gen_web_stream(WebAVStream &web_stream, AVStream *stream, AVFormatContext * WebAVStream get_av_stream(std::string filename, int type, int wanted_stream_nb) { - // av_log_set_level(AV_LOG_QUIET); - AVFormatContext *fmt_ctx = NULL; int ret; @@ -600,6 +598,10 @@ int read_av_packet(std::string filename, double start, double end, int type, int return 1; } +void set_av_log_level(int level) { + av_log_set_level(level); +} + EMSCRIPTEN_BINDINGS(web_demuxer) { value_object("Tag") @@ -667,6 +669,7 @@ EMSCRIPTEN_BINDINGS(web_demuxer) function("get_av_packet", &get_av_packet, return_value_policy::take_ownership()); function("get_av_packets", &get_av_packets, return_value_policy::take_ownership()); function("read_av_packet", &read_av_packet); + function("set_av_log_level", &set_av_log_level); register_vector("vector"); register_vector("vector"); diff --git a/src/ffmpeg.worker.ts b/src/ffmpeg.worker.ts index ee30cd9..12bf75c 100644 --- a/src/ffmpeg.worker.ts +++ b/src/ffmpeg.worker.ts @@ -1,4 +1,4 @@ -import { WebAVPacket, WebAVStream } from "./types"; +import { FFMpegWorkerMessageType, GetAVPacketMessageData, GetAVPacketsMessageData, GetAVStreamMessageData, GetAVStreamsMessageData, GetMediaInfoMessageData, LoadWASMMessageData, ReadAVPacketMessageData, SetAVLogLevelMessageData, WebAVPacket, WebAVStream } from "./types"; let Module: any; // TODO: rm any @@ -7,114 +7,28 @@ self.postMessage({ }); self.addEventListener("message", async function (e) { - const { type, data = {}, msgId } = e.data; + const { type, data, msgId } = e.data try { - if (type === "LoadWASM") { - const { wasmLoaderPath } = data || {} - - const ModuleLoader = await import(/* @vite-ignore */wasmLoaderPath); - Module = await ModuleLoader.default(); - } else if (type === "GetAVStream") { - const { - file, - streamType, - streamIndex, - } = data; - const result = Module.getAVStream(file, streamType, streamIndex); - - self.postMessage( - { - type, - msgId, - result, - }, - [result.extradata.buffer], - ); - } else if (type === 'GetAVStreams') { - const { - file, - } = data; - const result = Module.getAVStreams(file); - - self.postMessage( - { - type, - msgId, - result, - }, - result.map((stream: WebAVStream) => stream.extradata.buffer) - ); - } else if (type === "GetMediaInfo") { - const { - file, - } = data; - const result = Module.getMediaInfo(file); - - self.postMessage({ - type, - msgId, - result, - }, result.streams.map((stream: WebAVStream) => stream.extradata.buffer)); - } else if (type === "GetAVPacket") { - const { - file, - time, - streamType, - streamIndex, - } = data; - const result = Module.getAVPacket( - file, - time, - streamType, - streamIndex, - ); - - self.postMessage( - { - type, - msgId, - result, - }, - [result.data.buffer], - ); - } else if (type === 'GetAVPackets') { - const { - file, - time, - } = data; - const result = Module.getAVPackets(file, time); - - self.postMessage( - { - type, - msgId, - result, - }, - result.map((packet: WebAVPacket) => packet.data.buffer), - ); - } else if (type === "ReadAVPacket") { - const { - file, - start, - end, - streamType, - streamIndex, - } = data; - const result = await Module.readAVPacket( - msgId, - file, - start, - end, - streamType, - streamIndex, - ); - - self.postMessage({ - type, - msgId, - result, - }); + switch (type) { + case "LoadWASM": + return handleLoadWASM(data); + case "GetAVStream": + return handleGetAVStream(data, msgId); + case "GetAVStreams": + return handleGetAVStreams(data, msgId); + case "GetMediaInfo": + return handleGetMediaInfo(data, msgId); + case "GetAVPacket": + return handleGetAVPacket(data, msgId); + case "GetAVPackets": + return handleGetAVPackets(data, msgId); + case "ReadAVPacket": + return handleReadAVPacket(data, msgId); + case "SetAVLogLevel": + return handleSetAVLogLevel(data, msgId); + default: + return; } } catch (e) { self.postMessage({ @@ -124,3 +38,107 @@ self.addEventListener("message", async function (e) { }); } }); + +async function handleLoadWASM(data: LoadWASMMessageData) { + const { wasmLoaderPath } = data || {}; + const ModuleLoader = await import(/* @vite-ignore */wasmLoaderPath); + Module = await ModuleLoader.default(); +} + +function handleGetAVStream(data: GetAVStreamMessageData, msgId: number) { + const { file, streamType, streamIndex } = data; + const result = Module.getAVStream(file, streamType, streamIndex); + + self.postMessage( + { + type: FFMpegWorkerMessageType.GetAVStream, + msgId, + result, + }, + [result.extradata.buffer], + ); +} + +function handleGetAVStreams(data: GetAVStreamsMessageData, msgId: number) { + const { file } = data; + const result = Module.getAVStreams(file); + + self.postMessage( + { + type: FFMpegWorkerMessageType.GetAVStreams, + msgId, + result, + }, + result.map((stream: WebAVStream) => stream.extradata.buffer), + ); +} + +function handleGetMediaInfo(data: GetMediaInfoMessageData, msgId: number) { + const { file } = data; + const result = Module.getMediaInfo(file); + + self.postMessage( + { + type: FFMpegWorkerMessageType.GetMediaInfo, + msgId, + result, + }, + result.streams.map((stream: WebAVStream) => stream.extradata.buffer) + ); +} + +function handleGetAVPacket(data: GetAVPacketMessageData, msgId: number) { + const { file, time, streamType, streamIndex } = data; + const result = Module.getAVPacket(file, time, streamType, streamIndex); + + self.postMessage( + { + type: FFMpegWorkerMessageType.GetAVPacket, + msgId, + result, + }, + [result.data.buffer], + ); +} + +function handleGetAVPackets(data: GetAVPacketsMessageData, msgId: number) { + const { file, time } = data; + const result = Module.getAVPackets(file, time); + + self.postMessage( + { + type: FFMpegWorkerMessageType.GetAVPackets, + msgId, + result, + }, + result.map((packet: WebAVPacket) => packet.data.buffer), + ); +} + +async function handleReadAVPacket(data: ReadAVPacketMessageData, msgId: number) { + const { file, start, end, streamType, streamIndex } = data; + const result = await Module.readAVPacket( + msgId, + file, + start, + end, + streamType, + streamIndex, + ); + + self.postMessage({ + type: FFMpegWorkerMessageType.ReadAVPacket, + msgId, + result, + }); +} + +function handleSetAVLogLevel(data: SetAVLogLevelMessageData, msgId: number) { + const { level } = data + + Module.setAVLogLevel(level); + self.postMessage({ + type: "SetAVLogLevel", + msgId, + }) +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 96f1f3a..ff104e1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,6 @@ +import { WebDemuxer } from "./web-demuxer"; + export type { WebAVStream, WebAVPacket } from './types'; -export { AVMediaType } from './types'; -export { WebDemuxer } from "./web-demuxer"; +export { AVMediaType, AVLogLevel } from './types'; +export { WebDemuxer }; +export default WebDemuxer; diff --git a/src/types/avutil.ts b/src/types/avutil.ts index d3c246f..1394334 100644 --- a/src/types/avutil.ts +++ b/src/types/avutil.ts @@ -10,3 +10,46 @@ export enum AVMediaType { AVMEDIA_TYPE_ATTACHMENT, ///< Opaque data information usually sparse AVMEDIA_TYPE_NB, } + +export enum AVLogLevel { + /** + * Print no output. + */ + AV_LOG_QUIET = -8, + /** + * Something went really wrong and we will crash now. + */ + AV_LOG_PANIC = 0, + /** + * Something went wrong and recovery is not possible. + * For example, no header was found for a format which depends + * on headers or an illegal combination of parameters is used. + */ + AV_LOG_FATAL = 8, + /** + * Something went wrong and cannot losslessly be recovered. + * However, not all future data is affected. + */ + AV_LOG_ERROR = 16, + /** + * Something somehow does not look correct. This may or may not + * lead to problems. An example would be the use of '-vstrict -2'. + */ + AV_LOG_WARNING = 24, + /** + * Standard information. + */ + AV_LOG_INFO = 32, + /** + * Detailed information. + */ + AV_LOG_VERBOSE = 40, + /** + * Stuff which is only useful for libav* developers. + */ + AV_LOG_DEBUG = 48, + /** + * Extremely verbose debugging, useful for libav* development. + */ + AV_LOG_TRACE = 56, +} diff --git a/src/types/demuxer.ts b/src/types/demuxer.ts index b1f5841..7da78be 100644 --- a/src/types/demuxer.ts +++ b/src/types/demuxer.ts @@ -39,3 +39,14 @@ export interface WebAVPacket { size: number; data: Uint8Array; } + +export interface WebMediaInfo { + format_name: string; + start_time: number; + duration: number; + bit_rate: string; + nb_streams: number; + nb_chapters: number; + flags: number; + streams: WebAVStream[]; +} diff --git a/src/types/ffmpeg-worker-message.ts b/src/types/ffmpeg-worker-message.ts index 953844b..7e783db 100644 --- a/src/types/ffmpeg-worker-message.ts +++ b/src/types/ffmpeg-worker-message.ts @@ -1,4 +1,4 @@ -import { AVMediaType } from "./avutil"; +import { AVLogLevel, AVMediaType } from "./avutil"; export enum FFMpegWorkerMessageType { FFmpegWorkerLoaded = "FFmpegWorkerLoaded", @@ -13,6 +13,7 @@ export enum FFMpegWorkerMessageType { AVPacketStream = "AVPacketStream", ReadNextAVPacket = "ReadNextAVPacket", StopReadAVPacket = "StopReadAVPacket", + SetAVLogLevel = "SetAVLogLevel", } export type FFMpegWorkerMessageData = @@ -21,7 +22,9 @@ export type FFMpegWorkerMessageData = | GetAVStreamMessageData | GetAVStreamsMessageData | ReadAVPacketMessageData - | LoadWASMMessageData; + | LoadWASMMessageData + | SetAVLogLevelMessageData + | GetMediaInfoMessageData; export interface GetAVStreamMessageData { file: File; @@ -57,7 +60,16 @@ export interface LoadWASMMessageData { wasmLoaderPath: string; } +export interface GetMediaInfoMessageData { + file: File; +} + +export interface SetAVLogLevelMessageData { + level: AVLogLevel; +} + export interface FFMpegWorkerMessage { type: FFMpegWorkerMessageType; data: FFMpegWorkerMessageData; + msgId: number; } diff --git a/src/web-demuxer.ts b/src/web-demuxer.ts index 575498c..9a17061 100644 --- a/src/web-demuxer.ts +++ b/src/web-demuxer.ts @@ -1,9 +1,11 @@ import { + AVLogLevel, AVMediaType, FFMpegWorkerMessageData, FFMpegWorkerMessageType, WebAVPacket, WebAVStream, + WebMediaInfo, } from "./types"; import FFmpegWorker from "./ffmpeg.worker.ts?worker&inline"; @@ -91,7 +93,7 @@ export class WebDemuxer { } /** - * load a file + * Load a file for demuxing * @param file file to load * @returns load status */ @@ -104,7 +106,8 @@ export class WebDemuxer { } /** - * destroy the instance + * Destroy the demuxer instance + * terminate the worker */ public destroy() { this.file = undefined; @@ -113,9 +116,9 @@ export class WebDemuxer { // ================ base api ================ /** - * get av stream - * @param streamType - * @param streamIndex + * Gets information about a specified stream in the media file. + * @param streamType The type of media stream + * @param streamIndex The index of the media stream * @returns WebAVStream */ public getAVStream( @@ -130,7 +133,8 @@ export class WebDemuxer { } /** - * get all av streams + * Get all streams + * @returns WebAVStream[] */ public getAVStreams(): Promise { return this.getFromWorker(FFMpegWorkerMessageType.GetAVStreams, { @@ -139,19 +143,20 @@ export class WebDemuxer { } /** - * get file media info + * Get file media info + * @returns WebMediaInfo */ - public getMediaInfo() { + public getMediaInfo(): Promise { return this.getFromWorker(FFMpegWorkerMessageType.GetMediaInfo, { file: this.file!, }); } /** - * get av packet - * @param time - * @param streamType - * @param streamIndex + * Gets the data at a specified time point in the media file. + * @param time time in seconds + * @param streamType The type of media stream + * @param streamIndex The index of the media stream * @returns WebAVPacket */ public getAVPacket( @@ -168,8 +173,8 @@ export class WebDemuxer { } /** - * get av packets in all streams - * @param time + * Get all packets at a time point from all streams + * @param time time in seconds * @returns WebAVPacket[] */ public getAVPackets( @@ -182,11 +187,11 @@ export class WebDemuxer { } /** - * read av packet - * @param start start time - * @param end end time - * @param streamType - * @param streamIndex + * Returns a `ReadableStream` for streaming packet data. + * @param start start time in seconds + * @param end end time in seconds + * @param streamType The type of media stream + * @param streamIndex The index of the media stream * @returns ReadableStream */ public readAVPacket( @@ -261,24 +266,58 @@ export class WebDemuxer { ); } + /** + * Set log level + * @param level log level + */ + public setLogLevel(level: AVLogLevel) { + return this.getFromWorker(FFMpegWorkerMessageType.SetAVLogLevel, { level }) + } + // ================ convenience api ================ + /** + * Get video stream + * @param streamType The type of media stream + * @returns WebAVStream + */ public getVideoStream(streamIndex?: number) { return this.getAVStream(AVMediaType.AVMEDIA_TYPE_VIDEO, streamIndex); } + /** + * Get audio stream + * @param streamIndex The index of the media stream + * @returns + */ public getAudioStream(streamIndex?: number) { return this.getAVStream(AVMediaType.AVMEDIA_TYPE_AUDIO, streamIndex); } + /** + * Seek video packet at a time point + * @param time seek time in seconds + * @returns WebAVPacket + */ public seekVideoPacket(time: number) { return this.getAVPacket(time, AVMediaType.AVMEDIA_TYPE_VIDEO); } + /** + * Seek audio packet at a time point + * @param time seek time in seconds + * @returns WebAVPacket + */ public seekAudioPacket(time: number) { return this.getAVPacket(time, AVMediaType.AVMEDIA_TYPE_AUDIO); } + /** + * Read video packet as a stream + * @param start start time in seconds + * @param end end time in seconds + * @returns ReadableStream + */ public readVideoPacket(start?: number, end?: number) { return this.readAVPacket( start, @@ -287,6 +326,12 @@ export class WebDemuxer { ); } + /** + * Read audio packet as a stream + * @param start start time in seconds + * @param end end time in seconds + * @returns ReadableStream + */ public readAudioPacket(start?: number, end?: number) { return this.readAVPacket( start, @@ -297,6 +342,11 @@ export class WebDemuxer { // =========== custom api for webcodecs =========== + /** + * Generate VideoDecoderConfig from WebAVStream + * @param avStream WebAVStream + * @returns VideoDecoderConfig + */ public genVideoDecoderConfig(avStream: WebAVStream): VideoDecoderConfig { return { codec: avStream.codec_string, @@ -309,6 +359,11 @@ export class WebDemuxer { }; } + /** + * Generate EncodedVideoChunk from WebAVPacket + * @param avPacket WebAVPacket + * @returns EncodedVideoChunk + */ public genEncodedVideoChunk(avPacket: WebAVPacket): EncodedVideoChunk { return new EncodedVideoChunk({ type: avPacket.keyframe === 1 ? "key" : "delta", @@ -318,6 +373,11 @@ export class WebDemuxer { }); } + /** + * Generate AudioDecoderConfig from WebAVStream + * @param avStream WebAVStream + * @returns AudioDecoderConfig + */ public genAudioDecoderConfig(avStream: WebAVStream): AudioDecoderConfig { return { codec: avStream.codec_string || "", @@ -330,6 +390,11 @@ export class WebDemuxer { }; } + /** + * Generate EncodedAudioChunk from WebAVPacket + * @param avPacket WebAVPacket + * @returns EncodedAudioChunk + */ public genEncodedAudioChunk(avPacket: WebAVPacket): EncodedAudioChunk { return new EncodedAudioChunk({ type: avPacket.keyframe === 1 ? "key" : "delta", @@ -339,26 +404,44 @@ export class WebDemuxer { }); } + /** + * Get WebCodecs VideoDecoderConfig + * @returns VideoDecoderConfig + */ public async getVideoDecoderConfig() { const videoStream = await this.getVideoStream(); return this.genVideoDecoderConfig(videoStream); } - public async seekEncodedVideoChunk(timestamp: number) { - const videoPacket = await this.seekVideoPacket(timestamp); + /** + * Seek and return EncodedVideoChunk + * @param time time in seconds + * @returns EncodedVideoChunk + */ + public async seekEncodedVideoChunk(time: number) { + const videoPacket = await this.seekVideoPacket(time); return this.genEncodedVideoChunk(videoPacket); } + /** + * Get WebCodecs AudioDecoderConfig + * @returns AudioDecoderConfig + */ public async getAudioDecoderConfig() { const audioStream = await this.getAudioStream(); return this.genAudioDecoderConfig(audioStream); } - public async seekEncodedAudioChunk(timestamp: number) { - const audioPacket = await this.seekAudioPacket(timestamp); + /** + * Seek and return EncodedAudioChunk + * @param time time in seconds + * @returns EncodedAudioChunk + */ + public async seekEncodedAudioChunk(time: number) { + const audioPacket = await this.seekAudioPacket(time); return this.genEncodedAudioChunk(audioPacket); }