热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

跨平台播放器开发,开发一个播放器需要用到的FFmpeg知识

咱们先来看一个流程图:该系列文章就是将上图拆分为具体的代码模块,那么该篇咱们主要讲解如何利用FFmpegAPI来对一个输入数据进行解封装,读取原始音频视

咱们先来看一个流程图:

该系列文章就是将上图拆分为具体的代码模块,那么该篇咱们主要讲解如何利用 FFmpeg API 来对一个输入数据进行解封装,读取原始音频视频信息,然后对音频视频做一些基本操作。基本上在播放器模块中用到的 FFmpeg API 咱们都要对它有一个了解。


FFmpeg 基础知识


解封装

利用 FFmpeg api 来对输入视频进行解封装,先来看一下使用 api 的流程

看完上图是不是对解封装的 API 有一个大概的了解? 从一个 输入 URL 到读取到 压缩数据流 就这么几步,很简单的,下面我们用代码实际演示一下:

1.注册所有函数

av_register_all()

其实在最新的版本中该函数已经过时了,在最低的版本中还是必须调用该函数的。

 

2.注册网络模块

//初始化网络库(可以打开 rtmp、rtsp、http 等协议的流媒体视频)
avformat_network_init();

 3.打开输入流并读取头信息

//参数设置AVDictionary *opts &#61; NULL;//设置rtsp流已tcp协议打开av_dict_set(&opts, "rtsp_transport", "tcp", 0);//网络延时时间av_dict_set(&opts, "max_delay", "1000", 0);//解封装上下文AVFormatContext *ic &#61; NULL;int re &#61; avformat_open_input(&ic,inpath,0, // 0表示自动选择解封器&opts //参数设置&#xff0c;比如rtsp的延时时间);//返回值 0 成功if (re !&#61; 0) {char buf[1024] &#61; {0};av_strerror(re, buf, sizeof(buf) - 1);cout <<"open " <

这里要注意&#xff0c;调用该函数那么在结尾处一定要调用 avformat_close_input()

4.读取媒体文件数据包

//return >&#61;0 if OK, AVERROR_xxx on error
re &#61; avformat_find_stream_info(ic, 0);//打印视频流详细信息
av_dump_format(ic, 0, inpath, 0);

 

5.获取音视频流信息


  1. 通过遍历的方式获取

//获取音视频流信息 &#xff08;遍历&#xff0c;函数获取&#xff09;for (int i &#61; 0; i nb_streams; i&#43;&#43;) {AVStream *as &#61; ic->streams[i];cout <<"codec_id &#61; " <codecpar->codec_id <codecpar->format <codecpar->codec_type &#61;&#61; AVMEDIA_TYPE_AUDIO) {audioStream &#61; i;cout <codecpar->sample_rate <codecpar->channels <codecpar->frame_size <codecpar->codec_type &#61;&#61; AVMEDIA_TYPE_VIDEO) {videoStream &#61; i;cout <codecpar->width <codecpar->height <avg_frame_rate) <

  1. 2通过 API 方式获取

//获取视频流
videoStream &#61; av_find_best_stream(ic, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
AVStream *as &#61; ic->streams[videoStream];
cout <cout <<"width&#61;" <codecpar->width <cout <<"height&#61;" <codecpar->height <//帧率 fps 分数转换
cout <<"video fps &#61; " <avg_frame_rate) <

 6.读取压缩数据包

AVPacket *pkt &#61; av_packet_alloc();for (;;) {int re &#61; av_read_frame(ic, pkt);if (re !&#61; 0) {//循环播放cout <<"&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;end&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;" <size &#61; " <size <pts &#61; " <pts <pts ms &#61; " <pts * (r2d(ic->streams[pkt->stream_index]->time_base) * 1000) <dts &#61; " <dts <stream_index &#61;&#61; videoStream) {cout <<"图像" <stream_index &#61;&#61; audioStream) {cout <<"音频" <

调试之后的 log

 

 


解码

调用 ·ffmpeg api 来对音视频压缩数据解码的话&#xff0c;其实也很简单&#xff0c;主要使用如下几个 api ,见下图:

 

我们接着在解封装的代码基础上进行添加&#xff0c;代码如下:

//找到视频解码器AVCodec *vcodec &#61; avcodec_find_decoder(ic->streams[videoStream]->codecpar->codec_id);if (!vcodec) {cout <<"can&#39;t find the codec id" <streams[videoStream]->codecpar->codec_id <streams[videoStream]->codecpar);//配置解码线程vctx->thread_count &#61; 8;//打开解码器上下文re &#61; avcodec_open2(vctx, 0, 0);if (re !&#61; 0) {char buf[1024] &#61; {0};av_strerror(re, buf, sizeof(buf) - 1);cout <<"video avcodec_open2 failed!" <

 

