[TOC]
开始前的BB
之前我们都是拿ffplay播放视频,做为一个专业的开发人员,会用就够了么?
image.png
本章,我们就来进行(莞式)(分离-解码-显示)一条龙。
这章的这里就得简单介绍一下SDL2了,
SDL 是一个跨平台的媒体开发库 用C写的(pygame就是包装的它),主要功能包括,图像显示、音频播放、线程控制、事件处理、定时器、字节序无关(大小端)
SDL2就是SDL1的升级版本,变了很多API(没有错,我解释的就是这么通俗)
SDL2我们可以直接自己编译一下 下载地址
选择
下载源码,解压之后通过终端进入,大概是这样
image.png
然后我们就开始输入命令编译
./configure --disable-libsamplerate --disable-libudev --disable-dbus --disable-ime --disable-ibus --disable-fcitx
make -j8
make install
完事之后我们把include
这个目录直接拷贝到我们项目的include/SDL2
里
在/usr/local/lib/
目录找到libSDL2-2.0.0.dylib
,复制到librarys里
然后在Cmake文件中
image.png
把SDL2加进来,就准备开始愉快的玩耍了
在src中新建chapter_06/sdl_video.h
,撸码开始
SDL2 播放解码后的视频
整体先浏览一下调用方法以及顺序
/** 1.初始化SDL2 **/
void initSDL2();
/** 2.初始化FFmpeg **/
void preparDecodec(const char *url);
/** 3.解码播放 **/
void decodecFrame();
/** 4.释放资源 **/
void freeContext();
/** 3.1 绘制一帧数据 在 decodecFrame() 中调用 **/
void drawFrame(AVFrame *frame);
/** 播放视频 (外部调用的总方法)**/
void playVideo(const char *url);
初始化SDL2
首先我们把SDL2初始化 新建方法initSDL2()
#define WINDOW_WIDTH 1080
#define WINDOW_HEIGHT 720
/** ########## SDL2 相关 ############# **/
SDL_Window *window;
SDL_Renderer *render;
SDL_Texture *texture;
SDL_Rect rect;
/**
* 初始化SDL2
*/
void initSDL2() {
//初始化SDL2
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS | SDL_INIT_TIMER)) {
cout << "[error] SDL Init error!" << endl;
return;
}
//创建Window
window = SDL_CreateWindow("LearnFFmpeg", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, WINDOW_WIDTH,
WINDOW_HEIGHT, SDL_WINDOW_OPENGL);
if (!window) {
cout << "[error] SDL CreateWindow error!" << endl;
return;
}
//创建Render
render = SDL_CreateRenderer(window, -1, 0);
//创建Texture
texture = SDL_CreateTexture(render, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING, WINDOW_WIDTH, WINDOW_HEIGHT);
rect.x = 0;
rect.y = 0;
rect.w = WINDOW_WIDTH;
rect.h = WINDOW_HEIGHT;
}
FFmpeg 解复用+解码
初始好窗口之后,我们来初始化ffmpeg相关的变量以及参数
/** ########### FFmpeg 相关 ############# **/
AVFormatContext *formatContext;
AVCodecContext *codecContext;
AVCodec *codec;
AVPacket *packet;
AVFrame *frame;
int videoIndex = -1;
/** 初始化FFmpeg **/
void preparDecodec(const char *url) {
int retcode;
//初始化FormatContext
formatContext = avformat_alloc_context();
if (!formatContext) {
cout << "[error] alloc format context error!" << endl;
return;
}
//打开输入流
retcode = avformat_open_input(&formatContext, url, nullptr, nullptr);
if (retcode != 0) {
cout << "[error] open input error!" << endl;
return;
}
//读取媒体文件信息
retcode = avformat_find_stream_info(formatContext, NULL);
if (retcode != 0) {
cout << "[error] find stream error!" << endl;
return;
}
//分配codecContext
codecContext = avcodec_alloc_context3(NULL);
if (!codecContext) {
cout << "[error] alloc codec context error!" << endl;
return;
}
//寻找到视频流的下标
videoIndex = av_find_best_stream(formatContext, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
//将视频流的的编解码信息拷贝到codecContext中
retcode = avcodec_parameters_to_context(codecContext, formatContext->streams[videoIndex]->codecpar);
if (retcode != 0) {
cout << "[error] parameters to context error!" << endl;
return;
}
//查找解码器
codec = avcodec_find_decoder(codecContext->codec_id);
if (codec == nullptr) {
cout << "[error] find decoder error!" << endl;
return;
}
//打开解码器
retcode = avcodec_open2(codecContext, codec, nullptr);
if (retcode != 0) {
cout << "[error] open decodec error!" << endl;
return;
}
//初始化一个packet
packet = av_packet_alloc();
//初始化一个Frame
frame = av_frame_alloc();
}
初始化好之后就可以进行解码
/** 解码数据 **/
void decodecFrame() {
int sendcode = 0;
//读取包
while (av_read_frame(formatContext, packet) == 0) {
if (packet->stream_index != videoIndex)continue;
//接受解码后的帧数据
while (avcodec_receive_frame(codecContext, frame) == 0) {
//绘制图像
drawFrame(frame);
}
//发送解码前的包数据
sendcode = avcodec_send_packet(codecContext, packet);
//根据发送的返回值判断状态
if (sendcode == 0) {
cout << "[debug] " << "SUCCESS" << endl;
} else if (sendcode == AVERROR_EOF) {
cout << "[debug] " << "EOF" << endl;
} else if (sendcode == AVERROR(EAGAIN)) {
cout << "[debug] " << "EAGAIN" << endl;
} else {
cout << "[debug] " << av_err2str(AVERROR(sendcode)) << endl;
}
}
}
这边我发现网上的教程都没有说avcodec_send_packet
和avcodec_receive_frame
返回值是什么意思,这边我来解释一部分
0
读取成功
AVERROR_EOF
已经读取到最后 流结束的标志
AVERROR(EAGAIN)
当前发送/接受队里已满/已空,需要调用对应的recive
/send
接受到AVFrame
数据后调用drawFrame()
进行绘制
SDL2显示一帧画面
/** 绘制一帧数据 **/
void drawFrame(AVFrame *frame) {
if (frame == nullptr)return;
//上传YUV到Texture
SDL_UpdateYUVTexture(texture, &rect,
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, &rect);
SDL_RenderPresent(render);
}
最后记得释放资源
/** 释放资源 **/
void freeContext() {
if (formatContext != nullptr) avformat_close_input(&formatContext);
if (codecContext != nullptr) avcodec_free_context(&codecContext);
if (packet != nullptr) av_packet_free(&packet);
if (frame != nullptr) av_frame_free(&frame);
}
整合步骤
我们来把这几个方法组装一下,方便外部调用
/** 播放视频 **/
void playVideo(const char *url) {
initSDL2();
preparDecodec(url);
decodecFrame();
freeContext();
}
我们在main
方法中调用
const char *url = "../video/test_video.mp4";
playVideo(url);
image.png
喏,就显示出来了
视频自同步
是不是有些同学看的显示的非常快,没有错,因为他没有进行同步的操作,我们可以来个简单的同步操作
- 根据视频的帧率进行同步
我们都知道帧率是描述了视频图像连续出现在显示器上的频率,他的局限是有些帧之间的PTS差别较大/小的时候这种方式仍然会按照每个帧固定停留的时间进行显示,无法动态变化,通过下面的公式计算出平均每帧显示的时间(s)
s = 1/fps
所以我们可以新建一个变量double displayTimeUs = 0;
,decodecFrame()
可以改为
/** 解码数据 **/
void decodecFrame() {
int sendcode = 0;
//计算帧率
double frameRate = av_q2d(formatContext->streams[videoIndex]->avg_frame_rate);
//计算显示的时间
displayTimeUs = 1*1000/frameRate;
//读取包
while (av_read_frame(formatContext, packet) == 0) {
if (packet->stream_index != videoIndex)continue;
//接受解码后的帧数据
while (avcodec_receive_frame(codecContext, frame) == 0) {
//绘制图像
drawFrame(frame);
}
//发送解码前的包数据
sendcode = avcodec_send_packet(codecContext, packet);
//根据发送的返回值判断状态
if (sendcode == 0) {
cout << "[debug] " << "SUCCESS" << endl;
} else if (sendcode == AVERROR_EOF) {
cout << "[debug] " << "EOF" << endl;
} else if (sendcode == AVERROR(EAGAIN)) {
cout << "[debug] " << "EAGAIN" << endl;
} else {
cout << "[debug] " << av_err2str(AVERROR(sendcode)) << endl;
}
}
}
在drawFrame()
中新增一行代码SDL_Delay(displayTimeUs);
/** 绘制一帧数据 **/
void drawFrame(AVFrame *frame) {
if (frame == nullptr)return;
//上传YUV到Texture
SDL_UpdateYUVTexture(texture, &rect,
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, &rect);
SDL_RenderPresent(render);
SDL_Delay(displayTimeUs);
}
然后点击启动
启动
然后就会发现播放起来已经是正常了
未完持续。。。
网友评论