一、前言

从代码层面以及自由度来说,用ffmpeg来写全能播放组件是最佳方案(跨平台最好最多、编解码能力最强),尽管已经有优秀的vlc/mpv等方案可以直接用,但是vlc/mpv对标主要是播放器应用层面,其他层面比如视频监控行业领域就比较鸡肋,所以还是从底层一点一滴做解码编码会让自己更熟练。关于网上很多ffmpeg的示例,尤其是播放的示例,数不胜数,比较适合用来入门学习,问题是随着ffmpeg官方不断的迭代更新,很多代码都不可用,因为api变了,尤其是最近5年迭代的特别快,从2017年开始直接猛飚版本,现在直接干到了ffmpeg6版本,一般在安排取消或者改动某些api接口前几个版本,都会打上对应的标记,既有新方法,也兼容旧的api,一般会放在下一个大版本将旧的api接口移除,以便减轻历史包袱,在核心功能编解码这块,一直是兼容的,不会说新版本不兼容以前旧版本的一些编解码格式。

编写这个全能播放组件,面对用户各种各样的需求,当然需要从ffmpeg2兼容到ffmpeg6以及后续的版本,现在用的最多的还是ffmpeg4版本,目测三五年后会陆续切换到ffmpeg5/ffmpeg6,主要是支持的格式多了,尤其是某些新标准的编解码的效率更高。在ffmpeg提供的头文件接口中,并没有提供ffmpeg的大版本号,只提供了字符串版本,所以需要通过子库的主版本号来定义一个ffmpeg的版本号,比如编解码库LIBAVCODEC_VERSION_MAJOR,56=ffmpeg2/57=ffmpeg3/58=ffmpeg4/59=ffmpeg5/60=ffmpeg6,这个编解码库就是ffmpeg的核心,看家的本领都在里面,个人觉得ffmpeg最牛逼的就是编解码和滤镜。在兼容各个版本的这条路上,大致整理了以下几条:

  1. 以前AVStream带了解码器参数,stream->codec,现在去掉了,对应放在stream->codecpar中。
  2. 拷贝上下文参数以前是avcodec_copy_context,现在对应avcodec_parameters_copy。
  3. 参数拷贝以前是avcodec_copy_context,现在对应avcodec_parameters_from_context/avcodec_parameters_to_context。
  4. 编码以前是avcodec_encode_video2/avcodec_encode_audio2,现在对应avcodec_send_frame后avcodec_receive_packet(视音频步骤一样)。
  5. 解码以前是avcodec_decode_video2/avcodec_decode_audio4,现在对应avcodec_send_packet后avcodec_receive_frame(视音频步骤一样)。
  6. 打印解码编码器名称以前是av_codec_next,现在对应av_codec_iterate。
  7. 获取和设置旋转角度以前是av_dict_get(stream->metadata, "rotate", NULL, 0)/av_dict_set(&stream->metadata, "rotate", "90", 0),现在对应av_stream_get_side_data后av_display_rotation_get/av_stream_new_side_data后av_display_rotation_set。

二、效果图

三、体验地址

  1. 国内站点:https://gitee.com/feiyangqingyun
  2. 国际站点:https://github.com/feiyangqingyun
  3. 个人作品:https://blog.csdn.net/feiyangqingyun/article/details/97565652
  4. 体验地址:https://pan.baidu.com/s/1d7TH_GEYl5nOecuNlWJJ7g 提取码:01jf 文件名:bin_video_demo。

四、相关代码