找解码器也可以通过如下 API 形式进行:

AVCodec *avcodec_find_decoder_by_name(const char *name);

如果想要打开音频解码器&#xff0c;代码一样&#xff0c;换下参数即可&#xff0c;下面进行真正解码&#xff1a;

//malloc AVPacket并初始化 AVPacket *pkt &#61; av_packet_alloc(); //接收解码的原始数据 AVFrame *frame &#61; av_frame_alloc(); for (;;) { int re &#61; av_read_frame(ic, pkt); if (re !&#61; 0) { break; } //解码视频 //发送 packet 到解码线程 re &#61; avcodec_send_packet(avcc, pkt); //释放&#xff0c;引用计数-1 为0释放空间 av_packet_unref(pkt); //一次 send 可能对于多次 receive for (;;) { re &#61; avcodec_receive_frame(avcc, frame); if (re !&#61; 0)break; //释放&#xff0c;引用计数-1 为0释放空间 av_frame_unref(frame); }

这样就可以进行解码了&#xff0c;现在我们添加一些打印参数&#xff0c;比如音频采样信息&#xff0c;视频宽高信息:

 

总时长:totalMs &#61; 10534 ms视频信息&#xff1a;bitrate&#61;907_500fps &#61; 30.0003codec_id &#61; 86018format &#61; AV_PIX_FMT_YUV420P 11521080 - 1920pict_type&#61; AV_PICTURE_TYPE_I音频信息&#xff1a;sample_rate &#61; 48000channels &#61; 2

 


视频像素格式转换

视频像素格式其实就是 YUV 转 RGB 的一个过程&#xff0c; FFmpeg 也提供了对应的 API &#xff0c;它是使用 CPU 运算能力来转换&#xff0c;效率是比较低的。咱们播放器使用 OpenGL GPU 来转&#xff0c;效率比较高。虽然 FFmpeg API 转换效率比较低&#xff0c;但是我们还是可以学习一下的。使用流程如下:

 

仅仅 2 个 API 就可以达到对 YUV 的转换或者裁剪&#xff0c;代码示例:

const int in_width &#61; frame->width; const int in_height &#61; frame->height; const int out_width &#61; in_width / 2; const int out_height &#61; in_height / 2; /** * &#64;param context : 缩放上下文&#xff0c;如果为 NULL,那么内部会进行创建&#xff0c; * 如果已经存在&#xff0c;参数也没有发生变化&#xff0c;那么就直接返回当前&#xff0c;否者释放缩放上下文&#xff0c;重新创建。 * &#64;param srcW : 输入的宽 * &#64;param srcH : 输入的高 * &#64;param srcFormat : 输入的格式 * &#64;param dstW : 输出的宽 * &#64;param dstH : 输出的高 * &#64;param dstFormat : 输出的格式 * &#64;param flags : 提供了一系列的算法&#xff0c;快速线性&#xff0c;差值&#xff0c;矩阵&#xff0c;不同的算法性能也不同&#xff0c; 快速线性算法性能相对较高。只针对尺寸的变换。 * &#64;param srcFilter : 输入过滤器 * &#64;param dstFilter : 输出过滤器 * &#64;param param : 这个跟 flags 算法相关&#xff0c;一般传入 O * &#64;return : 缩放的上下文 */ vsctx &#61; sws_getCachedContext( vsctx,//传入NULL 会新创建 in_width, in_height, (AVPixelFormat) frame->format, //输入的宽高&#xff0c;格式 out_width, out_height, AV_PIX_FMT_RGBA, //输出的宽高&#xff0c;格式 SWS_BILINEAR, //尺寸变换的算法 0, 0, 0 ); /** * &#64;param c 缩放上下文 * &#64;param srcSlice YUV 切换数据可以是指针&#xff0c;也可以是数组 * &#64;param srcStride 对应 YUV 一行的大小 * &#64;param srcSliceY 这个用不到传入 0 即可 * &#64;param srcSliceH YUV 的高 * &#64;param dst 输出的像素格式数据 * &#64;param dstStride 输出的像素格式数据的大小 * &#64;return 返回转换后的高 */ re &#61; sws_scale(vsctx, frame->data, //输入数据 frame->linesize,//输入行大小 0, frame->height,//输出高度 (uint8_t *const *) (data), //输出数据 lines//输出大小 );

 

