FFmpeg入门 - 视频播放

news2024/11/17 9:59:28

音视频最好从能够直接看到东西,也更加贴近用户的播放开始学起.

音视频编解码基础

我们可以通过http、rtmp或者本地的视频文件去播放视频。这里的"视频"实际上指的是mp4、avi这种既有音频也有视频的文件格式。

这样的视频文件可能会有多条轨道例如视频轨道、音频轨道、字幕轨道等. 有些格式限制比较多,例如AVI视频轨道只能有一条,音频轨道也只能有一条. 而有些格式则比较灵活,例如OGG视频的视频、音频轨道都能有多条.

像音频、视频这种数据量很大的轨道,上面的数据实际上都是通过压缩的。 视频轨道上可能是H264、H256这样压缩过的图像数据,通过解码可以还原成YUV、RGB等格式的图像数据。 音频轨道上可能是MP3、AAC这样压缩过的的音频数据,通过解码可以还原成PCM的音频裸流。

截屏2022-09-04 下午1.47.57.png

实际上使用ffmpeg去播放视频也就是根据文件的格式一步步还原出图像数据交给显示设备显示、还原出音频数据交给音频设备播放:

截屏2022-09-04 下午1.48.08.png

文末名片免费领取音视频开发学习资料,内容包括(C/C++,Linux 服务器开发,FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,srs)以及音视频学习路线图等等。

ffmpeg简单入门

了解了视频的播放流程之后我们来做一个简单的播放器实际入门一下ffmpeg。由于这篇博客是入门教程,这个播放器功能会进行简化:

  1. 使用ffmpeg 4.4.2版本 - 4.x的版本被使用的比较广泛,而且最新的5.x版本资料比较少

  2. 只解码一个视频轨道的画面进行播放 - 不需要考虑音视频同步的问题

  3. 使用SDL2在主线程解码 - 不需要考虑多线程同步问题

  4. 使用源码+Makefile构建 - 在MAC和Ubuntu上验证过,Windows的同学需要自己创建下vs的工程了

使用ffmpeg去解码大概有下面的几个步骤和关键函数,大家可以和上面的流程图对应一下:

解析文件流(解协议和解封装)

  1. avformat_open_input : 可以打开File、RTMP等协议的数据流,并且读取文件头解析出视频信息,如解析出各个轨道和时长等

  2. avformat_find_stream_info : 对于没有文件头的格式如MPEG或者H264裸流等,可以通过这个函数解析前几帧得到视频的信息

创建各个轨道的解码器(分流)

  1. avcodec_find_decoder: 查找对应的解码器

  2. avcodec_alloc_context3: 创建解码器上下文

  3. avcodec_parameters_to_context: 设置解码所需要的参数

  4. avcodec_open2: 打开解码器

使用对应的解码器解码各个轨道(解码)

  1. av_read_frame: 从视频流读取视频数据包

  2. avcodec_send_packet: 发送视频数据包给解码器解码

  3. avcodec_receive_frame: 从解码器读取解码后的帧数据

为了几种精力在音视频部分,我拆分出了专门进行解码的VideoDecoder类和专门进行画面显示的SdlWindow类,大家主要关注VideoDecoder部分即可。

视频流解析

由于实际解码前的解析文件流和创建解码器代码比较固定化,我直接将代码贴出来,大家可能跟着注释看下每个步骤的含义:

