若该文为原创文章,未经允许不得转载
原博主博客地址:https://blog.csdn.net/qq21497936
原博主博客导航:https://blog.csdn.net/qq21497936/article/details/102478062
本文章博客地址:https://blog.csdn.net/qq21497936/article/details/108648385
各位读者,知识无穷而人力有穷,要么改需求,要么找专业人士,要么自己研究

 

前言

  ffmpeg解码之后,显示需要同步,一是需要显示,本篇使用SDL进行显示,二是需要对时间戳进行同步。

 

FFmpeg解码

 

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解码视频并使用SDL同步时间显示播放的更多相关文章

  1. FFmpeg开发笔记(四):ffmpeg解码的基本流程详解

    若该文为原创文章,未经允许不得转载原博主博客地址:https://blog.csdn.net/qq21497936原博主博客导航:https://blog.csdn.net/qq21497936/ar ...

  2. FFmpeg开发笔记(五):ffmpeg解码的基本流程详解(ffmpeg3新解码api)

    若该文为原创文章,未经允许不得转载原博主博客地址:https://blog.csdn.net/qq21497936原博主博客导航:https://blog.csdn.net/qq21497936/ar ...

  3. FFmpeg开发笔记(九):ffmpeg解码rtsp流并使用SDL同步播放

    前言   ffmpeg播放rtsp网络流和摄像头流.   Demo   使用ffmpeg播放局域网rtsp1080p海康摄像头:延迟0.2s,存在马赛克     使用ffmpeg播放网络rtsp文件流 ...

  4. FFmpeg开发笔记(三):ffmpeg介绍、windows编译以及开发环境搭建

    前言   本篇章是对之前windows环境的补充,之前windows的是无需进行编译的,此篇使用源码进行编译,版本就使用3.4.8.   FFmpeg简介   FFmpeg是领先的多媒体框架,能够解码 ...

  5. FFmpeg开发笔记(十):ffmpeg在ubuntu上的交叉编译移植到海思HI35xx平台

    FFmpeg和SDL开发专栏(点击传送门) 上一篇:<FFmpeg开发笔记(九):ffmpeg解码rtsp流并使用SDL同步播放>下一篇:敬请期待   前言   将ffmpeg移植到海思H ...

  6. Django开发笔记六

    Django开发笔记一 Django开发笔记二 Django开发笔记三 Django开发笔记四 Django开发笔记五 Django开发笔记六 1.登录功能完善 登录成功应该是重定向到首页,而不是转发 ...

  7. FFmpeg开发笔记(一)搭建Linux系统的开发环境

    对于初学者来说,如何搭建FFmpeg的开发环境是个不小的拦路虎,因为FFmpeg用到了许多第三方开发包,所以要先编译这些第三方源码,之后才能给FFmpeg集成编译好的第三方库.不过考虑到刚开始仅仅调用 ...

  8. ffmpeg学习笔记-初识ffmpeg

    ffmpeg用来对音视频进行处理,那么在使用ffmpeg前就需要ffmpeg有一个大概的了解,这里使用雷神的ppt素材进行整理,以便于复习 音视频基础知识 视频播放器的原理 播放视频的流程大致如下: ...

  9. FFmpeg开发笔记(二)搭建Windows系统的开发环境

    由于Linux系统比较专业,个人电脑很少安装Linux,反而大都安装Windows系统,因此提高了FFmpeg的学习门槛,毕竟在Windows系统搭建FFmpeg的开发环境还是比较麻烦的.不过若有已经 ...

  10. netty权威指南学习笔记六——编解码技术之MessagePack

    编解码技术主要应用在网络传输中,将对象比如BOJO进行编解码以利于网络中进行传输.平常我们也会将编解码说成是序列化/反序列化 定义:当进行远程跨进程服务调用时,需要把被传输的java对象编码为字节数组 ...

随机推荐

  1. [转帖]Kafka 核心技术与实战学习笔记(六)kafka线上集群部署方案

    一.操作系统-Linux Kafka是JVM系的大数据框架 kafka由Scala语言和Java语言编写而成,编译之后的源代码就是普通的".class"文件 使用Linux kaf ...

  2. [转帖]天行健,国产CPU当自强不息

      https://baijiahao.baidu.com/s?id=1699201892754975586 本页面的文字和图像允许在CC-BY-SA 3.0协议四和GNU自由文档许可证下修改和再使用 ...

  3. [转帖]解读内核 sysctl 配置中 panic、oops 相关项目

    写在前面 本篇文章的内容主要来自内核源码树 Documentation/admin-guide/sysctl/kernel.rst文件. softlockup vs hardlockup softlo ...

  4. [转帖]Linux 防火墙开放特定端口 (iptables)

    查看状态: iptables -L -n 下面添加对特定端口开放的方法: 使用iptables开放如下端口 /sbin/iptables -I INPUT -p tcp --dport 8000 -j ...

  5. Java单元测试浅析(JUnit+Mockito)

    作者:京东物流 秦彪 1. 什么是单元测试 (1)单元测试环节: 测试过程按照阶段划分分为:单元测试.集成测试.系统测试.验收测试等.相关含义如下: 1)       单元测试: 针对计算机程序模块进 ...

  6. TypeScript中的元组 Tuple

    元组类型 // 元组类型:表示一个已知元素数量和类型的数组,各元素的类型不必相同 let undata: [string, '男'| '女']; //已知数量是两个.类型分别是字符串和男或者女 und ...

  7. MySQL数据库精选(从入门使用到底层结构)

    基本使用MySQL 通用语法及分类 DDL: 数据定义语言,用来定义数据库对象(数据库.表.字段) DML: 数据操作语言,用来对数据库表中的数据进行增删改 DQL: 数据查询语言,用来查询数据库中表 ...

  8. Gin 路由注册与请求参数获取

    目录 一.Web应用开发的两种模式 1.前后端不分离模式 2.前后端分离模式 二.RESTful介绍 三.API接口 3.1 RESTful API设计指南 3.2 API与用户的通信协议 3.3 R ...

  9. Asp .Net Core 部署在阿里云Centos上 :使用Docker部署

    参照 https://www.cnblogs.com/xiaxiaolu/p/9973631.html 运行环境 使用SecureCrt连接服务器 1.阿里云ECS 4核 16 GiB 8Mbps 带 ...

  10. 在K8S中,deploy升级过程包括什么?

    在Kubernetes (K8S) 中,Deployment的升级过程主要包括以下几个步骤: 更新Deployment配置: 当需要对应用程序进行升级时,通常会更新Deployment的YAML配置文 ...