//通过avcode版本定义对应主版本
#if (LIBAVCODEC_VERSION_MAJOR == 56)
#define FFMPEG_VERSION_MAJOR 2
#elif (LIBAVCODEC_VERSION_MAJOR == 57)
#define FFMPEG_VERSION_MAJOR 3
#elif (LIBAVCODEC_VERSION_MAJOR == 58)
#define FFMPEG_VERSION_MAJOR 4
#elif (LIBAVCODEC_VERSION_MAJOR == 59)
#define FFMPEG_VERSION_MAJOR 5
#elif (LIBAVCODEC_VERSION_MAJOR == 60)
#define FFMPEG_VERSION_MAJOR 6
#endif int FFmpegHelper::getRotate(AVStream *stream)
{
int rotate = 0;
//测试发现ffmpeg2不支持旋转滤镜
#if (FFMPEG_VERSION_MAJOR < 3)
return rotate;
#endif #if (FFMPEG_VERSION_MAJOR < 5)
AVDictionaryEntry *tag = NULL;
tag = av_dict_get(stream->metadata, "rotate", NULL, 0);
if (tag) {
rotate = atoi(tag->value);
}
#else
//从ffplay源码中找到的方法
double theta = 0;
quint8 *displaymatrix = av_stream_get_side_data(stream, AV_PKT_DATA_DISPLAYMATRIX, NULL);
if (displaymatrix) {
theta = -av_display_rotation_get((qint32 *) displaymatrix);
theta -= 360 * floor(theta / 360 + 0.9 / 360);
rotate = theta;
}
#endif
return rotate;
} void FFmpegHelper::setRotate(AVStream *stream, int rotate)
{
#if (FFMPEG_VERSION_MAJOR < 5)
av_dict_set(&stream->metadata, "rotate", QString::number(rotate).toUtf8().constData(), 0);
#else
quint8 *sidedata = av_stream_new_side_data(stream, AV_PKT_DATA_DISPLAYMATRIX, sizeof(qint32) * 9);
if (sidedata) {
av_display_rotation_set((qint32 *)sidedata, rotate);
}
#endif
} void FFmpegHelper::getStreamInfo(AVStream *stream, int &id, int &width, int &height, qint64 &bitrate, int &sampleRate, int &channelCount, int &profile)
{
#if (FFMPEG_VERSION_MAJOR < 3)
id = stream->codec->codec_id;
width = stream->codec->width;
height = stream->codec->height;
bitrate = stream->codec->bit_rate;
sampleRate = stream->codec->sample_rate;
channelCount = stream->codec->channels;
profile = stream->codec->profile;
#else
id = stream->codecpar->codec_id;
width = stream->codecpar->width;
height = stream->codecpar->height;
bitrate = stream->codecpar->bit_rate;
sampleRate = stream->codecpar->sample_rate;
channelCount = stream->codecpar->channels;
profile = stream->codecpar->profile;
#endif
} int FFmpegHelper::copyContext(AVStream *streamIn, AVStream *streamOut)
{
int result = -1;
//设置 codec_tag = 0 这个很关键(不加保存的数据可能不正确)
#if (FFMPEG_VERSION_MAJOR < 3)
result = avcodec_copy_context(streamOut->codec, streamIn->codec);
streamOut->codec->codec_tag = 0;
#else
result = avcodec_parameters_copy(streamOut->codecpar, streamIn->codecpar);
streamOut->codecpar->codec_tag = 0;
#endif
return result;
} int FFmpegHelper::copyContext(AVCodecContext *avctx, AVStream *stream, bool from)
{
int result = -1;
#if (FFMPEG_VERSION_MAJOR < 3)
if (from) {
result = avcodec_copy_context(stream->codec, avctx);
} else {
result = avcodec_copy_context(avctx, stream->codec);
}
#else
if (from) {
result = avcodec_parameters_from_context(stream->codecpar, avctx);
} else {
result = avcodec_parameters_to_context(avctx, stream->codecpar);
}
#endif
return result;
} int FFmpegHelper::decode(FFmpegThread *thread, AVCodecContext *avctx, AVPacket *packet, AVFrame *frame, bool video)
{
int result = -1;
#ifdef videoffmpeg
QString flag = video ? "视频解码" : "音频解码";
#if (FFMPEG_VERSION_MAJOR < 3)
if (video) {
avcodec_decode_video2(avctx, frame, &result, packet);
if (result < 0) {
thread->debug(result, flag, "avcodec_decode_video2");
return result;
}
} else {
avcodec_decode_audio4(avctx, frame, &result, packet);
if (result < 0) {
thread->debug(result, flag, "avcodec_decode_audio4");
return result;
}
}
goto end;
#else
result = avcodec_send_packet(avctx, packet);
if (result < 0 && (result != AVERROR(EAGAIN)) && (result != AVERROR_EOF)) {
//if (result < 0) {
thread->debug(result, flag, "avcodec_send_packet");
return result;
} while (result >= 0) {
result = avcodec_receive_frame(avctx, frame);
if (result == AVERROR(EAGAIN) || result == AVERROR_EOF) {
break;
} else if (result < 0) {
thread->debug(result, flag, "avcodec_receive_frame");
break;
}
goto end;
}
#endif
return result; end:
//调用线程处理解码后的数据
if (video) {
thread->decodeVideo2(packet);
} else {
thread->decodeAudio2(packet);
}
#endif
return result;
} int FFmpegHelper::encode(FFmpegSave *thread, AVCodecContext *avctx, AVPacket *packet, AVFrame *frame, bool video)
{
int result = -1;
#ifdef videosave
QString flag = video ? "视频编码" : "音频编码";
#if (FFMPEG_VERSION_MAJOR < 3)
if (video) {
avcodec_encode_video2(avctx, packet, frame, &result);
if (result < 0) {
thread->debug(result, flag, "avcodec_encode_video2");
return result;
}
} else {
avcodec_encode_audio2(avctx, packet, frame, &result);
if (result < 0) {
thread->debug(result, flag, "avcodec_encode_audio2");
return result;
}
}
goto end;
#else
result = avcodec_send_frame(avctx, frame);
if (result < 0) {
thread->debug(result, flag, "avcodec_send_frame");
return result;
} while (result >= 0) {
result = avcodec_receive_packet(avctx, packet);
if (result == AVERROR(EAGAIN) || result == AVERROR_EOF) {
break;
} else if (result < 0) {
thread->debug(result, flag, "avcodec_receive_packet");
break;
}
goto end;
}
#endif
return result; end:
thread->writePacket2(packet, video);
#endif
return result;
}