bool VideoDecoder::Load(const string& url) {
    mUrl = url;
​
    // 打开文件流读取文件头解析出视频信息如轨道信息、时长等
    // mFormatContext初始化为NULL,如果打开成功,它会被设置成非NULL的值,在不需要的时候可以通过avcodec_free_context释放。
    // 这个方法实际可以打开多种来源的数据,url可以是本地路径、rtmp地址等
    // 在不需要的时候通过avformat_close_input关闭文件流
    if(avformat_open_input(&mFormatContext, url.c_str(), NULL, NULL) < 0) {
        cout << "open " << url << " failed" << endl;
        return false;
    }
​
    // 对于没有文件头的格式如MPEG或者H264裸流等,可以通过这个函数解析前几帧得到视频的信息
    if(avformat_find_stream_info(mFormatContext, NULL) < 0) {
        cout << "can't find stream info in " << url << endl;
        return false;
    }
​
    // 查找视频轨道,实际上我们也可以通过遍历AVFormatContext的streams得到,代码如下:
    // for(int i = 0 ; i < mFormatContext->nb_streams ; i++) {
    //     if(mFormatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
    //         mVideoStreamIndex = i;
    //         break;
    //     }
    // }
    mVideoStreamIndex = av_find_best_stream(mFormatContext, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
    if(mVideoStreamIndex < 0) {
        cout << "can't find video stream in " << url << endl;
        return false;
    }
​
    // 获取视频轨道的解码器相关参数
    AVCodecParameters* codecParam = mFormatContext->streams[mVideoStreamIndex]->codecpar;
    cout << "codec id = " << codecParam->codec_id << endl;
    
    // 通过codec_id获取到对应的解码器
    // codec_id是enum AVCodecID类型,我们可以通过它知道视频流的格式,如AV_CODEC_ID_H264(0x1B)、AV_CODEC_ID_H265(0xAD)等
    // 当然如果是音频轨道的话它的值可能是AV_CODEC_ID_MP3(0x15001)、AV_CODEC_ID_AAC(0x15002)等
    AVCodec* codec = avcodec_find_decoder(codecParam->codec_id);
    if(codec == NULL) {
        cout << "can't find codec" << endl;
        return false;
    }
​
    // 创建解码器上下文,解码器的一些环境就保存在这里
    // 在不需要的时候可以通过avcodec_free_context释放
    mCodecContext = avcodec_alloc_context3(codec);
    if (mCodecContext == NULL) {
        cout << "can't alloc codec context" << endl;
        return false;
    }
​
​
    // 设置解码器参数
    if(avcodec_parameters_to_context(mCodecContext, codecParam) < 0) {
        cout << "can't set codec params" << endl;
        return false;
    }
​
    // 打开解码器,从源码里面看到在avcodec_free_context释放解码器上下文的时候会close,
    // 所以我们可以不用自己调用avcodec_close去关闭
    if(avcodec_open2(mCodecContext, codec, NULL) < 0) {
        cout << "can't open codec" << endl;
        return false;
    }
​
    // 创建创建AVPacket接收数据包
    // 无论是压缩的音频流还是压缩的视频流,都是由一个个数据包组成的
    // 解码的过程实际就是从文件流中读取一个个数据包传给解码器去解码
    // 对于视频,它通常应包含一个压缩帧
    // 对于音频,它可能是一段压缩音频、包含多个压缩帧
    // 在不需要的时候可以通过av_packet_free释放
    mPacket = av_packet_alloc();
    if(NULL == mPacket) {
        cout << "can't alloc packet" << endl;
        return false;
    }
​
    // 创建AVFrame接收解码器解码出来的原始数据(视频的画面帧或者音频的PCM裸流)
    // 在不需要的时候可以通过av_frame_free释放
    mFrame = av_frame_alloc();
    if(NULL == mFrame) {
        cout << "can't alloc frame" << endl;
        return false;
    }
​
    // 可以从解码器上下文获取视频的尺寸
    // 这个尺寸实际上是从AVCodecParameters里面复制过去的,所以直接用codecParam->width、codecParam->height也可以
    mVideoWidth = mCodecContext->width;
    mVideoHegiht =  mCodecContext->height;
​
    // 可以从解码器上下文获取视频的像素格式
    // 这个像素格式实际上是从AVCodecParameters里面复制过去的,所以直接用codecParam->format也可以
    mPixelFormat = mCodecContext->pix_fmt;
​
    return true;
}

我们使用VideoDecoder::Load打开视频流并准备好解码器。之后就是解码的过程,解码完成之后再调用VideoDecoder::Release去释放资源:

void VideoDecoder::Release() {
    mUrl = "";
    mVideoStreamIndex = -1;
    mVideoWidth = -1;
    mVideoHegiht = -1;
    mDecodecStart = -1;
    mLastDecodecTime = -1;
    mPixelFormat = AV_PIX_FMT_NONE;
​
    if(NULL != mFormatContext) {
        avformat_close_input(&mFormatContext);
    }
​
    if (NULL != mCodecContext) {
        avcodec_free_context(&mCodecContext);
    }
    
    if(NULL != mPacket) {
        av_packet_free(&mPacket);
    }
​
    if(NULL != mFrame) {
        av_frame_free(&mFrame);
    }
}

视频解码

解码器创建完成之后就可以开始解码了:

AVFrame* VideoDecoder::NextFrame() {
    if(av_read_frame(mFormatContext, mPacket) < 0) {
        return NULL;
    }
​
    AVFrame* frame = NULL;
    if(mPacket->stream_index == mVideoStreamIndex
        && avcodec_send_packet(mCodecContext, mPacket) == 0
        && avcodec_receive_frame(mCodecContext, mFrame) == 0) {
        frame = mFrame;
​
        ... //1.解码速度问题
    }
​
    av_packet_unref(mPacket); // 2.内存泄漏问题
​
    if(frame == NULL) {
        return NextFrame(); // 3.AVPacket帧类型问题
    }
​
    return frame;
}

它的核心逻辑其实就是下面这三步:

  1. 使用av_read_frame 从视频流读取视频数据包

  2. 使用avcodec_send_packet 发送视频数据包给解码器解码

  3. 使用avcodec_receive_frame 从解码器读取解码后的帧数据

除了关键的三个步骤之外还有些细节需要注意:

1.解码速度问题

由于解码的速度比较快,我们可以等到需要播放的时候再去解码下一帧。这样可以降低cpu的占用,也能减少绘制线程堆积画面队列造成内存占用过高.

由于这个demo没有单独的解码线程,在渲染线程进行解码,sdl渲染本身就耗时,所以就算不延迟也会发现画面是正常速度播放的.可以将绘制的代码注释掉,然后在该方法内加上打印,会发现一下子就解码完整个视频了。

2.内存泄漏问题

解码完成之后压缩数据包的数据就不需要了,需要使用av_packet_unref将AVPacket释放。

其实AVFrame在使用完成之后也需要使用av_frame_unref去释放AVFrame的像画面素数据,但是在avcodec_receive_frame内会调用av_frame_unref将上一帧的内存清除,而最后一帧的数据也会在Release的时候被av_frame_free清除,所以我们不需要手动调用av_frame_unref.

3.AVPacket帧类型问题

由于视频压缩帧存在i帧、b帧、p帧这些类型,并不是每种帧都可以直接解码出原始画面,b帧是双向差别帧,也就是说b帧记录的是本帧与前后帧的差别,还需要后面的帧才能解码.

如果这一帧AVPacket没有解码出数据来的话,就递归调用NextFrame解码下一帧,直到解出下一帧原生画面来

PTS同步

AVFrame有个pts的成员变量,代表了画面在什么时候应该显示.由于视频的解码速度通常会很快,例如一个1分钟的视频可能一秒钟就解码完成了.所以我们需要计算出这一帧应该在什么时候播放,如果时间还没有到就添加延迟。

有些视频流不带pts数据,按30fps将每帧间隔统一成32ms:

if(AV_NOPTS_VALUE == mFrame->pts) {
    int64_t sleep = 32000 - (av_gettime() - mLastDecodecTime);
    if(mLastDecodecTime != -1 && sleep > 0) {
        av_usleep(sleep);
    }
    mLastDecodecTime = av_gettime();
} else {
    ...
}

如果视频流带pts数据,我们需要计算这个pts具体是视频的第几微秒.

pts的单位可以通过AVFormatContext找到对应的AVStream,然后再获取AVStream的time_base得到:

AVRational timebase = mFormatContext->streams[mPacket->stream_index]->time_base;
AVRational是个分数,代表几分之几秒:

/**
 * Rational number (pair of numerator and denominator).
 */
typedef struct AVRational{
    int num; ///< Numerator
    int den; ///< Denominator
} AVRational;

我们用timebase.num * 1.0f / timebase.den计算出这个分数的值,然后乘以1000等到ms,再乘以1000得到us.后半部分的计算其实可以放到VideoDecoder::Load里面保存到成员变量,但是为了讲解方便就放在这里了:

int64_t pts = mFrame->pts * 1000 * 1000 * timebase.num * 1.0f / timebase.den;

这个pts都是以视频开头开始计算的,所以我们需要先保存第一帧的时间戳,然后再去计算当前播到第几微秒.完整代码如下:

if(AV_NOPTS_VALUE == mFrame->pts) {
    ...
} else {
    AVRational timebase = mFormatContext->streams[mPacket->stream_index]->time_base;
    int64_t pts = mFrame->pts * 1000 * 1000 * timebase.num * 1.0f / timebase.den;
​
    // 如果是第一帧就记录开始时间
    if(mFrame->pts == 0) {
        mDecodecStart = av_gettime() - pts;
    }
​
    // 当前时间减去开始时间,得到当前播放到了视频的第几微秒
    int64_t now = av_gettime() - mDecodecStart;
​
    // 如果这一帧的播放时间还没有到就等到播放时间到了再返回
    if(pts > now) {
        av_usleep(pts - now);
    }
}

其他

完整的Demo已经放到Github上,图像渲染的部分在SdlWindow类中,它使用SDL2去做ui绘制,由于和音视频编解码没有关系就不展开讲了.视频解码部分在VideoDecoder类中.

编译的时候需要修改Makefile里面ffmpeg和sdl2的路径,然后make编译完成之后用下面命令即可播放视频:

demo -p 视频路径播放视频

PS:

某些函数会有数字后缀,如avcodec_alloc_context3、avcodec_open2等,实际上这个数字后缀是这个函数的第几个版本的意思,从源码的doc/APIchanges可以看出来:

2011-07-10 - 3602ad7 / 0b950fe - lavc 53.8.0
  Add avcodec_open2(), deprecate avcodec_open().
  NOTE: this was backported to 0.7
​
  Add avcodec_alloc_context3. Deprecate avcodec_alloc_context() and
  avcodec_alloc_context2().

 

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/30245.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

(200,10)和(10,)的ndarray数组怎么计算内积,得到的是什么维度?

今天在定义一个内积运算的时候&#xff0c;发现了一个模糊的问题&#xff0c;两个变量&#xff0c;都是ndarray数组&#xff0c;一个维度是(200,10)&#xff0c;一个维度是(10,)&#xff0c;两者求内积后得到的新数组的维度是(200,)&#xff0c;该如何理解&#xff1f; 一、数…

你给我解释解释,什么TMD叫TMD attention(持续更新ing...)

诸神缄默不语-个人CSDN博文目录 开宗明义&#xff1a;attention就是一个加权机制&#xff0c;但是权重需要经过训练得到。 本文介绍一些我自己整理的常见attention类型。 本文不关注别的博文里已经写烂了的attention机制的重要性和直觉解释&#xff08;又不是写论文还要写int…

SpringBoot项目调用Matlab方法

SpringBoot项目调用Matlab方法需求环境准备步骤Matlab方面java方面结果需求 java调用matlab的函数 环境准备 Matlab&#xff0c;jdk&#xff0c;maven管理的springboot项目 步骤 Matlab方面 准备一个简单函数 function [x,y,z] jarDemo(arg) disp("hello world fr…

JVM运行数据区深度解析

运行数据区 字节码只是一个二进制文件存放在那里。要想在jvm里跑起来&#xff0c;先得有个运行的内存环境。 也就是我们所说的jvm运行时数据区。 1&#xff09;运行时数据区的位置 运行时数据区是jvm中最为重要的部分&#xff0c;执行引擎频繁操作的就是它。类的初始化&…

IPD-产品需求管理过程(2)

1、需求分析方法 需求分析分为需求解释、需求过滤、需求分类、需求排序四步。 SEG&#xff1a;system engineering group,意为系统工程组 其中&#xff0c;需求解释环节要完成客户需求语言向内部规范描述语言的转换&#xff1b;需求过滤环节要去伪存真、去粗取精&#x…

C++11 异步操作future和aysnc

目录 C11异步操作的4个接口 1. std::aysnc和std::future std::future和std::aysnc的使用Demo 2. std::packaged_task std::packaged_task的使用Demo 3. std::promise std::promise的使用Demo 总结 C/CLinux服务器开发/后台架构师【零声教育】-学习视频教程-腾讯课堂 C1…

STM32F407高级定时器-死区时间研究-STM32CubeMX

距离上次写笔记&#xff0c;已经过去好长时间了 中间也折腾过不少东西&#xff0c;但是都没咋整理&#xff0c;主要是这中间都是在干活儿&#xff0c;不是自己想要研究的&#xff0c;也没想着要写。 从去年10月份开始想要学习FOC&#xff0c;10月份研究了一个月&#xff0c;到…

HTML5学习笔记(一)

XHTML&#xff0c;全称“EXtensible HyperText Mark-up Language&#xff08;扩展的超文本标记语言&#xff09;”&#xff0c;它是XML风格的HTML4.01&#xff0c;我们可以称之为更严格、更纯净的HTML4.01。 HTML语法书写比较松散&#xff0c;比较利于开发者编写。但是对于机器…

计算机的种类

文章目录计算机的种类一&#xff0c;模拟计算机二&#xff0c;数字计算机三&#xff0c;混合计算机计算机的种类 《计算机是什么》一节中讲到&#xff0c;根据不同计算机的尺寸&#xff0c;可以将计算机分为微型计算机、小型计算机、大型计算机、工作站和超级计算机5类。实际上…

string类(一)

目录 一、 string类对象的常见构造 二、string类对象的容量操作 2.1 size(返回字符串有效字符长度) 2.2 capacity(返回空间总大小) 2.3 reserve扩空间​编辑 2.4 resize初始化不会覆盖本来的空间​编辑 2.5 对于test_string7中每一句代码进行调试运行 三、string类对象的…

云中网络的隔离:GRE、VXLAN

对于云平台中的隔离问题&#xff0c;前面咱们用的策略一直都是 VLAN&#xff0c;但是我们也说过这种策略的问题&#xff0c;VLAN 只有 12 位&#xff0c;共 4096 个。当时设计的时候&#xff0c;看起来是够了&#xff0c;但是现在绝对不够用&#xff0c;怎么办呢&#xff1f; …

真的,Java并发编程基础入门看这个就够了

Java并发编程学习之02Java并发编程入门指南 真的&#xff0c;Java并发编程入门看这个就够了1. Java天生多线程2. Java启动多线程实现方式2.1 实现代码2.2 Thread和Runnable的区别2.3 start和run方法的区别3. Java如何停止线程呢3.1 已弃用方法3.2 推荐使用4. 守护线程5. 优先级…

JVM的GC算法CMS和G1

GC算法 -XX:UseSerialGC 新生代和老年代都使用串行收集器 串行收集器使用单线程并且是独占式的垃圾回收 -XX:UseParNewGC 新生代使用ParNew垃圾回收器&#xff0c;老年代使用串行收集器 ParNew是串行收集器的多线程版本&#xff0c;只工作在新生代&#xff08;可以见名知…

jmeter逻辑控制器和定时器

常用逻辑控制器和定时器一、认识逻辑控制器一、作用&#xff1a;⼀个事务会包含并请求二、常见逻辑控制器介绍1、simple controller2、recorder controller3、loop controller4、random controller5、if controller6、module/include controller7、transaction controller三、J…

A Survey on Bias and Fairness in Machine Learning 阅读笔记

论文链接 Section 1 引言 1. In the context of decision-making, fairness is the absence of any prejudice or favoritism toward an individual or group based on their inherent or acquired characteristics. 公平是指基于个人或群体的固有或后天特征而对其没有任何偏…

【sfu】rtc 入口

rtc 入口 入口是 rtc adpter 类。 准备 call模块的所有资源 通过call模块使用webrtc内置 各类 rtcadpter的创建类rtcadpterfactory 是外部创建的 创建adapterfactory的外部类是peerconnection adapter_factory_ = std::move(std::make_unique

融云全球社交泛娱乐洞察,互联网社交换挡期的「社区产品」机遇

一切应用都将社交化。关注【融云全球互联网通信云】回复【融云】抽取高颜值大容量高端可乐保温杯哦~ 融入社交能力&#xff0c;创造增长奇迹。激活用户在不同场景的社交需求&#xff0c;让应用焕发新的生命力&#xff0c;也让“社交X”成为出海最大的浪。 《2022 社交泛娱乐出…

编译原理--基本块的划分

基本块的划分原则有4条 初始语句作为第一个基本块的入口遇到标号类语句&#xff0c;结束当前基本块&#xff0c;标号作为新基本块的入口&#xff08;标号不在当前基本块中&#xff0c;而是划到下一个基本块&#xff09;遇到转移类语句时&#xff0c;结束当前当前基本块&#x…

Java反序列化之CommonsCollections(CC1)分析篇

文章目录前言一、过程分析 1.入口点---危险方法InvokerTransformer.transform() 2.触发危险函数TransformedMap.checkSetValue() 3.AnnotationInvocationHandler类 4.ChainedTransformer类 5.ChainedTransformer类前言 本文包括&#xff1a;Java反序列化之CommonsCol…

3DMAX一键生成螺母和螺栓插件使用教程

插件介绍 3DMAX一键生成螺母和螺栓插件&#xff0c;用于创建精缰化的螺母和螺栓模型。这些模型是逼真的&#xff0c;只需单击一下即可生成。有许多参数可以定制模型的外观或尺寸。 主要功能 单击创建螺母、螺栓和垫圈在螺栓顶部创建文本标记&#xff08;商标&#xff09;直径…