【写在前面】
本篇只单讲 FFmpeg 解码视频,即使含有音频。
本篇主要内容:
1、多媒体基础概念
2、视频基础概念
3、FFmpeg解码基本流程
【正文开始】
- 首先,稍微解释一下,什么是多媒体。
从字面意思看:多媒体 (Multimedia) 是多种媒体的综合,一般包括文本,声音和图像等多种媒体形式。
实际上,容纳了多种媒体的集合,也可以称之为多媒体容器。
当然,整个多媒体的格式非常多,比如我们常见的视频格式有:mp4、avi、wmv、mkv,音频格式有:mp3、wav等等。
可能你还听说过h.264、mpeg4格式,然而那些都是编码格式,与多媒体的格式不同。
要注意,这里的容器和编码是两个不同的概念:
对于一个多媒体容器,它可以包含视频,音频,字幕等等,而编码则是压缩视频、音频的某个算法。
例如,有一个MP4文件(容器),它里面有使用 A 编码的视频和使用 B 编码的音频,而且 A 和 B 并不是固定的,可以是任意。
因此,想要播放一部视频的第一步,就是要知道 编码 A 和 编码 B。
这一步一般称之为解复用 Demux ( 或者叫解封装 )。
- 现在进入第一步:解复用。
我们先不管各种AV*、AV*Context等等各种数据结构,这里先讲流程:
avformat_open_input(&formatContext, m_filename.toStdString().c_str(), nullptr, nullptr);
avformat_find_stream_info(formatContext, nullptr);
videoIndex = av_find_best_stream(formatContext, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0);
if (videoIndex >= 0)
videoStream = formatContext->streams[videoIndex];
if (videoStream)
videoDecoder = avcodec_find_decoder(videoStream->codecpar->codec_id);
if (videoDecoder)
codecContext = avcodec_alloc_context3(videoDecoder);
if (codecContext)
avcodec_parameters_to_context(codecContext, videoStream->codecpar);
if (codecContext)
avcodec_open2(codecContext, videoDecoder, nullptr);
1、使用 avformat_open_input() 来打开一个媒体文件。
2、使用 avformat_find_stream_info() 读取媒体文件的数据以获取流信息。
所谓的流是码流,即经过编码后的数据流。
3、使用 av_find_best_stream() 找到匹配(最佳)的流并返回其索引,记住,所有的流存在于 formatContext->streams 数组中。实际上,到这里解复用就算是结束了,不过,在解码之前,还需要一些额外的工作。
4、使用 avcodec_find_decoder() 找到匹配的编解码器。
5、使用 avcodec_alloc_context3() 分配一个 AVCodecContext *,后面需要使用 avcodec_free_context() 来释放它。
6、使用 avcodec_parameters_to_context() 使用编解码器中相关值来填充编解码器上下文 ( AVCodecContext )。
7、最后,打开编解码上下文:avcodec_open2() 。
- 一切准备就绪,现在进入第二步,开始解码。
前面提到过,编码就是使用某种算法把视频( 或者音频 ) 给压缩了 视频 -> 码流。
那么解码就是把码流还原成视频, 码流 -> 视频。
AVPacket *packet = av_packet_alloc();
AVFrame *frame = av_frame_alloc();
packet->data = nullptr;
packet->size = 0;
while (m_runnable && av_read_frame(formatContext, packet) >= 0) {
if (packet->stream_index == videoIndex) {
//发送给解码器
int ret = avcodec_send_packet(codecContext, packet);
while (ret >= 0) {
//从解码器接收解码后的帧
ret = avcodec_receive_frame(codecContext, frame);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) break;
else if (ret < 0) return;
int dst_linesize[4];
uint8_t *dst_data[4];
av_image_alloc(dst_data, dst_linesize, m_width, m_height, AV_PIX_FMT_RGB24, 1);
sws_scale(swsContext, frame->data, frame->linesize, 0, frame->height, dst_data, dst_linesize);
QImage image = QImage(dst_data[0], m_width, m_height, QImage::Format_RGB888).copy();
av_freep(&dst_data[0]);
m_frameQueue.enqueue(image);
av_frame_unref(frame);
}
}
av_packet_unref(packet);
}
1、初始化一个 AVPacket 和 AVFrame。
注意:FFmpeg很多时候都要求传入的指针被初始化为nullptr,例如packet->data。
2、使用 av_read_frame() 读取一帧数据包。
3、使用 avcodec_send_packet() 将数据包发送给解码器。
4、使用 avcodec_receive_frame() 接收一帧解码器解码后的数据( 这里是图像 ),然后我用 sws_scale() 将其转换成 QImage ,接着定时( 根据 fps )显示 image 即可播放。
5、av_frame_unref(frame),av_packet_unref(packet) 和 C++中的引用计数类似,不过需要手动取消计数,注意,如果不取消引用计数,就不会释放内存。
至此,我们就完成了使用 FFmpeg 解码视频的基本流程。
这里,我画了个图,大概就是这样:
【结语】
本篇主要就讲了视频的一些基本的概念,因为我也还在学习中。
然后这里只有一些关键部分的代码,完整代码见:https://github.com/mengps/FFmpeg-Learn,编译运行就可以看到效果了。
还有关于图像转换部分的也没有讲,毕竟这里主要讲解码流程,所以可能后面会进行讲解吧。