FFmpeg开发笔记(六):ffmpeg解码视频并使用SDL同步时间显示播放
若该文为原创文章,未经允许不得转载
原博主博客地址:https://blog.csdn.net/qq21497936
原博主博客导航:https://blog.csdn.net/qq21497936/article/details/102478062
本文章博客地址:https://blog.csdn.net/qq21497936/article/details/108648385
各位读者,知识无穷而人力有穷,要么改需求,要么找专业人士,要么自己研究
上一篇:《FFmpeg开发笔记(五):ffmpeg解码的基本流程详解(ffmpeg3新解码api)》
下一篇:敬请期待
前言
ffmpeg解码之后,显示需要同步,一是需要显示,本篇使用SDL进行显示,二是需要对时间戳进行同步。
FFmpeg解码
FFmpeg解码的基本流程请参照:
《FFmpeg开发笔记(四):ffmpeg解码的基本流程详解》
《FFmpeg开发笔记(五):ffmpeg解码的基本流程详解(ffmpeg3新解码api)》
SDL显示
SDL显示图片的基本流程请参照:
《SD开发笔记(三):使用SDL渲染窗口颜色和图片》
ffmpeg同步
ffmpeg同步包含音频、视频、字幕等等,此处描述的同步是时间与视频显示的同步。
基本流程

同步关键点
计算帧率,拿到信息后,用流上下文获取总时间、总帧数,这样计算出来的时间间隔是最准确的,也可以使用时间基数去计算,也准确只是复杂点。
视频与时间的同步,比如25fps,那么40ms显示一帧,那么计算好时间间隔,在下一帧的时间节点之前先挂起直到到达该帧显示的时间节点再显示。
基于消息的就更好处理,计算下一帧的时间间隔,直接定时为这个时间点后进行显示。
此处还可能涉及到跳帧处理,如系统卡顿以下,解码时间间隔较大,如果按照上一帧往后加40ms计算下一帧,则会导致第一针为0ms,第二针卡顿为50ms,第三针为90ms,那么导致整体时长加长。
综合以上,ffmpeg在做同步显示的时候,需要选取最开始播放的时间作为基准,用帧序号和帧间隔去计算下一帧的显示时间。
ffmpeg同步相关结构体详解
AVStream
流信息的结构体,里面包含了AVCodecContext的实例指针
struct AVStream {
AVCodecContext *codec; // 编码器相关的信息
AVRational avg_frame_rate; // 根据该参数,可以把PTS转化为实际的时间(单位为秒s)
int64_t duration; // 视频时长,单位为10ms,不是ms,所以要除以10000
int64_t nb_frames; // 视频总帧数
}
AVCodecContext
该结构体是编码上下文信息,对文件流进行探测后,就能得到文件流的具体相关信息了,关于编解码的相关信息就在此文件结构中。
与同步视频显示相关的变量在此详解一下,其他的可以自行去看ffmpeg源码上对于结构体AVCodecContext的定义。
struct AVCodecContext {
AVMediaType codec_type; // 编码器的类型,如视频、音频、字幕等等
AVCodec *codec; // 使用的编码器
int bit_rata; // 平均比特率
AVRational time_base: // 根据该参数,可以把PTS转化为实际的时间(单位为秒s)
int width, height: // 如果是视频的话,代表宽和高
enum AVPixelFormat pix_fmt; // 代表视频像素的格式...
} AVCodecContext;


