ffmpeg AVIOContext 自定義 IO 及 seek

原文地址html

AvIOContext

使用場景是: 使用 ffmpeg 相關解碼代碼要編譯成 wasm 在瀏覽器端使用,js 層面拿到視頻 buffer 數據(拉取的 m3u8 分片也好,本地上傳的視頻文件等等),將 buffer 傳遞給 c 解封裝、解碼,這時候就用到 AVIOContext

AVIOContext瀏覽器

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 主要概念的圖示:ide

iocontext.png

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 上下文使用流程

  1. 定義一個結構體,存儲視頻大 buffer 數據相關信息
typedef struct _BufferData
{
  uint8_t *ptr; // 指向buffer數據中 `還沒被io上下文消耗的位置`
  uint8_t *ori_ptr; // 也是指向buffer數據的指針,之因此定義ori_ptr,是用在自定義seek函數中
  size_t size; // 視頻buffer還沒被消耗部分的大小,隨着不斷消耗,愈來愈小
  size_t file_size; //原始視頻buffer的大小,也是用在自定義seek函數中
} BufferData;

// 拿到視頻buffer數據及長度 定義bd 存儲相關信息
BufferData bd;
bd.ptr = buffer
bd.ori_ptr = buffer
bd.size =xxxx
bd.file_size=xxxx
  1. 分配 io 上下文使用的小 buffer
#define IO_CTX_BUFFER_SIZE 4096 * 4;
uint8_t *ioCtxBuffer = av_malloc(IO_CTX_BUFFER_SIZE);
  1. 自定義 read_packet() 、seek()方法 (自定義 seek 方法下面 seek 部分介紹)
static int read_packet(void *opaque, uint8_t *buf, int buf_size)
{
  BufferData *bd = (BufferData *)opaque;
  buf_size = MIN(bd->size, buf_size);

  if (!buf_size)
  {
    printf("no buf_size pass to read_packet,%d,%d\n", buf_size, bd->size);
    return -1;
  }
  printf("ptr in file:%p io.buffer ptr:%p, size:%ld,buf_size:%ld\n", bd->ptr, buf, bd->size, buf_size);
  memcpy(buf, bd->ptr, buf_size);
  bd->ptr += buf_size;
  bd->size -= buf_size; // left size in buffer
  return buf_size;
}
主要實現功能就是從視頻 buffer 某個位置開始,copy 一段數據到 io 上下文小 buffer

參數介紹:ui

opaque: 指向 自定義 BufferData 結構體的實例,由於要從視頻 buffer 數據中不斷經過 read_packet 讀數據到 io 上下文小 buffer。this

buf: io 上下文小 buffer 的開始位置,也就是上面定義的ioCtxBuffer,這個位置一直不變,小buffer的數據不斷被覆蓋spa

buf_size: 就是 io 上下文小 buffer 的大小,如上定義的IO_CTX_BUFFER_SIZEdebug

  1. 分配 io 上下文
AVIOContext *avioCtx;
avioCtx = avio_alloc_context(ioCtxBuffer, IO_CTX_BUFFER_SIZE, 0, &bd, &read_packet, NULL,NULL);
  1. 建立 AVFormatContext,並掛載 io 上下文
AVFormatContext *fmtCtx = avformat_alloc_context();
fmtCtx.pb = avioCtx
fmtCtx->flags |= AVFMT_FLAG_CUSTOM_IO;

至此,就經過自定義 io 上下文,讓 AVFormatContext 能夠解封裝提供的視頻 buffer 數據了,以後的解封裝、解碼流程和不使用 io 上下文同樣指針

seek

seek 功能研究卡住了幾天,緣由有二,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 要想不花屏須要設置 flag AVSEEK_FLAG_BACKWARD

對於 mp4 格式沒毛病,seek 到裏指定 pts 以前最近的關鍵幀,而後開始解碼,從關鍵幀到指定的 pts 以前的視頻幀能夠手動丟棄,而後從指定 pts 位置開始展現

對於 ts 格式效果就沒那麼好,av_seek_frame() 對 ts 是會精確的 seek 到指定的 pts 位置的,但找不到 pts 以前最近的關鍵幀,指定 AVSEEK_FLAG_BACKWARD 也不行,效果就是: 從指定的 pts 位置開始解碼,花屏直到遇到下一個關鍵幀,對於單個 ts 分片,只在開頭有一個關鍵幀的這種,seek 後可能從指定的 pts 位置開始直接所有解碼失敗了。測試對一個 ts 文件 經過 ffplay ffplay -ss 秒數 -i filepath 進行 seek 播放,要麼只有聲音播放沒有畫面、要麼先花屏一會。因此通常的 hls ts seek 操做都是不精準 seek,跨分片的。

最後,av_seek_frame()後須要刷新解碼器上下文

avcodec_flush_buffers(audioState.codecCtx);
avcodec_flush_buffers(videoState.codecCtx);
相關文章
相關標籤/搜索