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

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 bufferreturn buf_size;
}

主要实现功能就是从视频 buffer 某个位置开始,copy 一段数据到 io 上下文小 buffer

参数介绍:

opaque: 指向 自定义 BufferData 结构体的实例,因为要从视频 buffer 数据中不断通过 read_packet 读数据到 io 上下文小 buffer。

buf: io 上下文小 buffer 的开始位置,也就是上面定义的ioCtxBuffer,这个位置一直不变,小buffer的数据不断被覆盖

buf_size: 就是 io 上下文小 buffer 的大小,如上定义的IO_CTX_BUFFER_SIZE

  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 要想不花屏需要设置 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相关推荐

  1. ffmpeg的IO操作

    ffmpeg的IO操作主要在libavformat库中实现,部分实现用到了libavutl中的工具.网上有一些介绍ffmpeg的IO的文章,但是有些比较老了,并且一些现在ffmpeg结构已经一些变化, ...

  2. JAVA IO - RandomAccessFile Seek学习

    2019独角兽企业重金招聘Python工程师标准>>> JAVA IO - RandomAccessFile Seek学习 import java.io.RandomAccessFi ...

  3. Ffmpeg 定位文件(seek file)

    有朋友问到ffmpeg播放文件如何定位问题,我想到应该还有一些新手朋友对这一块比较陌生.ffmpeg定位问题用到seek方法,代码 如下: void SeekFrame(AVFormatContext ...

  4. ffmpeg自定义编解码器

    小结 avdeviceffmpeg自定义输出设备OPENGL avcodecffmpeg自定义字节流转换器MJPEG2JPEG avcodecffmpeg自定义硬件加速解码器videotoolbox ...

  5. Arduino ESP8266 自定义IO组网页状态显示与控制-改版

    Arduino ESP8266/32 自定义IO组网页状态显示与控制-改版

  6. ffmpeg 自定义数据来源, 可以是文件,可以是内存,可以是网络, 爱咋的咋的...

    2019独角兽企业重金招聘Python工程师标准>>> // ffmpeg_custom_context.cpp : Defines the entry point for the ...

  7. 小乌龟奔跑的无聊日记~

    2022-1-15 13:34 很开心,终于实现了自由人,近一年没有上班了,加油!加油!再定个3年目标,争取当一个不大不小的老板!骚年加油! 2021-3-24 20:44 很久不讲自己了,目前已从鲁 ...

  8. FFmpeg源码分析:AVIOContext、IO模型与协议

    FFmpeg的IO模型从avio_open()方法开始,核心结构体由AVIOContext和URLProtocol组成.如果需要读取缓冲区buffer数据进行播放,可以通过自定义AVIOContext ...

  9. Java使用FFmpeg(自定义cmd)系列之获取视频/音频时长(File文件方式)

    ffmpeg系列 Java使用FFmpeg(自定义cmd) Java使用FFmpeg(自定义cmd)系列之获取视频/音频时长 Java使用FFmpeg(自定义cmd)系列之官方API获取视频/音频信息 ...

  10. 深聊性能测试,从入门到放弃之:如何对IO进行性能调优

    1.引言 2. 硬盘知识 2.1 磁盘原理 2.2 磁盘接口 2.3 磁盘读写 2.4 磁盘KPI 2.5 计算 2.5.1 IOPS计算 2.5.2 传输速率/吞吐率计算 2.6 IO延时 2.6. ...

最新文章

  1. FSMC(STM32)
  2. P5236-[模板]静态仙人掌【tarjan,LCA】
  3. 35岁前需要完成的事
  4. 使用AVPlayer制作一个播放器
  5. 【学习总结】Git学习-参考廖雪峰老师教程十-自定义Git
  6. JAVA实现杨辉三角的三种方式
  7. 倍福PLC_添加CX2500-0060以太网模块后无法从TwinCAT软件扫描到问题
  8. 为什么正定矩阵等于转置_线性代数28——对称矩阵及正定矩阵,正定性
  9. u盘量产linux pe,WinPE U盘量产ISO
  10. 银行IT系统 -整体架构
  11. 学习笔记——最小生成树
  12. AlertManager警报通知 E-mail 微信 模板
  13. python100天发音_GitHub - hhttss999/Python-100-Days: Python - 100天从新手到大师
  14. PreparedStatement操作Blob类型的变量报错
  15. centos 7 开启80,443端口
  16. SuperMap iDesktopX 影像/栅格数据不用导入导出即可分析处理啦!
  17. pip异常No module named ‘pip‘处理方法
  18. java多音字首字母排序
  19. 痛快 SpringBoot终于禁掉了循环依赖
  20. jvm探秘五:Class类文件结构之属性表

热门文章

  1. conda:未找到命令的解决方法
  2. Xcode8去除控制台多余打印
  3. 树莓派——4G网卡华为ME909s-821 4G上网及开机自启动(1)
  4. python自然语言处理第三章:处理原始文本
  5. java多态性表现在哪两个方面_Java的多态性主要表现在______、______和______三个方面。...
  6. 洛谷P2386放苹果
  7. html当前页面的脚本发生错误,如何解决“当前页面脚本发生错误”的问题
  8. 如何缩减XFS分区格式的根目录
  9. [Java Framework] SpringBoot几种启动后自动初始化的几种方式
  10. 读书笔记(十五)--将才