Demo源码
void FFmpegManager::testDecodeSyncShow()
{
// QString fileName = "test/1.avi";
QString fileName = "test/1.mp4";
// SDL相关变量预先定义
SDL_Window *pSDLWindow = 0;
SDL_Renderer *pSDLRenderer = 0;
SDL_Surface *pSDLSurface = 0;
SDL_Texture *pSDLTexture = 0;
SDL_Event event;
qint64 startTime = 0; // 记录播放开始
int currentFrame = 0; // 当前帧序号
double fps = 0; // 帧率
double interval = 0; // 帧间隔
// ffmpeg相关变量预先定义与分配
AVFormatContext *pAVFormatContext = 0; // ffmpeg的全局上下文,所有ffmpeg操作都需要
AVStream *pAVStream = 0; // ffmpeg流信息
AVCodecContext *pAVCodecContext = 0; // ffmpeg编码上下文
AVCodec *pAVCodec = 0; // ffmpeg编码器
AVPacket *pAVPacket = 0; // ffmpag单帧数据包
AVFrame *pAVFrame = 0; // ffmpeg单帧缓存
AVFrame *pAVFrameRGB32 = 0; // ffmpeg单帧缓存转换颜色空间后的缓存
struct SwsContext *pSwsContext = 0; // ffmpag编码数据格式转换
int ret = 0; // 函数执行结果
int videoIndex = -1; // 音频流所在的序号
int numBytes = 0; // 解码后的数据长度
uchar *outBuffer = 0; // 解码后的数据存放缓存区
pAVFormatContext = avformat_alloc_context(); // 分配
pAVPacket = av_packet_alloc(); // 分配
pAVFrame = av_frame_alloc(); // 分配
pAVFrameRGB32 = av_frame_alloc(); // 分配
if(!pAVFormatContext || !pAVPacket || !pAVFrame || !pAVFrameRGB32)
{
LOG << "Failed to alloc";
goto END;
}
// 步骤一:注册所有容器和编解码器(也可以只注册一类,如注册容器、注册编码器等)
av_register_all();
// 步骤二:打开文件(ffmpeg成功则返回0)
LOG << "文件:" << fileName << ",是否存在:" << QFile::exists(fileName);
ret = avformat_open_input(&pAVFormatContext, fileName.toUtf8().data(), 0, 0);
if(ret)
{
LOG << "Failed";
goto END;
}
// 步骤三:探测流媒体信息
ret = avformat_find_stream_info(pAVFormatContext, 0);
if(ret < 0)
{
LOG << "Failed to avformat_find_stream_info(pAVFormatContext, 0)";
goto END;
}
// 步骤四:提取流信息,提取视频信息
for(int index = 0; index < pAVFormatContext->nb_streams; index++)
{
pAVCodecContext = pAVFormatContext->streams[index]->codec;
pAVStream = pAVFormatContext->streams[index];
switch (pAVCodecContext->codec_type)
{
case AVMEDIA_TYPE_UNKNOWN:
LOG << "流序号:" << index << "类型为:" << "AVMEDIA_TYPE_UNKNOWN";
break;
case AVMEDIA_TYPE_VIDEO:
LOG << "流序号:" << index << "类型为:" << "AVMEDIA_TYPE_VIDEO";
videoIndex = index;
LOG;
break;
case AVMEDIA_TYPE_AUDIO:
LOG << "流序号:" << index << "类型为:" << "AVMEDIA_TYPE_AUDIO";
break;
case AVMEDIA_TYPE_DATA:
LOG << "流序号:" << index << "类型为:" << "AVMEDIA_TYPE_DATA";
break;
case AVMEDIA_TYPE_SUBTITLE:
LOG << "流序号:" << index << "类型为:" << "AVMEDIA_TYPE_SUBTITLE";
break;
case AVMEDIA_TYPE_ATTACHMENT:
LOG << "流序号:" << index << "类型为:" << "AVMEDIA_TYPE_ATTACHMENT";
break;
case AVMEDIA_TYPE_NB:
LOG << "流序号:" << index << "类型为:" << "AVMEDIA_TYPE_NB";
break;
default:
break;
}
// 已经找打视频品流
if(videoIndex != -1)
{
break;
}
}
if(videoIndex == -1 || !pAVCodecContext)
{
LOG << "Failed to find video stream";
goto END;
}
// 步骤五:对找到的视频流寻解码器
pAVCodec = avcodec_find_decoder(pAVCodecContext->codec_id);
if(!pAVCodec)
{
LOG << "Fialed to avcodec_find_decoder(pAVCodecContext->codec_id):"
<< pAVCodecContext->codec_id;
goto END;
}
// 步骤六:打开解码器
ret = avcodec_open2(pAVCodecContext, pAVCodec, NULL);
if(ret)
{
LOG << "Failed to avcodec_open2(pAVCodecContext, pAVCodec, pAVDictionary)";
goto END;
}
// 显示视频相关的参数信息(编码上下文)
LOG << "比特率:" << pAVCodecContext->bit_rate;
LOG << "宽高:" << pAVCodecContext->width << "x" << pAVCodecContext->height;
LOG << "格式:" << pAVCodecContext->pix_fmt;
LOG << "帧率分母:" << pAVCodecContext->time_base.den;
LOG << "帧率分子:" << pAVCodecContext->time_base.num;
LOG << "帧率分母:" << pAVStream->avg_frame_rate.den;
LOG << "帧率分子:" << pAVStream->avg_frame_rate.num;
LOG << "总时长:" << pAVStream->duration / 10000.0 << "s";
LOG << "总帧数:" << pAVStream->nb_frames;
fps = pAVStream->nb_frames / (pAVStream->duration / 10000.0);
LOG << "平均帧率:" << fps;
interval = pAVStream->duration / 10.0 / pAVStream->nb_frames;
LOG << "帧间隔:" << interval << "ms";
// 步骤七:对拿到的原始数据格式进行缩放转换为指定的格式高宽大小
pSwsContext = sws_getContext(pAVCodecContext->width,
pAVCodecContext->height,
pAVCodecContext->pix_fmt,
pAVCodecContext->width,
pAVCodecContext->height,
AV_PIX_FMT_RGBA,
SWS_FAST_BILINEAR,
0,
0,
0);
numBytes = avpicture_get_size(AV_PIX_FMT_RGBA,
pAVCodecContext->width,
pAVCodecContext->height);
outBuffer = (uchar *)av_malloc(numBytes);
// pAVFrame32的data指针指向了outBuffer
avpicture_fill((AVPicture *)pAVFrameRGB32,
outBuffer,
AV_PIX_FMT_RGBA,
pAVCodecContext->width,
pAVCodecContext->height);
ret = SDL_Init(SDL_INIT_VIDEO);
if(ret)
{
LOG << "Failed";
goto END;
}
pSDLWindow = SDL_CreateWindow(fileName.toUtf8().data(),
0,
0,
pAVCodecContext->width,
pAVCodecContext->height,
SDL_WINDOW_ALWAYS_ON_TOP | SDL_WINDOW_OPENGL);
if(!pSDLWindow)
{
LOG << "Failed";
goto END;
}
pSDLRenderer = SDL_CreateRenderer(pSDLWindow, -1, 0);
if(!pSDLRenderer)
{
LOG << "Failed";
goto END;
}
startTime = QDateTime::currentDateTime().toMSecsSinceEpoch();
currentFrame = 0;
// 步骤八:读取一帧数据的数据包
while(av_read_frame(pAVFormatContext, pAVPacket) >= 0)
{
if(pAVPacket->stream_index == videoIndex)
{
// 步骤八:对读取的数据包进行解码
ret = avcodec_send_packet(pAVCodecContext, pAVPacket);
if(ret)
{
LOG << "Failed to avcodec_send_packet(pAVCodecContext, pAVPacket) ,ret =" << ret;
break;
}
while(!avcodec_receive_frame(pAVCodecContext, pAVFrame))
{
sws_scale(pSwsContext,
(const uint8_t * const *)pAVFrame->data,
pAVFrame->linesize,
0,
pAVCodecContext->height,
pAVFrameRGB32->data,
pAVFrameRGB32->linesize);
// 格式为RGBA=8:8:8:8”
// rmask 应为 0xFF000000 但是颜色不对 改为 0x000000FF 对了
// gmask 0x00FF0000 0x0000FF00
// bmask 0x0000FF00 0x00FF0000
// amask 0x000000FF 0xFF000000
// 测试了ARGB,也是相反的,而QImage是可以正确加载的
// 暂时只能说这个地方标记下,可能有什么设置不对什么的
pSDLSurface = SDL_CreateRGBSurfaceFrom(outBuffer,
pAVCodecContext->width,
pAVCodecContext->height,
4 * 8,
pAVCodecContext->width * 4,
0x000000FF,
0x0000FF00,
0x00FF0000,
0xFF000000
);
pSDLTexture = SDL_CreateTextureFromSurface(pSDLRenderer, pSDLSurface);
SDL_FreeSurface(pSDLSurface);
// pSDLSurface = SDL_LoadBMP("testBMP/1.bmp");
// pSDLTexture = SDL_CreateTextureFromSurface(pSDLRenderer, pSDLSurface);
// 清除Renderer
SDL_RenderClear(pSDLRenderer);
// Texture复制到Renderer
SDL_RenderCopy(pSDLRenderer, pSDLTexture, 0, 0);
// 更新Renderer显示
SDL_RenderPresent(pSDLRenderer);
// 事件处理
SDL_PollEvent(&event);
}
// 下一帧
currentFrame++;
while(QDateTime::currentDateTime().toMSecsSinceEpoch() - startTime < currentFrame * interval)
{
SDL_Delay(1);
}
LOG << "current:" << currentFrame <<"," << time << (QDateTime::currentDateTime().toMSecsSinceEpoch() - startTime);
}
}
END:
LOG << "释放回收资源";
if(outBuffer)
{
av_free(outBuffer);
outBuffer = 0;
}
if(pSwsContext)
{
sws_freeContext(pSwsContext);
pSwsContext = 0;
LOG << "sws_freeContext(pSwsContext)";
}
if(pAVFrameRGB32)
{
av_frame_free(&pAVFrameRGB32);
pAVFrame = 0;
LOG << "av_frame_free(pAVFrameRGB888)";
}
if(pAVFrame)
{
av_frame_free(&pAVFrame);
pAVFrame = 0;
LOG << "av_frame_free(pAVFrame)";
}
if(pAVPacket)
{
av_free_packet(pAVPacket);
pAVPacket = 0;
LOG << "av_free_packet(pAVPacket)";
}
if(pAVCodecContext)
{
avcodec_close(pAVCodecContext);
pAVCodecContext = 0;
LOG << "avcodec_close(pAVCodecContext);";
}
if(pAVFormatContext)
{
avformat_close_input(&pAVFormatContext);
avformat_free_context(pAVFormatContext);
pAVFormatContext = 0;
LOG << "avformat_free_context(pAVFormatContext)";
}
// 步骤五:销毁渲染器
SDL_DestroyRenderer(pSDLRenderer);
// 步骤六:销毁窗口
SDL_DestroyWindow(pSDLWindow);
// 步骤七:退出SDL
SDL_Quit();
}
工程模板v1.2.0
对应工程模板v1.2.0:增加解码视频并使用SDL显示Demo。
上一篇:《FFmpeg开发笔记(五):ffmpeg解码的基本流程详解(ffmpeg3新解码api)》
下一篇:敬请期待
原博主博客地址:https://blog.csdn.net/qq21497936
原博主博客导航:https://blog.csdn.net/qq21497936/article/details/102478062
本文章博客地址:https://blog.csdn.net/qq21497936/article/details/108648385
FFmpeg开发笔记(六):ffmpeg解码视频并使用SDL同步时间显示播放的更多相关文章
- FFmpeg开发笔记(四):ffmpeg解码的基本流程详解
若该文为原创文章,未经允许不得转载原博主博客地址:https://blog.csdn.net/qq21497936原博主博客导航:https://blog.csdn.net/qq21497936/ar ...
- FFmpeg开发笔记(五):ffmpeg解码的基本流程详解(ffmpeg3新解码api)
若该文为原创文章,未经允许不得转载原博主博客地址:https://blog.csdn.net/qq21497936原博主博客导航:https://blog.csdn.net/qq21497936/ar ...
- FFmpeg开发笔记(九):ffmpeg解码rtsp流并使用SDL同步播放
前言 ffmpeg播放rtsp网络流和摄像头流. Demo 使用ffmpeg播放局域网rtsp1080p海康摄像头:延迟0.2s,存在马赛克 使用ffmpeg播放网络rtsp文件流 ...
- FFmpeg开发笔记(三):ffmpeg介绍、windows编译以及开发环境搭建
前言 本篇章是对之前windows环境的补充,之前windows的是无需进行编译的,此篇使用源码进行编译,版本就使用3.4.8. FFmpeg简介 FFmpeg是领先的多媒体框架,能够解码 ...
- FFmpeg开发笔记(十):ffmpeg在ubuntu上的交叉编译移植到海思HI35xx平台
FFmpeg和SDL开发专栏(点击传送门) 上一篇:<FFmpeg开发笔记(九):ffmpeg解码rtsp流并使用SDL同步播放>下一篇:敬请期待 前言 将ffmpeg移植到海思H ...
- Django开发笔记六
Django开发笔记一 Django开发笔记二 Django开发笔记三 Django开发笔记四 Django开发笔记五 Django开发笔记六 1.登录功能完善 登录成功应该是重定向到首页,而不是转发 ...
- FFmpeg开发笔记(一)搭建Linux系统的开发环境
对于初学者来说,如何搭建FFmpeg的开发环境是个不小的拦路虎,因为FFmpeg用到了许多第三方开发包,所以要先编译这些第三方源码,之后才能给FFmpeg集成编译好的第三方库.不过考虑到刚开始仅仅调用 ...
- ffmpeg学习笔记-初识ffmpeg
ffmpeg用来对音视频进行处理,那么在使用ffmpeg前就需要ffmpeg有一个大概的了解,这里使用雷神的ppt素材进行整理,以便于复习 音视频基础知识 视频播放器的原理 播放视频的流程大致如下: ...
- FFmpeg开发笔记(二)搭建Windows系统的开发环境
由于Linux系统比较专业,个人电脑很少安装Linux,反而大都安装Windows系统,因此提高了FFmpeg的学习门槛,毕竟在Windows系统搭建FFmpeg的开发环境还是比较麻烦的.不过若有已经 ...
- netty权威指南学习笔记六——编解码技术之MessagePack
编解码技术主要应用在网络传输中,将对象比如BOJO进行编解码以利于网络中进行传输.平常我们也会将编解码说成是序列化/反序列化 定义:当进行远程跨进程服务调用时,需要把被传输的java对象编码为字节数组 ...
随机推荐
- [转帖]《Linux性能优化实战》笔记(一)—— 平均负载
最近在看极客时间的<Linux性能优化实战>课程,记录下学习内容. 一. 平均负载(Load Average) 1. 概念 我们都知道uptime命令的最后三列分别是过去 1 分钟.5 分 ...
- SQLServer Core 序列号使用CPU限制的处理
SQLServer Core 序列号使用CPU限制的处理 背景 有客户是SQLSERVER的数据库. 说要进行一下压测. 这边趁着最后进行一下环境的基础搭建工作. 然后在全闪的环境上面搭建了一个Win ...
- [转帖]windows使用net user add用户并加入管理员,从而支持rdp远程登陆访问
C:\phpstudy_pro\WWW> net user test2 /add 命令成功完成. C:\phpstudy_pro\WWW> net user test2 Huawei ...
- vue3新特性teleport传送原来这么神奇
我对teleport的理解 teleport有传送的意思,读音[te li po t][嘻嘻],看官们应该知道读啥子了吧 它可以将你写的代码传送到某一个地方 传送到哪一个地方呢? 传送到你标记的地方, ...
- el-popover 点击取消按钮,弹窗仍然无法关闭
<el-popover placement="bottom" width="200" :ref="aa" :visible.sync= ...
- DotLiquid(.net模版引擎)
可用生成C#代码,在KSFramework中有使用:https://github.com/mr-kelly/KSFramework 主页:http://dotliquidmarkup.org/ 文档: ...
- 窗口管理器 dwm安装
上一篇博文中,已经完成了archlinux的安装,但是进去仅仅是一个冰冷冷的交互式命令窗口.没有图像,也无法打开浏览器.离日常使用还差的很远,接下来首先需要做的就是安装桌面环境.这里我不打算使用诸如g ...
- ROS节点通信(一)消息发布和订阅
目录 1.说明 2.创建工作空间 3.创建功能包 4.编写自定义传输类型文件 5.编写源代码 5.1.编写发布者代码 5.2.编写订阅者代码 6.编译 7.启动运行 8.查看ROS网络结构图 1.说明 ...
- CH58x/CH57x硬件SPI操作外部flash学习记录
官方提供的58x的spi例程,spi主机模式下的发送方式有三种单字节发送,FIFO连续发送,DMA连续发送.本文分别对SPI0主机模式下三种发送模式进行使用. 本次使用的是CH582m做为主机,W25 ...
- electron useContentSize的详解
useContentSize作用就是 由于window窗体有边框和title区域menu等,该区域不能显示自己的html页面(new BrowserWindow 时设置frame=false禁用边框 ...