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对象编码为字节数组 ...
随机推荐
- [转帖]自动化配置SSH免密登录和取消SSH免密配置脚本
1. 前文 搭建了一套有多台主机的局域网环境,不完全考虑安全性的情况下,为方便管理局域网内主机,配置SSH免密登录,因主机较多,前阵子针对配置ssh免密和取消ssh免密功能单独写了脚本来自动化批量部署 ...
- [转帖]Jmeter学习笔记(九)——响应断言
Jmeter学习笔记(九)--响应断言 https://www.cnblogs.com/pachongshangdexuebi/p/11571348.html Jmeter中又一个元件叫断言,用于检查 ...
- [转帖]kafka漏洞升级记录,基于SASL JAAS 配置和 SASL 协议,涉及版本3.4以下
攻击者可以使用基于 SASL JAAS 配置和 SASL 协议的任意 Kafka 客户端,在对 Kafka Connect worker 创建或修改连接器时,通过构造特殊的配置,进行 JNDI 注入. ...
- [转帖]@Autowired 和 @Resource 的区别
@Autowired 和 @Resource 的区别 默认注入方式不同 @Autowired 默认的注入方式为byType(根据类型进行匹配),也就是说会优先根据接口类型去匹配并注入 Bean (接口 ...
- [转帖]Elasticsearch 技术分析(五):如何通过SQL查询Elasticsearch
https://www.cnblogs.com/jajian/p/10053504.html 前言# 这篇博文本来是想放在全系列的大概第五.六篇的时候再讲的,毕竟查询是在索引创建.索引文档数据生成和一 ...
- awk的简单样例
shell awk求和 当第一列相同时,对应的第二列相加 awk'{sum[$1]+=$2}END{for(c in sum){print c,sum[c]}}'输入文件名 在Shell中,我们可以用 ...
- grafana与K8S的下载地址
https://grafana.com/grafana/download/9.1.6 https://dl.k8s.io/v1.15.1/kubernetes-node-linux-amd64.tar ...
- postman数据驱动(.csv文件)
做api测试的时候同一个接口我们会用大量的数据(正常流/异常流)去验证,要是一种场 景写一个接口的话相对于比较麻烦,这个时候就可以使用数据驱动来实现 1.本地创建一个txt文件,第一行写上字段名,多个 ...
- Ant Design Vue中Table的选中详解
<template> <a-table :columns="columns" :data-source="data" :row-selecti ...
- P5963 [BalticOI ?] Card 卡牌游戏【来源请求】
[rt](https://www.luogu.com.cn/problem/P5963)------------## part1### 题意简述给你 $n$ 张纸牌,每张纸牌有两个面.将 $n$ 张纸 ...