五、功能特点

  1. 支持各种音视频文件、本地摄像头设备,各种视频流网络流。
  2. 支持开始播放、暂停播放、继续播放、停止播放、设置播放进度、倍速播放。
  3. 可设置音量、静音切换、抓拍图片、录像存储。
  4. 自动提取专辑信息比如标题、艺术家、专辑、专辑封面,自动显示专辑封面。
  5. 完美支持音视频同步和倍速播放。
  6. 解码策略支持速度优先、质量优先、均衡处理、最快速度。
  7. 支持手机视频旋转角度显示,比如一般手机拍摄的视频是旋转了90度的,解码显示的时候需要重新旋转90度才是正的。
  8. 自动转换yuv420格式,比如本地摄像头是yuyv422格式,有些视频文件是xx格式,统一将非yuv420格式转换,然后再进行处理。
  9. 支持硬解码dxva2、d3d11va等,性能极高尤其是大分辨率比如4K视频。
  10. 视频响应极低延迟0.2s左右,极速响应打开视频流0.5s左右,专门做了优化处理。
  11. 硬解码和GPU绘制组合,极低CPU占用,比海康大华等客户端更优。
  12. 支持视频流中的各种音频格式,AAC、PCM、G.726、G.711A、G.711Mu、G.711ulaw、G.711alaw、MP2L2等都支持,推荐选择AAC兼容性跨平台性最好。
  13. 视频存储支持yuv、h264、mp4多种格式,音频存储支持pcm、wav、aac多种格式。默认视频mp4格式、音频aac格式。
  14. 支持分开存储音频视频文件,也支持合并到一个mp4文件,默认策略是无论何种音视频文件格式存储,最终都转成mp4及aac格式,然后合并成音视频一起的mp4文件。
  15. 支持本地摄像头实时视频显示带音频输入输出,音视频录制合并到一个mp4文件。
  16. 支持H264/H265编码(现在越来越多的监控摄像头是H265视频流格式)生成视频文件,内部自动识别切换编码格式。
  17. 自动识别视频流动态分辨率改动,重新打开视频流。
  18. 支持用户信息中包含特殊字符(比如用户信息中包含+#@等字符)的视频流播放,内置解析转义处理。
  19. 纯qt+ffmpeg解码,非sdl等第三方绘制播放依赖,gpu绘制采用qopenglwidget,音频播放采用qaudiooutput。
  20. 同时支持ffmpeg2、ffmpeg3、ffmpeg4、ffmpeg5、ffmpeg6以及后续版本,全部做了兼容处理。如果需要支持xp需要选用ffmpeg3或ffmpeg2。
  21. 支持滤镜,源头带各种水印及图形效果,可以将OSD标签信息和各种图形信息写入到MP4文件。

Qt/C++编写全能播放组件(支持ffmpeg2/3/4/5/6/Qt4/5/6)的更多相关文章

  1. Qt高仿Excel表格组件-支持冻结列、冻结行、内容自适应和合并单元格

    目录 一.概述 二.效果展示 三.实现思路 1.冻结行.冻结列 2.行高自适应 3.蚂蚁线 四.测试代码 1.添加表格数据 2.设置冻结行.列 3.行高.列宽 4.单元格背景色 5.单元格文字 6.其 ...

  2. Qt实现表格树控件-支持多级表头

    目录 一.概述 二.效果展示 三.实现方式 四.多级表头 1.数据源 2.表格 3.QStyledItemDelegate绘制代理 五.测试代码 六.相关文章 原文链接:Qt实现表格树控件-支持多级表 ...

  3. C#编写媒体播放器--Microsoft的Directx提供的DirectShow组件,该组件的程序集QuartzTypeLib.dll.

    使用C#编写媒体播放器时,需要用到Microsoft的Directx提供的DirectShow组件.用该组件前需要先注册程序集QuartzTypeLib.dll. 1.用QuartzTypeLib.d ...

  4. Qt大型工程开发技术选型Part3:Qt调用C#编写的COM组件实例

    Qt大型工程开发技术选型Part3:Qt调用C#编写的COM组件实例以及错误总结 ok,前面铺垫了那么多,现在来写一个开发实例,我会把其中隐藏的坑和陷阱简单谈谈,并在文章最后总结. 不愿意看长篇大论的 ...

  5. QT | 聊聊QT与直播流播放——从QMediaPlayer到Qt-AV

    [原创文章,转载请注明来源,方便查看本文更新] 这段时间需要用QT开发一个播放直播流的功能,能够播放各种格式的直播流,并且CPU占用率不要太高(可以占用GPU),这些是我们的目标. 直播流推流的技术进 ...

  6. DirectShow多媒体流捕获播放组件

    英文原文地址:https://msdn.microsoft.com/en-us/library/windows/desktop/dd390351(v=vs.85).aspx Microsoft®Dir ...

  7. artDialog是一个基于javascript编写的对话框组件,它拥有精致的界面与友好的接口

    artDialog是一个基于javascript编写的对话框组件,它拥有精致的界面与友好的接口 自适应内容 artDialog的特殊UI框架能够适应内容变化,甚至连外部程序动态插入的内容它仍然能自适应 ...

  8. JavaScript 是如何工作:Shadow DOM 的内部结构 + 如何编写独立的组件!

    这是专门探索 JavaScript 及其所构建的组件的系列文章的第 17 篇. 如果你错过了前面的章节,可以在这里找到它们: JavaScript 是如何工作的:引擎,运行时和调用堆栈的概述! Jav ...

  9. vue中引入awesomeswiper的方法以及编写轮播组件

    1.先安装less-loader npm install less less-loader --save 2.再安装css-loader npm install css-loader --save 3 ...

  10. 利用Eric+Qt Designer编写倒计时时钟

    [前言]前几日通过编写命令行通讯录,掌握了Python的基本语法结构,于是开始向更高水平冲击,利用Eric与Qt Designer 编写一个带界面的小程序.本次实操中也确实遇到了不少问题,通过学习也都 ...

随机推荐

  1. spring上 -基于注解配置bean,动态代理,AOP笔记

    用的是jdk8,spring框架里jar包的下载可以自己搜到 注解用到的jar包. 60,注解配置Bean快速入门 基本介绍 代码结构: UserDao.java package com.hspedu ...

  2. 基于 KubeSphere 的 AI 平台开发实践

    概述 本文基于 "KubeSphere & Friends 2021 Meetup 北京站" 分享主要内容整理而来,详细内容建议观看视频,本文有一定删减. 作者:胡涛(Da ...

  3. CKS 认证备考指南

    作者:scwang18,主要负责技术架构,在容器云方向颇有研究. 前言 CKA 和 CKS 是 Linux 基金会联合 CNCF 社区组织的云原生技术领域权威的技术水平认证考试,考试采用实操方式进行. ...

  4. windows运行sentry

    原文:https://blog.51cto.com/u_15089766/2602175 解决无法转换镜像版本为v2的问题: https://blog.csdn.net/qq_33306246/art ...

  5. Windows平台安装MongoDB步骤以及问题处理

    今天开始向MongoDB进军,结果一开始就给我来了个下马威--安装不成功,死在了第一步,我滴个乖乖,哪能服气,为了不让后面的小伙伴踩坑,特此记录,希望能帮到有需要的小伙伴. 一.安装步骤 1.下载安装 ...

  6. python实现基于RPC协议的接口自动化测试

    01什么是RPC RPC(Remote Procedure Call)远程过程调用协议是一个用于建立适当框架的协议.从本质上讲,它使一台机器上的程序能够调用另一台机器上的子程序,而不会意识到它是远程的 ...

  7. 2.5 使用U盘安装Linux系统

    前面章节介绍了如何通过虚拟机 VMware 安装 Linux 系统,而实际开发中,我们更多的是要将 Linux 系统直接安装到电脑上. 直接在电脑上安装 Linux 系统的常用方法有 2 种,分别是用 ...

  8. Linux日志轮替与总结

    目录 基本介绍 轮替文件的命名 配置文件 路径位置 参数说明 举例:把自己的日志加入日志轮替 说明 步骤 日志轮替机制原理[个人理解,仅供参考] 总结[关于日志需要掌握的点] 基本介绍 日志轮替就是把 ...

  9. delphi12 Android Edit SDK安装

    安装 delphi 12.1 后,编译 FMX Android 程序失败! 查找原因,SDK配置全是叹号! 之前用过SDK Manager.exe,直接打开即可以选择下载,但现在发现没有了,查找资料如 ...

  10. Microsoft.Extensions.AI 初探

    .NET Conf上的介绍 在今年的.NET Conf上Steve Sanderson带来了题为"AI Building Blocks - A new, unified AI layer&q ...