在iOS 上,开发者可通过VideoToolbox
实现硬解码,其具体的步骤可见iOS 硬解码总结。本文讲的是使用FFMpeg
进行视频流的软解码过程,因本demo中包含编译的iOS 下ffmpeg库,demo 较大,仓库已迁移到在gitee上可见。
为方便理解,我把软解码整个过程分为几个步骤,第一是读取,第二是解码,第三是渲染。
读取数据我将其分为如下几个步骤:
AVFormatContext
AVFormatContext
,对其内部的AVstream
进行赋值,得到数据信息AVPacket
具体代码如下:
// 注册编解码器
av_register_all();
AVDictionary *opts = NULL;
av_dict_set(&opts, "timeout", "1000000", 0);
// 解析地址或文件
int ret = avformat_open_input(&pFormatContext, path.UTF8String, NULL, &opts);
if (ret < 0) {
av_log(NULL, AV_LOG_DEBUG, "avformat_open_input失败");
}
// 查找数据信息,按照雷神的说法,该方法其实已经完成了部分解码的过程
if (avformat_find_stream_info(pFormatContext, &opts) < 0) {
av_log(NULL, AV_LOG_DEBUG, "没找到数据流信息\n");
}
// 查找视频流数据下标. 一个文件或者数据流可能存在音频、视频、字幕流
for (int i = 0; i < pFormatContext->nb_streams; i++) {
if (pFormatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
pVideoIndex = i;
break;
}
}
if (pVideoIndex == -1) {
av_log(NULL, AV_LOG_ERROR, "数据中不存在视频流\n");
}
读取指定流中的待解码数据:
dispatch_async(readQueue, ^{
while (!self->pStopParse) {
if (!self->pFormatContext) {
break;
}
AVPacket pkt;
av_init_packet(&pkt);
int ret = av_read_frame(self->pFormatContext, &pkt);
if (ret < 0 || pkt.size < 0) {
av_log(NULL, AV_LOG_ERROR, "读取数据失败\n");
self->pStopParse = YES;
break;
}
if (pkt.stream_index == self->pVideoIndex) {
// 视频数据传递
if (packet) {
packet(pkt);
}
}
av_packet_unref(&pkt);
}
// avformat_close_input(&self->pFormatContext);
// self->pFormatContext = NULL;
});
解码数据的步骤可分为:
AVCodecContext
得到codec 上下文AVCodecContext
较为复杂点是在获取CodecContext
上,先上代码
pCodecContext = avcodec_alloc_context3(NULL);
AVCodecParameters *parameters = pFormatContext->streams[pVideoIndex]->codecpar;
ret = avcodec_parameters_to_context(pCodecContext, parameters);
AVCodec *codeC = avcodec_find_decoder(parameters->codec_id);
if (codeC == NULL) {
NSLog(@"没有对应的解码器");
}
if (avcodec_open2(pCodecContext, codeC, NULL) < 0 ) {
NSLog(@"创建解码器上下文失败");
}
上文创建AVCodecContext
分为几个步骤,初始化context,对context内部结构体进行赋值,查找到对应的解码器,使用avcodec_open2
解码器的内部配置。
完成了以上步骤,似乎就只剩下查找I帧和解码的格式操作了,但是有一个关键的点在于使用上面步骤解析出来的AVFrame
默认格式是yuv420p,在使用目前的OpenGLES 是无法完成pixelbuffer 的构建的,需要使用sws_getcontext
才能完成图像构建。
所以回过头,我们需要对创建AVCodecContext
时的一些参数进行配置,指定其pix_fmt
格式。
我查找了资料,和参照了其他人的demo 发现,可以配置ffmpeg 的硬件解码来生成对应的数据格式。所以对以上代码进行配置增加后如下:
const char *codecName = av_hwdevice_get_type_name(AV_HWDEVICE_TYPE_VIDEOTOOLBOX);
enum AVHWDeviceType type = av_hwdevice_find_type_by_name(codecName);
int ret = av_find_best_stream(formatContext, AVMEDIA_TYPE_VIDEO, -1, -1, &codec, 0);
pCodecContext = avcodec_alloc_context3(NULL);
ret = avcodec_parameters_to_context(pCodecContext, parameters);
if (ret < 0) {
NSLog(@"解码器配置失败");
}
// 硬件设备支持的赋值,sws_getcontext 这个类似,但sws——getcontext是成像
ret = av_hwdevice_ctx_create(&hw_device_ctx, type, NULL, NULL, 0);
通过该配置就保证了数据格式的匹配。
寻找i帧很简单,AVPacket
结构体内部的flag
字段 为1 时,为keyFrame。其内部代码定义如下:
#define AV_PKT_FLAG_KEY 0x0001 ///< The packet contains a keyframe
#define AV_PKT_FLAG_CORRUPT 0x0002 ///< The packet content is corrupted
解码AVPacket
使用的是avcodec_send_packet
和 avcodec_receive_frame
将对应的未解码数据解码成AVFrame
对象。
渲染层面本人了解的还不够详细,就不写了,不然错的太多,引大家入了歧途不好,代码借用快手大神小东邪的渲染代码来完成。
本文参考资料:
雷神:FFMPEG 实现 YUV,RGB各种图像原始数据之间的转换
简书: FFmpeg 示例硬件解码hw_decode