上面的注释都很详细&#xff0c;相信大家也能看的明白&#xff0c;最后我们看下调试后的log,如下:

像素格式尺寸转换上下文创建或者获取成功&#xff01;in_width&#61;1080in_height&#61;1920out_width&#61;540out_height&#61;960sws_scale success! return height of the output slice &#61;960&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;end&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;

重采样

重采样的意思就是将音频的输入参数统一输出某个特定的值&#xff0c;这样做的好处就是归一化播放器的声音输出。那么怎么使用 FFmpeg API 来进行重采样呢? 先来看一张流程图:

 

我们还是以之前的代码继续写&#xff0c;

我们统一输出的参数为 sample_rate&#61;48000,sample_channel&#61;2,sample_fml&#61;AV_SAMPLE_FMT_S16

... //音频重采样 SwrContext *asctx &#61; swr_alloc(); //设置重采样参数 asctx &#61; swr_alloc_set_opts(asctx //重采样上下文 , av_get_default_channel_layout(2)//输出声道格式 , AV_SAMPLE_FMT_S16 //输出声音样本格式 , 48000 //输出采样率 , av_get_default_channel_layout(actx->channels)//输入通道数 , actx->sample_fmt //输入声音样本格式 , actx->sample_rate, 0, 0 //输入音频采样率 ); //初始化采样上下文 re &#61; swr_init(asctx); if (re !&#61; 0) { char buf[1024] &#61; {0}; av_strerror(re, buf, sizeof(buf) - 1); cout <<"audio swr_init failed!" <streams[pkt->stream_index]->time_base);// av_seek_frame(ic, videoStream, pos, AVSEEK_FLAG_BACKWARD | AVSEEK_FLAG_FRAME);// continue; break; } cout <<"pkt->size &#61; " <size <pts &#61; " <pts <pts ms &#61; " <pts * (r2d(ic->streams[pkt->stream_index]->time_base) * 1000) <dts &#61; " <dts <stream_index &#61;&#61; videoStream) { cout <<"图像" <stream_index &#61;&#61; audioStream) { cout <<"音频" <nb_samples * 16/8 * 2]; data[0] &#61; {pcm}; int len &#61; swr_convert(asctx, data, frame->nb_samples //输出 , (const uint8_t **) frame->data, frame->nb_samples //输入 ); if (len >&#61; 0) { cout <<"swr_convert success return len &#61; " <

转换后的log:

swr_convert success return len &#61; 1024

 


seek 操作

我们如果想要指定某个时间看某段画面的话就需要对视频做 seek 操作&#xff0c;FFmpeg 提供了 av_seek_frame 函数来对视频的跳转&#xff0c;它有 4 个输入参数&#xff0c;含义如下:

/*** 根据时间戳和音频或视频的索引 seek 到关键帧的操作** &#64;param s 媒体格式上下文* &#64;param stream_index 流索引&#xff0c;传入 -1 为默认* &#64;param timestamp 需要跳转到时间戳的位置* &#64;param flags seek 的模式* &#64;return >&#61; 0 on success*/
int av_seek_frame(AVFormatContext *s, int stream_index, int64_t timestamp,int flags);

我们着重看下最后一个 flags 参数


//AVSEEK_FLAG_BACKWARD

seek 到后面的关键帧

//AVSEEK_FLAG_BYTE

基于以字节为单位的位置查找

//AVSEEK_FLAG_ANY

Seek 到任意一帧&#xff0c;注意不是关键帧&#xff0c;那么会有花屏的可能。

//AVSEEK_FLAG_FRAME

seek 到关键帧的位置


 我们一般以这样的形式来进行 seek 操作:

int ms &#61; 3000; //三秒位置 根据时间基数&#xff08;分数&#xff09;转换
long long pos &#61; (double) ms / (double) 1000 * r2d(ic->streams[pkt->stream_index]->time_base);
av_seek_frame(ic, videoStream, pos, AVSEEK_FLAG_BACKWARD | AVSEEK_FLAG_FRAME);

上面的含义就是定位到 3000 ms 位置后面的关键帧处开始播放。后面播放器 seek 功能的时候我们会介绍如何精准 seek 操作 该篇文章对于 FFmpeg 的知识我们就介绍到这里,后面在开发中如果有新遇见的我会再详细介绍一下。 总结 播放器要用到的 FFmpeg 知识 大概就这么多&#xff0c;可以发现这些 API 其实都比较简单。此刻我相信你已经对这些 API 有一定的印象和了解了吧。 



