0.前言
前文我们讨论了FFmpeg中的封装和解封装操作,了解到了ffmpeg支持很多种不同格式的网络流媒体协议,只需要输入对应的url就可以进行操作。本文将讨论FFmpeg是如何实现对不同协议的支持,以及一种常用的自定义IO的多媒体文件处理方式。
1. 介绍
1.1 协议
首先需要明确的是“协议”的概念,准确上来说,FFmpeg中“协议”指的是“承载多媒体容器文件的传输协议”,也就是“传输多媒体文件的方式”,其最终要获得的净数据是“多媒体容器文件”,比如mp4,flv等。
所以ffmpeg支持协议的过程便是,用户输入一种传输协议(的标志),就是我们输入的url,从中提取出我们需要的“多媒体容器文件”。
1.2 AVIO
AVIO实际上是libavformat里面的一个子模块,头文件在“libavformat/avio.h”主要是提供“带缓存的IO输出接口”,我们可以类比C标准库中“stdio.h”的作用。除此之外,AVIO库还支持根据用户输入不同的url,选择对应的传输协议,在此基础之上完成正确的IO操作。我们可以根据下面脑图的梳理,对AVIO库有一个大致的印象。
AVIO支持两种输入方式,一是以URL作输入,FFmpeg内部会识别url的协议类型,底层会使用协议对应的读写操作。一是以自定义参数和读写方法为输入,用户可以提供自定义的buffer的读写接口,也就是我们后续谈论的自定义IO。
1.3 URL模式
我们可以根据下图来理解一下ffmpeg对于URL输入,和协议解析的整体结构设计。
首先,正如前文我们使用解封装时,需要使用avio_open2()打开文件,AVFormatContext的pb成员指向的是一个AVIOContext结构,对应的是一个输入/输出时的IO上下文。
以url作为输入的AVIOContext,此时opaque字段会指向一个URLContext结构体,这个结构体并未存在于FFmpeg公开的头文件中,换言之,协议只用于FFmpeg内部私有。如果我们想添加一个私有的协议,则必须修改FFmpeg的代码。
URLContext实际上是一个所有不同协议的公共上下文,存放着一些都会用到的字段。最主要的是其中的两个成员。
URLContext的prot字段,指向一个URLProtocol对象,根据输入url所用的协议的不同,其具体可能是ff_file_protocol(使用本地文件),ff_rtmp_protocol(RTMP流媒体协议),ff_udp_protocol(UDP协议)等,这些具体的协议对象主要是标明不同协议的具体行为,有着URLProtocol所定义的不同的接口实现。这些协议对象是在FFmpeg编译时创建,并保存在url_protocols这个数组中,当我们输入url时会根据其进行判断和选择。
URLContext的priv_data字段,指向的是不同协议的私有数据上下文,不同的协议对应的均不相同,如ff_file_protocol文件协议对应的上下文为FileContext结构体,URLProtocol结构体中priv_ata_size便是对应的私有Context的大小。
1.4 自定义IO模式
虽然AVIO的URL输入模式可以很方便地支持不同协议的IO操作,但是比如当我们的输入数据已经处于内存中,需要从内存中读入时,这种输入方式便不可用。所幸的是,AVIO也支持自定义的方式来读写数据。
如果我们需要使用到自定义IO模式,我们需要手动创建AVIOContext,并将可能用到的自定义read、write、seek方法,以及输入自定义参数设置给AVIOContext的opaque字段,在后续使用AVFormatContext进行读写时,会回调我们输入的方法。
我们主要是使用avio_alloc_context()来定制我们的IO操作,其接口定义如下:
/*
* buffer : AVIO使用到的读写缓存区
* buffer_size : buffer的大小
* write_flag : buffer是否可写
* opaque : 自定义输入参数
* read_packet : read方法函数指针, av_read_frame()调用时会回调该方法
* write_packet : write方法函数指针, av_write_frame()等调用时会回调
* seek : seek方法函数指针,当以字节为单位seek相关时会回调
*/
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));
另外,需要说明的是,由于opaque字段并不限定类型类型,所以我们可以传入不同的数据类型,并不限定于是一段内存,来实现对输入输出进行控制。比如,我们可以传入一个socket对象、fifo对象,或一个ssl的socket,这样都是可以的。
可以看到上面URL模式下AVIO的句柄也是在opaque字段,换言之我们也可以通过自定义IO的方式,实现自定义协议的支持,除此之外,类似于本地缓存的需要,也可以用该方式实现。
2. 示例
下面以一个示例来讲解AVIO的自定义IO模式的用法。以下示例已上传至github.com/zzakafool/M… 文件夹:4_BufferedIO
在下面例子中,我们映射了一个视频,将其完全读入到内存中,并通过内存拷贝的方式,自定义设置读取方法,解封装后统计视频里的所有帧数(包括不同的流)。
#include "bufferedIO.h"
extern "C" {
#include <libavformat/avformat.h>
#include <libavformat/avio.h>
}
const int AVIO_BUFFER_SIZE = 4096;
struct InputBufferData {
uint8_t *ptr = nullptr;
int size = 0;
int pos = 0;
};
int buffer_read_packet(void *opaque, uint8_t *buf, int buf_size) {
InputBufferData *ibd = (InputBufferData *)opaque;
// 超出则返回eof, 0
if(ibd->pos >= ibd->size) {
return 0;
}
// 只能读到末尾,防止超出
int read_len = buf_size;
if(ibd->pos + read_len >= ibd->size) {
read_len = ibd->size - ibd->pos;
}
memcpy(buf, ibd->ptr + ibd->pos, read_len);
ibd->pos += read_len;
return read_len;
}
void bufferedIO(uint8_t *buffer, int size) {
av_log(NULL, AV_LOG_INFO, "buffer size : %d\n", size);
// 将输入的文件buffer放入结构体中保存
InputBufferData ibd = {
.ptr = buffer,
.size = size,
};
// 创建AVFormatContext
AVFormatContext *fmtCtx = nullptr;
fmtCtx = avformat_alloc_context();
if(fmtCtx == nullptr) {
av_log(NULL, AV_LOG_ERROR, "Allocate avformat context failed");
return;
}
// 创建AVIOContext用到的IO缓存区
unsigned char* avio_ctx_buffer = (unsigned char *)av_malloc(AVIO_BUFFER_SIZE);
// 创建AVIOContext,并设置自定义的buffer_read_packet
AVIOContext *ioCtx = avio_alloc_context(avio_ctx_buffer, AVIO_BUFFER_SIZE, 0, &ibd, buffer_read_packet, NULL, NULL);
if(ioCtx == nullptr) {
av_log(NULL, AV_LOG_ERROR, "Allocate AVIOContext failed");
avformat_free_context(fmtCtx);
return;
}
// 设置给AVFormatContext
fmtCtx->pb = ioCtx;
// 剩下的就是解封装流程
if(avformat_open_input(&fmtCtx, NULL, NULL, NULL) < 0) {
av_log(NULL, AV_LOG_ERROR, "AVFormat open input failed");
avio_context_free(&ioCtx);
avformat_free_context(fmtCtx);
return;
}
// 解析流信息
if(avformat_find_stream_info(fmtCtx, NULL) < 0) {
av_log(NULL, AV_LOG_ERROR, "Find stream info failed");
avio_context_free(&ioCtx);
avformat_close_input(&fmtCtx);
}
AVPacket *packet = av_packet_alloc();
if(packet == nullptr) {
av_log(NULL, AV_LOG_ERROR, "Allocate Packet failed.\n");
avio_context_free(&ioCtx);
avformat_close_input(&fmtCtx);
return;
}
// 统计帧数
int packet_num = 0;
while(av_read_frame(fmtCtx, packet) >= 0) {
++packet_num;
// do something for compressed data.
av_packet_unref(packet);
}
av_log(NULL, AV_LOG_INFO, "Total %d packets\n", packet_num);
av_packet_free(&packet);
avio_context_free(&ioCtx);
avformat_close_input(&fmtCtx);
}
main函数:
#include "bufferedIO.h"
#include <unistd.h>
#include <fcntl.h>
#include <iostream>
#include <sys/mman.h>
int main() {
// 打开测试资源
int fd = open("../res/big_buck_bunny.mp4", O_RDONLY);
if(fd < 0) {
std::cerr << "Open file failed" <<std::endl;
return -1;
}
// 获取文件长度
int len = lseek(fd, 0, SEEK_END);
if(len == -1) {
std::cerr << "Get file len failed" <<std::endl;
return -2;
}
// 映射至内存中
uint8_t* buf = nullptr;
buf = (uint8_t *)mmap(nullptr, len, PROT_READ, MAP_SHARED, fd, 0);
if(buf == nullptr) {
std::cerr << "Mmap file failed" << std::endl;
return -3;
}
bufferedIO(buf, len);
// 解除映射
if(munmap(buf, len) < 0) {
std::cerr << "Munmap file failed" << std::endl;
return -4;
}
return 0;
}