FFmpeg入门 - 视频播放

2023-05-16

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

音视频编解码基础

我们可以通过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().

 

 

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

FFmpeg入门 - 视频播放 的相关文章

随机推荐

  • 打牌(求牌型方案数)

    问题描述 有 A B 张扑克牌 每张扑克牌有一个大小 整数 xff0c 记为a xff0c 范围区间是 0 到 A 1 xff09 和一个花色 xff08 整数 xff0c 记为b xff0c 范围区间是 0 到 B 1 扑克牌是互异的 x
  • 滑动窗口【区间最大值区间&最小值】【单调队列】

    问题描述 ZJM 有一个长度为 n 的数列和一个大小为 k 的窗口 窗口可以在数列上来回移动 现在 ZJM 想知道在窗口从左往右滑的时候 xff0c 每次窗口内数的最大值和最小值分别是多少 例如 xff1a 数列是 1 3 1 3 5 3
  • Q老师的考验【矩阵快速幂】【斐波那契数列】

    问题描述 Q老师 对数列有一种非同一般的热爱 xff0c 尤其是优美的斐波那契数列 这一天 xff0c Q老师 为了增强大家对于斐波那契数列的理解 xff0c 决定在斐波那契的基础上创建一个新的数列 f x 来考一考大家 数列 f x 定义
  • Q老师度假【动态规划dp】【矩阵快速幂优化】

    问题描述 忙碌了一个学期的 Q老师 决定奖励自己 N 天假期 假期中不同的穿衣方式会有不同的快乐值 已知 Q老师 一共有 M 件衬衫 xff0c 且如果昨天穿的是衬衫 A xff0c 今天穿的是衬衫 B xff0c 则 Q老师 今天可以获得
  • 插值算法[数学建模]

    插值 插值算法一维数据插值方法分段线性插值分段二次插值 xff08 分段抛物插值 xff09 拉格朗日插值法龙格现象 牛顿插值法埃尔米特插值法分段三次埃尔米特插值PCHIP 样条插值三次样条插值 n维数据插值应用 插值算法 在工程和数学应用
  • 时间序列【数学建模】

    时间序列 确定性时间序列分析方法移动平均法简单移动平均法加权移动平均法趋势移动平均法 指数平滑法一次指数平滑法二次指数平滑法三次指数平滑法 差分指数平滑法一阶差分指数平滑法二阶差分指数平滑法 具有季节性特点的时间序列的预测 平稳时间序列模型
  • UITabBarController的简单使用和属性方法总结

    一 引言 与导航控制器相类似 xff0c 标签控制器也是用于管理视图控制器的一个UI控件 xff0c 在其内部封装了一个标签栏 xff0c 与导航不同的是 xff0c 导航的管理方式是纵向的 xff0c 采用push与pop切换控制器 xf
  • 多元分析——聚类分析【数学建模】

    聚类分析 Q型聚类分析样本的相似性度量类与类间的相似性度量聚类图最短距离法的聚类举例Matlab 聚类分析的相关命令pdistlinkageclusterzsore X H 61 dendrogram Z P T 61 clusterdat
  • 多元分析——主成分分析【数学建模】

    主成分分析的主要目的是希望用较少的变量去解释原来资料中的大部分变异 xff0c 将许多相关性较高的变量转化为彼此相互独立或不相关的变量 通常是选出的比原始变量个数少 xff0c 能解释大部份资料中的变异的几个新变量 xff0c 即所谓主成分
  • Pytorch Resnet101

    Pytorch Resnet101 模型使用模板 模型 span class token string 34 34 span 34 model span class token operator span span class token
  • t-SNE可视化

    t SNE的全称是t Distributed Stochastic Neighbor Embedding xff0c 是一种降低维度的技术 xff0c 适用于将高维度数据可视化 它将数据点之间的相似性转换为联合概率 xff0c 并试图最小化
  • ubuntu下为apt-get设置代理

    debian mint都可以按此设置 xff08 debian系均可 xff0c 直接基于debian或间接基于debian的 xff09 现在公司很多都是通过代理上网的 xff0c 通过代理上网apt get install无法通网络进行
  • 开源虚拟化集群管理平台ProxmoxVE-安装介绍以及PCI直通和嵌套虚拟化说明

    说明 xff1a 当前测试环境为PVE7 0版本 官网 xff1a Proxmox Powerful open source server solutions 目录 背景 安装系统 PCI直通配置 xff08 开启iommu xff09 允
  • qt Linux arm 交叉编译

    1 操作系统 ubuntu1 20 04 2 qt5 9 9 Index of archive qt 5 9 5 9 9 下载qt源码 qt everywhere opensource src 5 9 9 tar xz 和程序 qt ope
  • 三角函数:图像和性质关系

    紧接上一篇 xff1a http blog csdn net yinhun2012 article details 79377728 这次我们通过函数图文和unity程序来观察三角函数的图像 xff0c 首先从基本的来 xff1a 1 f
  • Linux 每天定时关机 设置

    sudo gedit etc crontab 输入密码后 xff0c 在该文件插入一行 50 02 root sbin shutdown h now 保存后 xff0c 系统会在每天02 50 自动关机
  • anaconda如何配置环境变量

    anaconda安装好后 xff0c 在cmd输入conda xff0c 显示 xff1a conda 不是内部或外部命令 xff0c 也不是可运行的程序 或批处理文件 原因是 xff1a anaconda没有配置环境变量 那接下来我就教大
  • Windows下用 Code blocks + mingw 搭建 Fortran 编译环境

    方法一 xff1a 下载页面 xff1a http www codeblocks org downloads binaries 下载 codeblocks 17 12mingw fortran setup exe 这样的版本 方法二 xff
  • 音频编码格式介绍-AAC

    目录 概述 帧格式 算法简介 开源的软件 AAC和MP3的关键性不同 参考阅读 1 概述 AAC xff08 Advanced Audio Coding xff09 xff0c 被认为是MP3的继任者 xff0c 相对MP3有更高的压缩效率
  • FFmpeg入门 - 视频播放

    音视频最好从能够直接看到东西 也更加贴近用户的播放开始学起 音视频编解码基础 我们可以通过http rtmp或者本地的视频文件去播放视频 这里的 34 视频 34 实际上指的是mp4 avi这种既有音频也有视频的文件格式 这样的视频文件可能