推荐阅读
  • 本文介绍了Android中的assets目录和raw目录的共同点和区别,包括获取资源的方法、目录结构的限制以及列出资源的能力。同时,还解释了raw目录中资源文件生成的ID,并说明了这些目录的使用方法。 ... [详细]
  • Android工程师面试准备及设计模式使用场景
    本文介绍了Android工程师面试准备的经验,包括面试流程和重点准备内容。同时,还介绍了建造者模式的使用场景,以及在Android开发中的具体应用。 ... [详细]
  • XML介绍与使用的概述及标签规则
    本文介绍了XML的基本概念和用途,包括XML的可扩展性和标签的自定义特性。同时还详细解释了XML标签的规则,包括标签的尖括号和合法标识符的组成,标签必须成对出现的原则以及特殊标签的使用方法。通过本文的阅读,读者可以对XML的基本知识有一个全面的了解。 ... [详细]
  • 本文讨论了如何使用GStreamer来删除H264格式视频文件中的中间部分,而不需要进行重编码。作者提出了使用gst_element_seek(...)函数来实现这个目标的思路,并提到遇到了一个解决不了的BUG。文章还列举了8个解决方案,希望能够得到更好的思路。 ... [详细]
  • Android图形架构学习笔记(待修改)
    以下简单总结来自Android官网,稍作总结:https:source.android.google.cndevicesgraphics概览Andr ... [详细]
  • Android实现彩信附件的添加与删除功能-本文实例讲述了Android实现彩信附件的添加与删除功能。分享给大家供大家参考,具体如下:添加附件在ComposeMessageActi ... [详细]
  • 本文介绍了Web学习历程记录中关于Tomcat的基本概念和配置。首先解释了Web静态Web资源和动态Web资源的概念,以及C/S架构和B/S架构的区别。然后介绍了常见的Web服务器,包括Weblogic、WebSphere和Tomcat。接着详细讲解了Tomcat的虚拟主机、web应用和虚拟路径映射的概念和配置过程。最后简要介绍了http协议的作用。本文内容详实,适合初学者了解Tomcat的基础知识。 ... [详细]
  • 不同优化算法的比较分析及实验验证
    本文介绍了神经网络优化中常用的优化方法,包括学习率调整和梯度估计修正,并通过实验验证了不同优化算法的效果。实验结果表明,Adam算法在综合考虑学习率调整和梯度估计修正方面表现较好。该研究对于优化神经网络的训练过程具有指导意义。 ... [详细]
  • 本文介绍了UVALive6575题目Odd and Even Zeroes的解法,使用了数位dp和找规律的方法。阶乘的定义和性质被介绍,并给出了一些例子。其中,部分阶乘的尾零个数为奇数,部分为偶数。 ... [详细]
  • CF:3D City Model(小思维)问题解析和代码实现
    本文通过解析CF:3D City Model问题,介绍了问题的背景和要求,并给出了相应的代码实现。该问题涉及到在一个矩形的网格上建造城市的情景,每个网格单元可以作为建筑的基础,建筑由多个立方体叠加而成。文章详细讲解了问题的解决思路,并给出了相应的代码实现供读者参考。 ... [详细]
  • 第四章高阶函数(参数传递、高阶函数、lambda表达式)(python进阶)的讲解和应用
    本文主要讲解了第四章高阶函数(参数传递、高阶函数、lambda表达式)的相关知识,包括函数参数传递机制和赋值机制、引用传递的概念和应用、默认参数的定义和使用等内容。同时介绍了高阶函数和lambda表达式的概念,并给出了一些实例代码进行演示。对于想要进一步提升python编程能力的读者来说,本文将是一个不错的学习资料。 ... [详细]
  • 本文介绍了在Python中使用zlib模块进行字符串的压缩与解压缩的方法,并探讨了其在内存优化方面的应用。通过压缩存储URL等长字符串,可以大大降低内存消耗,虽然处理时间会增加,但是整体效果显著。同时,给出了参考链接,供进一步学习和应用。 ... [详细]
  • testcafe的版本-1.7.0和1.7.1(最新)铬版本-78.0.3904.108运行环境 ... [详细]
  • conda下安装pytorch最详细教程 // 安装pytorch踩坑记录 // cuda11.5下pytorch安装 // torch.cuda.is_available()输出False解决办法
    几点说在前面!!!!!我踩的坑:1、一开始入坑使用pytorch框架没有用anaconda,现在非常后悔!!!conda对小白管理环境真的巨好用!!!2、安装时候torch版本不对 ... [详细]
  • Html5第一章
    Html5第一章 ... [详细]
author-avatar
书友48058773
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有