FFmpeg Custom IO 与实况图片内嵌视频接入
1. 自定义 IO:format 层里普通 IO 和自定义 IO 的区别
在 FFmpeg 的 libavformat 层,demuxer 关心的不是“这是一个什么路径”,而是“我能否持续读到一段可 seek 的字节流”。
这也是普通 IO 和自定义 IO 的本质区别。
1.1 普通 IO
普通 IO 的典型调用方式是:
AVFormatContext *fmt = NULL;
avformat_open_input(&fmt, "a.mp4", NULL, NULL);
avformat_find_stream_info(fmt, NULL);
这种模式下:
- 调用方只提供
url/path - FFmpeg 内部自己完成
open/read/seek AVIOContext仍然存在,但由 FFmpeg 内部创建和维护fmt->pb最终也会有值,但不是业务侧赋的fmt->flags一般不带AVFMT_FLAG_CUSTOM_IO
可以理解成:
业务侧只描述“媒体在哪”,FFmpeg 负责“怎么读”。
1.2 自定义 IO
自定义 IO 的典型调用方式是:
unsigned char *buf = av_malloc(buf_size);
AVIOContext *avio = avio_alloc_context(
buf,
buf_size,
0,
opaque,
read_packet,
write_packet,
seek
);
AVFormatContext *fmt = avformat_alloc_context();
fmt->pb = avio;
fmt->flags |= AVFMT_FLAG_CUSTOM_IO;
avformat_open_input(&fmt, NULL, NULL, NULL);
avformat_find_stream_info(fmt, NULL);
这种模式下:
- 业务侧自己构造
AVIOContext - 业务侧自己定义底层
read/seek/write - FFmpeg 后续的 demux 行为不变,但它读到的数据入口变了
可以理解成:
业务侧不再只告诉 FFmpeg“媒体在哪”,而是直接告诉 FFmpeg“你应该怎么读这段媒体”。
需要注意的是,自定义 IO 并不等同于“只读 IO”。avio_alloc_context 同时支持读、写、seek 三类回调,因此常见组合至少有下面几种:
- 只读输入流
avio_alloc_context(buf, buf_size, 0, opaque, read_packet, NULL, seek);
- 只写输出流
avio_alloc_context(buf, buf_size, 1, opaque, NULL, write_packet, seek);
- 特殊的读写双向流
avio_alloc_context(buf, buf_size, 1, opaque, read_packet, write_packet, seek);
这里第三个参数 write_flag 的语义是“这个 AVIOContext 是否允许写”。因此:
write_flag = 0更适合输入型、只读型场景write_flag = 1更适合输出型,或确实需要写回调参与的场景
本文后续结合实况图片说明时,使用的是第一种:只读输入流。
1.3 这次实况图片场景里的区别
实况图片的物理文件通常是:
[ JPG/HEIC图片数据 ][ 内嵌MP4视频数据 ]
0 offset offset + length
如果走普通 IO:
avformat_open_input(&fmt, "a.jpg", ...)- FFmpeg 会从文件头开始读
- 它先看到的是 JPEG/HEIC 头,而不是 MP4 头
如果走自定义 IO:
- 业务侧把
offset ~ offset + length这段视频区间包装成逻辑输入流 - FFmpeg 看到的“第 0 个字节”已经是内嵌视频段的起点
- 后续 MP4 demux、stream info、解码流程都走原生逻辑
所以这次支持实况图片,并不是修改了 FFmpeg 的 MP4 能力,而是通过 custom IO 改变了数据进入 libavformat 的方式。
2. AVIOContext:如何通过这个上下文结构体实现自定义 IO
AVIOContext 不是 demuxer,也不是 decoder。它是 libavformat 层看到的统一 IO 抽象。
对 demuxer 来说,它只关心三件事:
- 继续读一些字节
- 跳到某个逻辑位置
- 可选:写一些字节
这三件事正是通过 AVIOContext 挂载的函数指针来完成的。
2.1 关键接口:avio_alloc_context
当前实现中,真正把自定义 IO 接进 FFmpeg 的地方是:
pMotionPhotoAVIO = avio_alloc_context(
ioBuf,
IO_BUF_SIZE,
0,
pIOContext,
MotionPhotoRead,
NULL,
MotionPhotoSeek
);
这几个参数分别表示:
ioBuf
FFmpeg IO 层使用的缓存区。IO_BUF_SIZE
上述缓存区大小,这次实现里是32KB。0
write_flag,这里为0表示只读。pIOContext
用户自定义上下文,FFmpeg 不解释它,只会在回调时原样传回。MotionPhotoRead
读回调,对应read_packet。NULL
写回调,这次没实现,因为场景只需要读,不需要写。MotionPhotoSeek
seek 回调。
更完整的理解是:
- 输入 custom IO:通常实现
read_packet + seek - 输出 custom IO:通常实现
write_packet,必要时再实现seek - 双向或特殊协议型 IO:才可能同时实现
read_packet + write_packet + seek
从接口设计上看,avio_alloc_context 的意义就是:
把“缓冲区 + 用户上下文 + 回调函数指针”绑定成一个标准 AVIOContext。
2.2 关键接口:fmt->pb
当前实现中,AVIOContext 创建完成后,会继续做下面这两步:
context->pb = pMotionPhotoAVIO;
context->flags |= AVFMT_FLAG_CUSTOM_IO;
这里做了两件事:
context->pb = pMotionPhotoAVIO
把AVFormatContext的 IO 入口替换成业务侧构造的AVIOContextcontext->flags |= AVFMT_FLAG_CUSTOM_IO
告诉 FFmpeg:这个pb不是默认 IO,而是外部自定义 IO
因此后面:
avformat_open_input(&m_context, nullptr, nullptr, &m_format_opts);
这里传 nullptr 作为输入地址是合理的,因为真正的输入来源已经不再是“路径”,而是 context->pb。
2.3 关键参数:opaque
opaque 是 FFmpeg custom IO 里最重要的桥接参数。
它的特点是:
- FFmpeg 不认识它的类型
- FFmpeg 不关心它的内容
- FFmpeg 只会在回调时把它原样传回
因此它最适合用来保存业务侧真正需要的上下文。
这次代码里定义了 MotionPhotoIOContext:
typedef struct {
FILE* pFile;
int64 llDataOffset;
int64 llDataLength;
int64 llCurrentPos;
} MotionPhotoIOContext;
这四个字段分别表示:
pFile
真实打开的外层 jpg/heic 文件句柄llDataOffset
内嵌视频区间的起始偏移llDataLength
内嵌视频区间的总长度llCurrentPos
当前逻辑流位置,范围是[0, llDataLength]
所以对于这次实现来说,opaque 的作用就是:
把“真实文件 + 视频区间边界 + 当前逻辑位置”带进 read/seek 回调。
2.4 打开完成后还要做什么
AVIOContext 只是解决“怎么把数据喂给 FFmpeg”。
真正打开成功后,还要继续做:
avformat_open_input(...)
avformat_find_stream_info(...)
2.5 资源释放
自定义 IO 还有一个关键点是释放责任。
这次在析构/关闭路径里,针对 custom IO 做了集中释放。释放顺序是:
- 先从
context->pb取出AVIOContext - 再从
AVIOContext->opaque取出MotionPhotoIOContext - 先关闭
pFile - 再释放
opaque - 再释放
AVIOContext的buffer - 最后释放
AVIOContext本身
这也是 custom IO 场景比普通 IO 更容易踩坑的地方:
AVIOContext、opaque、业务文件句柄的所有权必须统一收口,不能多处重复释放。
3. 结合实况图片:如何编写自定义 read / write / seek 回调
这次代码里真正实现了:
readseek
没有实现 write,因为当前场景是“读取实况图片中的视频段”,不涉及写回媒体。
3.1 目标:把外层 jpg 文件伪装成逻辑 MP4 文件
实况图片的真实文件布局可以抽象成:
[ 图片数据 ][ 视频数据 ]
^ offset
^ offset + length
业务侧希望 FFmpeg 看到的不是整个 jpg,而是:
逻辑流:
[ 视频数据 ]
0 length
因此回调需要做的,本质上是坐标映射:
- FFmpeg 的逻辑位置
pos - 对应到真实文件位置
offset + pos
3.2 read 回调怎么写
这次实现的 read 回调大致如下:
int MotionPhotoRead(void* opaque, uint8_t* buf, int size)
{
MotionPhotoIOContext* pContext = (MotionPhotoIOContext*)opaque;
int64_t remaining = pContext->llDataLength - pContext->llCurrentPos;
if (remaining <= 0)
{
return AVERROR_EOF;
}
int toRead = (size < remaining) ? size : (int)remaining;
int bytesRead = fread(buf, 1, toRead, pContext->pFile);
if (bytesRead > 0)
{
pContext->llCurrentPos += bytesRead;
return bytesRead;
}
return AVERROR_EOF;
}
它的逻辑是:
- 从
opaque里取出MotionPhotoIOContext - 计算当前还剩多少可读数据:
remaining = llDataLength - llCurrentPos;
- 本次最多只能读
remaining,不能越过视频段边界 - 调
fread从当前真实文件位置读取 - 成功后推进
llCurrentPos - 到达区间末尾时返回
AVERROR_EOF
这段逻辑的关键不是“怎么读文件”,而是“怎么限制可读边界”。
因为这保证了 FFmpeg 永远不会读到:
offset之前的图片数据offset + length之后的无关数据
read 回调负责把真实文件裁剪成一个只暴露内嵌视频区间的逻辑只读流。
3.3 seek 回调怎么写
这次实现的 seek 回调大致如下:
int64_t MotionPhotoSeek(void* opaque, int64_t offset, int whence)
{
MotionPhotoIOContext* pContext = (MotionPhotoIOContext*)opaque;
int64_t toPos = 0;
if (whence == SEEK_SET)
{
toPos = offset;
}
else if (whence == SEEK_CUR)
{
toPos = pContext->llCurrentPos + offset;
}
else if (whence == SEEK_END)
{
toPos = pContext->llDataLength + offset;
}
else if (whence == AVSEEK_SIZE)
{
return pContext->llDataLength;
}
else
{
return -1;
}
if (toPos < 0 || toPos > pContext->llDataLength)
{
return -1;
}
int64_t filePos = pContext->llDataOffset + toPos;
if (fseek(pContext->pFile, filePos, SEEK_SET) != 0)
{
return -1;
}
pContext->llCurrentPos = toPos;
return toPos;
}
它的逻辑是:
- 先根据
SEEK_SET / SEEK_CUR / SEEK_END算出逻辑位置toPos - 特殊处理
AVSEEK_SIZE:
return llDataLength;
这里返回的是视频区间长度,而不是整个 jpg 文件长度。
- 做边界检查,只允许
toPos落在[0, llDataLength] - 映射到真实文件位置:
filePos = llDataOffset + toPos;
- 调
fseek(pFile, filePos, SEEK_SET) - 更新
llCurrentPos
对实况图片来说,seek 的本质不是“在 jpg 文件里 seek”,而是:
在“内嵌视频逻辑空间”里 seek,再翻译成真实文件偏移。
这正是 AVIOContext 能把一个大文件中的子区间伪装成独立媒体文件的核心原因。
4. 总结
这次实况图片支持里,FFmpeg 相关的核心改动可以总结成三句话:
- 普通 IO 和自定义 IO 的区别,不在 demux 逻辑,而在数据进入
libavformat的入口。 AVIOContext是libavformat的统一 IO 抽象,自定义 IO 的本质是业务侧接管它的read/seek/write行为。- 在实况图片场景中,通过
MotionPhotoIOContext + MotionPhotoRead + MotionPhotoSeek,把 jpg/heic 文件中的内嵌视频区间虚拟成一个独立的逻辑 MP4 输入流,从而复用 FFmpeg 原生的 MP4 解复用与解码流程。

426

被折叠的 条评论
为什么被折叠?



