目录
- [1. 打开视频文件](#1. 打开视频文件)
- [2. 查找解码器 avcodec_find_decoder](#2. 查找解码器 avcodec_find_decoder)
- [3. 创建解码器上下文 avcodec_alloc_context3](#3. 创建解码器上下文 avcodec_alloc_context3)
- [4. 打开解码器上下文 avcodec_open2](#4. 打开解码器上下文 avcodec_open2)
- [5. 初始化音视频码流解析器 av_parser_init](#5. 初始化音视频码流解析器 av_parser_init)
- [6. 初始化SDL部分](#6. 初始化SDL部分)
- [7. 读取文件数据](#7. 读取文件数据)
- [8. 解析音视频裸流数据,从中分离出一帧一帧的压缩编码数据 av_parser_parse2](#8. 解析音视频裸流数据,从中分离出一帧一帧的压缩编码数据 av_parser_parse2)
- [9. 发送解码 avcodec_send_packet](#9. 发送解码 avcodec_send_packet)
- [10. 接收解码 avcodec_receive_frame](#10. 接收解码 avcodec_receive_frame)
- [11. SDL更新、渲染](#11. SDL更新、渲染)
- [12. 冲刷解码器](#12. 冲刷解码器)
- [13. 内存回收](#13. 内存回收)
- 代码合集
- 视频效果截图
之前已经使用ffmpeg编码生成了一段.h264的视频(800_600_25.h264),现在,将继续使用ffmpeg,将其解码出来;显示部分可以使用SDL2进行渲染;
整理流程如下:
cpp
开始(begin)
↓
1.打开视频文件(std::ifstream::open)
↓
2.查找解码器(avcodec_find_decoder)
↓
3.创建解码器上下文(avcodec_alloc_context3)
↓
4.打开解码器上下文(avcodec_open2)
↓
5.初始化音视频码流解析器(av_parser_init)
↓
6.初始化SDL部分
↓
7.读取文件数据(std::ifstream::read)
↓
8.解析音视频裸流数据(av_parser_parse2)
↓
9.发送解码(avcodec_send_packet)
↓
10.接收解码(avcodec_receive_frame)
↓
11.SDL更新、渲染
↓
12.冲刷解码器
↓
13.内存回收
↓
结束(end)
包含头文件:
cpp
#include <iostream>
#include <fstream>
#include <thread>
#include <chrono>
extern "C" {
#include "libavcodec/avcodec.h"
#include "libavutil/avutil.h"
}
#include "SDL.h"
// 打印错误信息
static void PrintErr(int ret) {
char buf[1024] = { 0 };
av_strerror(ret, buf, sizeof(buf));
std::cout << buf << std::endl;
}
1. 打开视频文件
使用了C++的std::ifstream文件处理
cpp
std::ifstream ifs;
ifs.open("800_600_25.h264", std::ios::binary);
if (!ifs.is_open()) {
std::cout << "ifs open file failed!\n";
return -1;
}
注意,一定要以二进制方式打开文件,否则视频会出现乱码!
2. 查找解码器 avcodec_find_decoder
avcodec_find_decoder 用于根据指定的编码器 ID 查找并返回对应的解码器。
①函数原型
cpp
const AVCodec *avcodec_find_decoder(enum AVCodecID id);
②参数说明
| 参数 | 类型 | 说明 |
|---|---|---|
| id | enum AVCodecID | 请求的解码器 ID,如 AV_CODEC_ID_H264、AV_CODEC_ID_AAC 等 |
③返回值
- 成功:返回一个指向 AVCodec 结构体的指针,表示找到的解码器
- 失败:返回 NULL,表示没有找到匹配的解码器
④使用示例
cpp
const AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_H264);
if (!codec) {
std::cout << "avcodec_find_decoder failed!" << std::endl;
return -1;
}
3. 创建解码器上下文 avcodec_alloc_context3
avcodec_alloc_context3 是 FFmpeg 库中用于分配并初始化编解码器上下文 AVCodecContext 结构体的核心 API。
①函数原型
cpp
AVCodecContext *avcodec_alloc_context3(const AVCodec *codec);
②参数说明
| 参数 | 类型 | 说明 |
|---|---|---|
| codec | const AVCodec* | 指向编解码器结构体的指针,通常通过 avcodec_find_encoder 或 avcodec_find_decoder 获得 |
③返回值
- 成功:非NULL,返回指向已分配的 AVCodecContext 结构体的指针,各字段已设置为默认值
- 失败:返回 NULL,分配失败
④使用示例
cpp
AVCodecContext *codec_ctx = avcodec_alloc_context3(codec);
if (!codec_ctx) {
std::cout << "avcodec_alloc_context3 failed!" << std::endl;
return -1;
}
4. 打开解码器上下文 avcodec_open2
avcodec_open2 是 FFmpeg 库中用于初始化编解码器上下文(AVCodecContext)并将其与指定的编解码器(AVCodec)进行关联的核心 API。
①函数原型
cpp
int avcodec_open2(AVCodecContext *avctx, const AVCodec *codec, AVDictionary **options);
②参数说明
| 参数 | 类型 | 说明 |
|---|---|---|
| avctx | AVCodecContext* | 需要初始化的编解码器上下文,必须已通过 avcodec_alloc_context3() 分配 |
| codec | const AVCodec* | 要打开的编解码器。如果上下文中已经通过 avcodec_alloc_context3() 传入了非 NULL 的编解码器,则此参数必须为 NULL 或与之前传入的编解码器相同 |
| options | AVDictionary** | 指向 AVDictionary 的指针,用于传递编解码器上下文选项和编解码器私有选项,一般传NULL即可 |
③返回值
- 成功:0,编解码器已成功打开并初始化
- 失败:< 0,返回负的错误码(AVERROR)
④使用示例
cpp
int ret = avcodec_open2(codec_ctx, codec, 0);
if (0 != ret) {
std::cout << "avcodec_open2 failed!" << std::endl;
PrintErr(ret);
return -1;
}
5. 初始化音视频码流解析器 av_parser_init
av_parser_init 是 FFmpeg 中用于初始化音视频码流解析器 的 API。它根据指定的编码器 ID(codec_id)创建一个 AVCodecParserContext 上下文,该上下文用于后续的裸流解析操作。
解析器的核心任务 :将输入的裸流 (即没有封装格式的纯编码数据,如 .h264、.hevc、.aac 文件)中的连续字节流分割成一帧一帧的压缩编码数据,并识别帧边界、帧类型(I/P/B 帧)等信息,为解码器提供可直接解码的数据包。
①函数原型
cpp
AVCodecParserContext *av_parser_init(int codec_id);
②参数说明
| 参数 | 类型 | 说明 |
|---|---|---|
| codec_id | int | 编码器 ID,来自 AVCodecID 枚举,如 AV_CODEC_ID_H264、AV_CODEC_ID_HEVC、AV_CODEC_ID_AAC 等 |
③返回值
- 成功:非 NULL,返回指向已初始化的 AVCodecParserContext 结构体的指针
- 失败:NULL,原因可能是:① FFmpeg 未包含该编码格式的解析器;② 内存不足
④使用示例
cpp
AVCodecParserContext *parser_ctx = av_parser_init(codec_id);
if (!parser_ctx) {
std::cout << "av_parser_init failed!" << std::endl;
return -1;
}
6. 初始化SDL部分
不需要显示视频画面可忽略;这部分内容可在网上了解。
cpp
SDL_Init(SDL_INIT_VIDEO);
// 创建窗口
SDL_Window *sdl_win = SDL_CreateWindow("ttttttttttt", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
codec_ctx->width, codec_ctx->height, SDL_WINDOW_OPENGL);
// 创建渲染器
SDL_Renderer *render = SDL_CreateRenderer(sdl_win, -1, SDL_RENDERER_TARGETTEXTURE);
// 创建 纹理|材质
SDL_Texture *texture = SDL_CreateTexture(render, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING,
codec_ctx->width, codec_ctx->height);
// 设置线性插值(抗锯齿)
SDL_SetTextureScaleMode(texture, SDL_ScaleModeLinear);
7. 读取文件数据
使用C++的文件处理,
cpp
unsigned char buf[4096] = { 0 };
unsigned char *data_in = nullptr;
int size_in = 0;
// 读取文件的内容到buf中
ifs.read((char *)buf, sizeof(buf));
size_in = ifs.gcount();
// 读取失败或读取完毕
if (0 >= size_in) {
std::cout << "ifs.read end!" << std::endl;
break;
}
8. 解析音视频裸流数据,从中分离出一帧一帧的压缩编码数据 av_parser_parse2
av_parser_parse2 是 FFmpeg 中用于解析裸流数据 (即没有封装格式的纯编码数据)的核心函数。它从输入的连续字节流中检测帧边界,并输出一帧完整的压缩编码数据(如一个 H.264 NALU 或一个完整的视频帧),同时提供帧类型、时间戳等元信息。
解析器不执行解码,只负责将数据流切割成独立的帧单元,供解码器使用。
①函数原型
cpp
int av_parser_parse2(
AVCodecParserContext *s,
AVCodecContext *avctx,
uint8_t **poutbuf,
int *poutbuf_size,
const uint8_t *buf,
int buf_size,
int64_t pts,
int64_t dts,
int64_t pos
);
②参数说明
| 参数 | 类型 | 说明 |
|---|---|---|
| s | AVCodecParserContext* | 解析器上下文,由 av_parser_init() 初始化 |
| avctx | AVCodecContext* | 解码器上下文,用于提供编码参数(如像素格式、宽高等) |
| poutbuf | uint8_t** | 输出:指向解析出的完整帧数据的指针(指向内部缓冲区,不应释放) |
| poutbuf_size | int* | 输出:解析出的帧数据的大小(字节数)。若为 0,表示未解析出完整帧 |
| buf | const uint8_t* | 输入:待解析的原始数据缓冲区 |
| buf_size | int | 输入:待解析数据的长度(字节)。若为 0,表示已无输入(冲刷) |
| pts | int64_t | 输入数据对应的显示时间戳(PTS),通常设为 AV_NOPTS_VALUE |
| dts | int64_t | 输入数据对应的解码时间戳(DTS),通常设为 AV_NOPTS_VALUE化 |
| pos | int64_t | 输入数据在源文件中的偏移位置,可用于追踪,可设置为0 |
重要说明
- poutbuf 指向的数据由解析器内部管理,调用者无需手动释放,但需注意在下一次调用 av_parser_parse2后,之前的数据可能失效,因此应在当前循环中立即使用。
- buf 可以包含部分帧、完整帧、或多帧,解析器会内部缓冲跨边界的数据。
- 当 buf_size == 0 时,表示数据流结束,解析器会输出内部残留的所有帧(冲刷)。
- pts、dts、pos 一般传入 AV_NOPTS_VALUE 和 0,解析器会将其传递给输出的每一帧。
③返回值
返回值表示从输入 buf 中实际消耗的字节数(即已处理的字节数)。
- 成功:>= 0,返回消耗字节数。调用者应将 buf 指针前移该数量,并减少剩余长度
- 失败:< 0,表示解析失败
④使用示例
通过判断 poutbuf_size > 0 ,说明当前解析出一帧数据,可以进行处理;
cpp
AVPacket *pkt = av_packet_alloc();
data_in = buf;
while (0 < size_in) {
// 8.解析音视频裸流数据,从中分离出一帧一帧的压缩编码数据
int re = av_parser_parse2(parser_ctx, codec_ctx, &pkt->data, &pkt->size,
data_in, size_in, AV_NOPTS_VALUE, AV_NOPTS_VALUE, 0);
data_in += re;
size_in -= re;
if (pkt->size) {
// 处理解析出来的pkt
}
}
- buf不足以解析出一帧数据时,pkt->size为0,内部会记录当前的数据,下一次循环读取,再将新读取到的内容和先前的内容一起解析,还不满足,则循环往复,直到满足,pkt->size > 0
- 假设读取文件的内容为4096,解析一帧数据只用到了3000字节,剩余1096字节,内部依旧会记录下来,下一次循环读取,再将新读取到的内容和先前的内容一起解析,循环往复。
官方给出的例子:
cpp
* Example:
* @code
* while(in_len){
* len = av_parser_parse2(myparser, AVCodecContext, &data, &size,
* in_data, in_len,
* pts, dts, pos);
* in_data += len;
* in_len -= len;
*
* if(size)
* decode_frame(data, size);
* }
* @endcode
9. 发送解码 avcodec_send_packet
avcodec_send_packet 用于将压缩的编码数据包(AVPacket)送入解码器 进行解码。与avcodec_receive_frame配对使用。
①函数原型
cpp
int avcodec_send_packet(AVCodecContext *avctx, const AVPacket *avpkt);
②参数说明
| 参数 | 类型 | 说明 |
|---|---|---|
| avctx | AVCodecContext* | 已通过 avcodec_open2() 打开的解码器上下文 |
| avpkt | const AVPacket* | 包含压缩编码数据的 AVPacket 指针,可以为 NULL |
参数 avpkt 的详细说明
- 正常输入:传入一个包含压缩编码数据的 AVPacket(通常从 av_read_frame 或 av_parser_parse2获得),解码器将对其进行解码。
- 刷新解码器(Flush):传入 NULL,表示没有更多的输入数据包,通知解码器结束解码过程并输出所有缓存的剩余解码帧。
⚠️ 所有权说明:avpkt 的所有权始终属于调用者。解码器不会修改数据包的内容,但可能会在内部引用其数据(如果数据包是引用计数的)。调用者在调用完成后仍需负责调用av_packet_unref() 释放数据包。
③返回值
- 成功:0,数据包已成功送入解码器
- 失败:0 >,失败了
④使用示例
cpp
ret = avcodec_send_packet(codec_ctx, pkt);
if (0 != ret) {
std::cout << "avcodec_send_packet failed! ret = " << ret << std::endl;
PrintErr(ret);
goto FAILED;
}
10. 接收解码 avcodec_receive_frame
avcodec_receive_frame 用于从解码器中读取解码后的原始音视频帧(AVFrame) 。与avcodec_send_packet配对使用。
①函数原型
cpp
int avcodec_receive_frame(AVCodecContext *avctx, AVFrame *frame);
②参数说明
| 参数 | 类型 | 说明 |
|---|---|---|
| avctx | AVCodecContext* | 已通过 avcodec_open2() 打开的解码器上下文 |
| frame | AVFrame* | 输出参数,函数执行成功后会填充为解码后的原始音视频帧 |
参数 frame 的重要说明
- 引用计数:输出的 AVFrame 由解码器分配,是引用计数的(refcounted)。
- 自动解引用:函数在执行业务逻辑前,总是会先调用 av_frame_unref(frame) 清理 frame 中之前可能残留的数据。
- 内存管理:调用者负责在使用完帧后调用av_frame_unref() 释放引用。
⚠️ 所有权说明:输出的 AVFrame 由解码器内部管理,调用者获得的是引用,不应手动释放其内部数据(av_free 等),而应使用 av_frame_unref()。
③返回值
- 成功:0,数据包已成功送入解码器
- 失败:0 >,失败了或当前还未有数据帧
④使用示例
cpp
AVFrame *frame = av_frame_alloc();
while (0 <= ret) {
// 10.接收解码
ret = avcodec_receive_frame(codec_ctx, frame);
if (AVERROR(EAGAIN) == ret || AVERROR_EOF == ret) {
// 当前还没有数据或者已经接收完毕
break;
}
if (0 > ret) {
std::cout << "avcodec_receive_frame failed! ret = " << ret << std::endl;
PrintErr(ret);
goto FAILED;
}
// other code ...
}
到此,frame就是已经被解码出来的一帧视频数据;
11. SDL更新、渲染
不需要显示视频画面可忽略;这部分内容可在网上了解。
cpp
SDL_UpdateYUVTexture(texture, NULL, frame->data[0], frame->linesize[0],
frame->data[1], frame->linesize[1], frame->data[2], frame->linesize[2]);
SDL_RenderClear(render);
SDL_RenderCopy(render, texture, NULL, NULL);
SDL_RenderPresent(render);
12. 冲刷解码器
将缓冲区中未被接收的frame全部读取出来;与正常发送解码,接收解码流程一致;
为什么需要冲刷(Flush)?
解码器内部有一个"延迟队列",如果不主动冲刷,最后的几帧画面会永远卡在队列里,导致视频播放丢失尾部几帧数据。
cpp
// 1. 冲刷解析器(如有)
while (1) {
int re = av_parser_parse2(parser_ctx, codec_ctx, &out_data, &out_size,
NULL, 0, AV_NOPTS_VALUE, AV_NOPTS_VALUE, 0);
if (out_size == 0) break;
pkt->data = out_data;
pkt->size = out_size;
// 发送 pkt 到解码器(可能需处理 EAGAIN)
ret = avcodec_send_packet(codec_ctx, pkt);
if (ret < 0 && ret != AVERROR(EAGAIN)) {
// 错误处理
break;
}
// 若 ret == EAGAIN,则需先接收帧,这里可简单循环处理,但为避免复杂,可忽略(极少发生)
}
// 2. 冲刷解码器
ret = avcodec_send_packet(codec_ctx, NULL);
if (0 != ret) {
std::cout << "avcodec_send_packet failed! ret = " << ret << std::endl;
PrintErr(ret);
goto FAILED;
}
// 3. 接收剩余帧
while (0 <= ret) {
ret = avcodec_receive_frame(codec_ctx, frame);
if (AVERROR(EAGAIN) == ret || AVERROR_EOF == ret) {
break;
}
if (0 > ret) {
std::cout << "avcodec_receive_packet failed! ret = " << ret << std::endl;
PrintErr(ret);
goto FAILED;
}
std::cout << pts++ << "- ";
SDL_UpdateYUVTexture(texture, NULL, frame->data[0], frame->linesize[0],
frame->data[1], frame->linesize[1], frame->data[2], frame->linesize[2]);
SDL_RenderClear(render);
SDL_RenderCopy(render, texture, NULL, NULL);
SDL_RenderPresent(render);
}
13. 内存回收
cpp
ifs.close();
av_frame_free(&frame);
av_packet_free(&pkt);
avcodec_free_context(&codec_ctx);
av_parser_close(parser_ctx);
if (texture) {
SDL_DestroyTexture(texture);
}
if (render) {
SDL_DestroyRenderer(render);
}
if (sdl_win) {
SDL_DestroyWindow(sdl_win);
}
SDL_Quit();
代码合集
cpp
#include <iostream>
#include <fstream>
#include <thread>
#include <chrono>
extern "C" {
#include "libavcodec/avcodec.h"
#include "libavutil/avutil.h"
}
#include "SDL.h"
// 打印错误信息
static void PrintErr(int ret) {
char buf[1024] = { 0 };
av_strerror(ret, buf, sizeof(buf));
std::cout << buf << std::endl;
}
#undef main
int main(void)
{
std::cout << av_version_info() << std::endl;
// 1.打开视频文件
std::ifstream ifs;
ifs.open("800_600_25.h264", std::ios::binary);
if (!ifs.is_open()) {
std::cout << "ifs open file failed!\n";
return -1;
}
AVCodecID codec_id = AV_CODEC_ID_H264;
// 2.查找解码器
const AVCodec *codec = avcodec_find_decoder(codec_id);
if (!codec) {
std::cout << "avcodec_find_decoder failed!" << std::endl;
return -1;
}
// 3.创建解码器上下文
AVCodecContext *codec_ctx = avcodec_alloc_context3(codec);
if (!codec_ctx) {
std::cout << "avcodec_alloc_context3 failed!" << std::endl;
return -1;
}
codec_ctx->time_base = {1, 25};
codec_ctx->width = 800;
codec_ctx->height = 600;
// 4.打开解码器上下文
int ret = avcodec_open2(codec_ctx, codec, 0);
if (0 != ret) {
std::cout << "avcodec_open2 failed!" << std::endl;
PrintErr(ret);
return -1;
}
// 5.初始化音视频码流解析器
AVCodecParserContext *parser_ctx = av_parser_init(codec_id);
if (!parser_ctx) {
std::cout << "av_parser_init failed!" << std::endl;
return -1;
}
// 6.初始化SDL部分
SDL_Init(SDL_INIT_VIDEO);
// 创建窗口
SDL_Window *sdl_win = SDL_CreateWindow("ttttttttttt", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
codec_ctx->width, codec_ctx->height, SDL_WINDOW_OPENGL);
// 创建渲染器
SDL_Renderer *render = SDL_CreateRenderer(sdl_win, -1, SDL_RENDERER_TARGETTEXTURE);
// 创建 纹理|材质
SDL_Texture *texture = SDL_CreateTexture(render, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING,
codec_ctx->width, codec_ctx->height);
// 设置线性插值(抗锯齿)
SDL_SetTextureScaleMode(texture, SDL_ScaleModeLinear);
int pts = 1;
unsigned char buf[4096] = { 0 };
unsigned char *data_in = nullptr;
int size_in = 0;
AVPacket *pkt = av_packet_alloc();
AVFrame *frame = av_frame_alloc();
while (1) {
// 7.读取文件数据
ifs.read((char *)buf, sizeof(buf));
size_in = ifs.gcount();
// 读取失败或读取完毕
if (0 >= size_in) {
std::cout << "ifs.read end!" << std::endl;
break;
}
data_in = buf;
while (0 < size_in) {
// 8.解析音视频裸流数据,从中分离出一帧一帧的压缩编码数据
int re = av_parser_parse2(parser_ctx, codec_ctx, &pkt->data, &pkt->size,
data_in, size_in, AV_NOPTS_VALUE, AV_NOPTS_VALUE, 0);
data_in += re;
size_in -= re;
if (pkt->size) {
// 9.发送解码
ret = avcodec_send_packet(codec_ctx, pkt);
if (0 != ret) {
std::cout << "avcodec_send_packet failed! ret = " << ret << std::endl;
PrintErr(ret);
goto FAILED;
}
while (0 <= ret) {
// 10.接收解码
ret = avcodec_receive_frame(codec_ctx, frame);
if (AVERROR(EAGAIN) == ret || AVERROR_EOF == ret) {
break;
}
if (0 > ret) {
std::cout << "avcodec_receive_frame failed! ret = " << ret << std::endl;
PrintErr(ret);
goto FAILED;
}
std::cout << pts++ << ", ";
// 11.SDL更新、渲染
SDL_UpdateYUVTexture(texture, NULL, frame->data[0], frame->linesize[0],
frame->data[1], frame->linesize[1], frame->data[2], frame->linesize[2]);
SDL_RenderClear(render);
SDL_RenderCopy(render, texture, NULL, NULL);
SDL_RenderPresent(render);
// 睡眠 40 毫秒
// std::this_thread::sleep_for(std::chrono::milliseconds(40));
}
}
}
}
// 12.冲刷解码器
ret = avcodec_send_packet(codec_ctx, NULL);
if (0 != ret) {
std::cout << "avcodec_send_packet failed! ret = " << ret << std::endl;
PrintErr(ret);
goto FAILED;
}
while (0 <= ret) {
ret = avcodec_receive_frame(codec_ctx, frame);
if (AVERROR(EAGAIN) == ret || AVERROR_EOF == ret) {
break;
}
if (0 > ret) {
std::cout << "avcodec_receive_packet failed! ret = " << ret << std::endl;
PrintErr(ret);
goto FAILED;
}
std::cout << pts++ << "- ";
SDL_UpdateYUVTexture(texture, NULL, frame->data[0], frame->linesize[0],
frame->data[1], frame->linesize[1], frame->data[2], frame->linesize[2]);
SDL_RenderClear(render);
SDL_RenderCopy(render, texture, NULL, NULL);
SDL_RenderPresent(render);
}
FAILED:
// 13.内存回收
ifs.close();
av_frame_free(&frame);
av_packet_free(&pkt);
avcodec_free_context(&codec_ctx);
av_parser_close(parser_ctx);
if (texture) {
SDL_DestroyTexture(texture);
}
if (render) {
SDL_DestroyRenderer(render);
}
if (sdl_win) {
SDL_DestroyWindow(sdl_win);
}
SDL_Quit();
return 0;
}
视频效果截图
之前生成的视频是两千帧,这里读取出来处理的也差不多少是两千帧左右;
