ffmpeg 自定义IO与Seek
AvIOContext使用場景是: 使用 ffmpeg 相關解碼代碼要編譯成 wasm 在瀏覽器端使用,js 層面拿到視頻 buffer 數據(拉取的 m3u8 分片也好,本地上傳的視頻文件等等),將 buffer 傳遞給 c 解封裝、解碼,這時候就用到 AVIOContextAVIOContext AVIOContext 主要使用邏輯: 我們有一塊大的視頻文件 buffer,然后 ffmpeg 對這塊數據的訪問借助于 io 上下文,io 上下文上自己維護了一個小的 buffer(注意:這個小的buffer也是需要我們手動給io上下文分配),之后 ffmpeg 內部解封轉、解碼需要的數據都從io上下文這個小buffer要, io 上下文上這個小 buffer 數據不足時又自己從我們的視頻大 buffer 中不斷補充數據先看此上下文結構體中一些重要的屬性:AVIOContext *avioCtx;avioCtx->buffer // 即io上下文中那個小buffer,通過avio_alloc_context()來分配io上下文時作為參數傳遞avioCtx->buffer_size // 小buffer的大小 avioCtx->buf_ptr // io上下文的小buffer中的數據當前被消耗的位置 avioCtx->buf_end // io上下文的小buffer數據的結束位置 avioCtx->opaque // ** 是一個自定義的結構體,存儲視頻大buffer的信息,如buffer開始位置,視頻buffer長度,這個結構會回傳給 read_packet、write_packet、seek回調函數!!! **, avioCtx->read_packet // 需要自己實現的一個 `用視頻大buffer數據` 填充 `io上下文小buffer`的回調函數 avioCtx->write_packet //自己實現 把io上下文中的小buffer數據寫到某處的回調函數 avioCtx->seek // 也是自己實現的 用來在視頻大buffer中seek的函數幾個回調函數使用見后面介紹avio.h 中關于 io 上下文 buffer 主要概念的圖示: avio_alloc_context()方法簽名: AVIOContext *avio_alloc_context(unsigned char *buffer,int buffer_size,int write_flag,void *opaque,int (*read_packet)(void *opaque, uint8_t *buf, int buf_size),int (*write_packet)(void *opaque, uint8_t *buf, int buf_size),int64_t (*seek)(void *opaque, int64_t offset, int whence));這幾個參數分別對應了上面結構體介紹中的對應屬性,看實際使用流程: io 上下文使用流程定義一個結構體,存儲視頻大 buffer 數據相關信息 分配 io 上下文使用的小 buffer 自定義 read_packet() 、seek()方法 (自定義 seek 方法下面 seek 部分介紹) 參數介紹: opaque: 指向 自定義 BufferData 結構體的實例,因為要從視頻 buffer 數據中不斷通過 read_packet 讀數據到 io 上下文小 buffer。 buf: io 上下文小 buffer 的開始位置,也就是上面定義的ioCtxBuffer,這個位置一直不變,小buffer的數據不斷被覆蓋 buf_size: 就是 io 上下文小 buffer 的大小,如上定義的IO_CTX_BUFFER_SIZE 分配 io 上下文 創建 AVFormatContext,并掛載 io 上下文 至此,就通過自定義 io 上下文,讓 AVFormatContext 可以解封裝提供的視頻 buffer 數據了,之后的解封裝、解碼流程和不使用 io 上下文一樣 seekseek 功能研究卡住了幾天,原因有二,1: AVIOContext 自定義 seek 函數的實現邏輯不清楚, 2: 對 ts 格式文件 精準 seek 存在花屏或解碼失敗問題,原以為自己實現邏輯存在問題,實際上對于 ts 格式,沒有像 mp4 一樣有地方存儲所有關鍵幀的位置偏移信息,ffmpeg 也無能為力ffmpeg 中 seek 功能通過 av_seek_frame()方法來進行 /*** Seek to the keyframe at timestamp.* 'timestamp' in 'stream_index'.** @param s media file handle* @param stream_index If stream_index is (-1), a default* stream is selected, and timestamp is automatically converted* from AV_TIME_BASE units to the stream specific time_base.* @param timestamp Timestamp in AVStream.time_base units* or, if no stream is specified, in AV_TIME_BASE units.* @param flags flags which select direction and seeking mode* @return >= 0 on success*/ int av_seek_frame(AVFormatContext *s, int stream_index, int64_t timestamp,int flags);/*** Seek to timestamp ts.* Seeking will be done so that the point from which all active streams* can be presented successfully will be closest to ts and within min/max_ts.* Active streams are all streams that have AVStream.discard < AVDISCARD_ALL.** If flags contain AVSEEK_FLAG_BYTE, then all timestamps are in bytes and* are the file position (this may not be supported by all demuxers).* If flags contain AVSEEK_FLAG_FRAME, then all timestamps are in frames* in the stream with stream_index (this may not be supported by all demuxers).* Otherwise all timestamps are in units of the stream selected by stream_index* or if stream_index is -1, in AV_TIME_BASE units.* If flags contain AVSEEK_FLAG_ANY, then non-keyframes are treated as* keyframes (this may not be supported by all demuxers).* If flags contain AVSEEK_FLAG_BACKWARD, it is ignored.AVIOContext 自定義 seek 函數 io 上下文 seek 函數的主要邏輯是: 原始視頻大 buffer 和長度知道,通過 seek 方法來把大 buffer 的要讀取位置指定到某個位置回調簽名: int64_t(* seek )(void *opaque, int64_t offset, int whence)參數介紹: opaque: 同 read_packet 回調,原始視頻 buffer 信息的結構體 offset: 要 seek 到的位置,可以是相對原始視頻的起始位置,可以是相對 io 上下文小 buffer 的起始位置,取決于 whence whence: seek 的類型,取值為 AVSEEK_SIZE 、SEEK_CUR 、SEEK_SET、SEEK_END, AVSEEK_SIZE: 不進行 seek 操作,而是要求返回 視頻 buffer 的長度大小 SEEK_CUR: 表示 offset 是相對 io 上下文小 buffer 開始位置的 SEEK_SET: 表示 offset 是相對 原始 buffer 開始位置的 SEEK_END: 表示 offset 是相對 原始 buffer 結束位置的 通過 debug av_seek_frame --> seek_frame_internal ---> seek_frame_byte --->avio_seek() 發現對 iocontext 自定義的 seek 方法是在 avio_seek() 中使用的。發現 avio_seek 中調用 ioContext->seek()時 whence 只會傳遞 AVSEEK_SIZE 或 SEEK_SET 所以自定義 io seek 函數實現如下即可: static int64_t seek_in_buffer(void *opaque, int64_t offset, int whence) {BufferData *bd = (BufferData *)opaque;int64_t ret = -1;// printf("whence=%d , offset=%lld , file_size=%ld\n", whence, offset, bd->file_size);switch (whence){case AVSEEK_SIZE:ret = bd->file_size;break;case SEEK_SET:bd->ptr = bd->ori_ptr + offset;bd->size = bd->file_size - offset;ret = bd->ptr;break;}return ret; }精準 seek av_seek_frame 要想不花屏需要設置 flagAVSEEK_FLAG_BACKWARD 對于 mp4 格式沒毛病,seek 到里指定 pts 之前最近的關鍵幀,然后開始解碼,從關鍵幀到指定的 pts 之前的視頻幀可以手動丟棄,然后從指定 pts 位置開始展示 對于 ts 格式效果就沒那么好,av_seek_frame() 對 ts 是會精確的 seek 到指定的 pts 位置的,但找不到 pts 之前最近的關鍵幀,指定 AVSEEK_FLAG_BACKWARD 也不行,效果就是: 從指定的 pts 位置開始解碼,花屏直到遇到下一個關鍵幀,對于單個 ts 分片,只在開頭有一個關鍵幀的這種,seek 后可能從指定的 pts 位置開始直接全部解碼失敗了。測試對一個 ts 文件 通過 ffplayffplay -ss 秒數 -i filepath進行 seek 播放,要么只有聲音播放沒有畫面、要么先花屏一會。所以一般的 hls ts seek 操作都是不精準 seek,跨分片的。 最后,av_seek_frame()后需要刷新解碼器上下文 avcodec_flush_buffers(audioState.codecCtx); avcodec_flush_buffers(videoState.codecCtx); |
總結
以上是生活随笔為你收集整理的ffmpeg 自定义IO与Seek的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【玩转rom助手】专属刷机的小帮手
- 下一篇: 许愿