FFmpeg简易播放器的实现-音视频同步

本文详细介绍了基于FFmpeg和SDL实现简易播放器,特别是音视频同步的实现过程。通过分析音视频同步的重要性,解码过程,以及视频同步到音频的策略,阐述了播放器中音视频同步的关键技术。实验中,作者对ffplay源码进行删减,保留核心的音视频同步部分,最终实现了一个可实际使用的简易播放器。
摘要由CSDN通过智能技术生成

本文为作者原创,转载请注明出处:https://www.cnblogs.com/leisure_chn/p/10284653.html

基于FFmpeg和SDL实现的简易视频播放器,主要分为读取视频文件解码和调用SDL显示两大部分。
FFmpeg简易播放器系列文章如下:
[1]. FFmpeg简易播放器的实现-最简版
[2]. FFmpeg简易播放器的实现-视频播放
[3]. FFmpeg简易播放器的实现-音频播放
[4]. FFmpeg简易播放器的实现-音视频播放
[5]. FFmpeg简易播放器的实现-音视频同步
前面四次实验,从最简入手,循序渐进,研究播放器的实现过程。第四次实验,虽然音频和视频都能播放出来,但是声音和图像无法同步,而没有音视频同步的播放器只是属于概念性质的播放器,无法实际使用。本次实验将实现音频和视频的同步,这样,一个能够实际使用的简易播放器才算初具雏形,在这个基础上,后续可再进行完善和优化。

音视频同步是播放器中比较复杂的一部分内容。前几次实验中的代码远不能满足要求,需要大幅修改。本次实验不在前几次代码上修改,而是基于ffplay源码进行修改。ffplay是FFmpeg工程自带的一个简单播放器,尽管称为简单播放器,其代码实现仍显得过为复杂,本实验对ffplay.c进行删减,删掉复杂的命令选项、滤镜操作、SEEK操作、逐帧插放等功能,仅保留最核心的音视频同步部分。

尽管不使用之前的代码,但播放器的基本原理和大致流程相同,前面几次实验仍具有有效参考价值。

1. 视频播放器基本原理

下图引用自“雷霄骅,视音频编解码技术零基础学习方法”,因原图太小,看不太清楚,故重新制作了一张图片。
播放器基本原理示意图
如下内容引用自“雷霄骅,视音频编解码技术零基础学习方法”:

解协议
将流媒体协议的数据,解析为标准的相应的封装格式数据。视音频在网络上传播的时候,常常采用各种流媒体协议,例如HTTP,RTMP,或是MMS等等。这些协议在传输视音频数据的同时,也会传输一些信令数据。这些信令数据包括对播放的控制(播放,暂停,停止),或者对网络状态的描述等。解协议的过程中会去除掉信令数据而只保留视音频数据。例如,采用RTMP协议传输的数据,经过解协议操作后,输出FLV格式的数据。

解封装
将输入的封装格式的数据,分离成为音频流压缩编码数据和视频流压缩编码数据。封装格式种类很多,例如MP4,MKV,RMVB,TS,FLV,AVI等等,它的作用就是将已经压缩编码的视频数据和音频数据按照一定的格式放到一起。例如,FLV格式的数据,经过解封装操作后,输出H.264编码的视频码流和AAC编码的音频码流。

解码
将视频/音频压缩编码数据,解码成为非压缩的视频/音频原始数据。音频的压缩编码标准包含AAC,MP3,AC-3等等,视频的压缩编码标准则包含H.264,MPEG2,VC-1等等。解码是整个系统中最重要也是最复杂的一个环节。通过解码,压缩编码的视频数据输出成为非压缩的颜色数据,例如YUV420P,RGB等等;压缩编码的音频数据输出成为非压缩的音频抽样数据,例如PCM数据。

音视频同步
根据解封装模块处理过程中获取到的参数信息,同步解码出来的视频和音频数据,并将视频音频数据送至系统的显卡和声卡播放出来。

2. 简易播放器的实现-音视频同步

2.1 实验平台

实验平台:  openSUSE Leap 42.3
            Microsoft Visual Studio 2017 (WIN10)  
FFmpeg版本:4.1  
SDL版本:   2.0.9  

本工程支持在Linux和Windows平台上运行。
Linux下FFmpeg开发环境搭建可参考“FFmpeg开发环境构建”。
Windows下使用Microsoft Visual Studio 2017打开工程目录下“ffplayer.sln”文件即可运行。

2.2 源码清单

使用如下命令下载源码:

git clone https://github.com/leichn/ffplayer.git

ffplay所有源码集中在ffplay.c一个文件中,ffplay.c篇幅过长。本实验将ffplay.c按功能点拆分为多个文件,源文件说明如下:

player.c    运行主线程,SDL消息处理
demux.c     解复用线程
video.c     视频解码线程和视频播放线程
audio.c     音频解码线程和音频播放线程
packet.c    packet队列操作函数
frame.c     frame队列操作函数
main.c      程序入口,外部调用示例
Makefile    Linux平台下编译用Makefile
lib_wins    Windows平台下FFmpeg和SDL编译时库和运行时库

本来想将ffplay.c中全局使用的大数据结构VideoState也拆分分散到各文件中去,但发现各文件对数据的引用关系错综复杂,很难拆分,因此作罢。

2.3 源码流程分析

源码流程和ffplay基本相同,不同的一点是ffplay中视频播放和SDL消息处理都是在同一个线程中(主线程),本工程中将视频播放独立为一个线程。
FFmpeg简易播放器流程图

2.4 音视频同步

音视频同步的详细介绍可参考“ffplay源码分析4-音视频同步”,为保证文章的完整性,本文保留此节内容。与“ffplay源码分析4-音视频同步”相比,本节源码及文字均作了适当精简。

音视频同步的目的是为了使播放的声音和显示的画面保持一致。视频按帧播放,图像显示设备每次显示一帧画面,视频播放速度由帧率确定,帧率指示每秒显示多少帧;音频按采样点播放,声音播放设备每次播放一个采样点,声音播放速度由采样率确定,采样率指示每秒播放多少个采样点。如果仅仅是视频按帧率播放,音频按采样率播放,二者没有同步机制,即使最初音视频是基本同步的,随着时间的流逝,音视频会逐渐失去同步,并且不同步的现象会越来越严重。这是因为:一、播放时间难以精确控制,二、异常及误差会随时间累积。所以,必须要采用一定的同步策略,不断对音视频的时间差作校正,使图像显示与声音播放总体保持一致。

音视频同步的方式基本是确定一个时钟(音频时钟、视频时钟、外部时钟)作为主时钟,非主时钟的音频或视频时钟为从时钟。在播放过程中,主时钟作为同步基准,不断判断从时钟与主时钟的差异,调节从时钟,使从时钟追赶(落后时)或等待(超前时)主时钟。按照主时钟的不同种类,可以将音视频同步模式分为如下三种:
音频同步到视频,视频时钟作为主时钟。
视频同步到音频,音频时钟作为主时钟。
音视频同步到外部时钟,外部时钟作为主时钟。
本实验采用ffplay默认的同步方式:视频同步到音频
ffplay中同步模式的定义如下:

enum {
   
    AV_SYNC_AUDIO_MASTER, /* default choice */
    AV_SYNC_VIDEO_MASTER,
    AV_SYNC_EXTERNAL_CLOCK, /* synchronize to an external clock */
};
2.4.1 time_base

time_base是PTS和DTS的时间单位,也称时间基。
不同的封装格式time_base不一样,转码过程中的不同阶段time_base也不一样。
以mpegts封装格式为例,假设视频帧率25FPS为。编码数据包packet(数据结构AVPacket)对应的time_base为AVRational{1,90000}。原始数据帧frame(数据结构AVFrame)对应的time_base为AVRational{1,25}。在解码或播放过程中,我们关注的是frame的time_base,定义在AVStream结构体中,其表示形式AVRational{1,25}是一个分数,值为1/25,单位是秒。在旧的FFmpeg版本中,AVStream中的time_base成员有如下注释:

For fixed-fps content, time base should be 1/framerate and timestamp increments should be 1.

当前新版本中已无此条注释。

2.4.2 PTS/DTS/解码过程

DTS(Decoding Time Stamp, 解码时间戳),表示packet的解码时间。
PTS(Presentation Time Stamp, 显示时间戳),表示packet解码后数据的显示时间。
音频中DTS和PTS是相同的。视频中由于B帧需要双向预测,B帧依赖于其前和其后的帧,因此含B帧的视频解码顺序与显示顺序不同,即DTS与PTS不同。当然,不含B帧的视频,其DTS和PTS是相同的。

解码顺序和显示顺序相关的解释可参考“视频编解码基础概念”,选用下图说明视频流解码顺序和显示顺序

解码和显示顺序

理解了含B帧视频流解码顺序与显示顺序的不同,才容易理解视频解码函数video_decode_frame()中对视频解码的处理:
avcodec_send_packet()按解码顺序发送packet。
avcodec_receive_frame()按显示顺序输出frame。
这个过程由解码器处理,不需要用户程序费心。
video_decode_frame()是非常核心的一个函数,实现如下:

// 从packet_queue中取一个packet,解码生成frame
static int video_decode_frame(AVCodecContext *p_codec_ctx, packet_queue_t *p_pkt_queue, AVFrame *frame)
{
   
    int ret;
    
    while (1)
    {
   
        AVPacket pkt;

        while (1)
        {
   
            // 3. 从解码器接收frame
            // 3.1 一个视频packet含一个视频frame
            //     解码器缓存一定数量的packet后,才有解码后的frame输出
            //     frame输出顺序是按pts的顺序,如IBBPBBP
            //     frame->pkt_pos变量是此frame对应的packet在视频文件中的偏移地址,值同pkt.pos
            ret = avcodec_receive_frame(p_codec_ctx, frame);
            if (ret < 0)
            {
   
                if (ret == AVERROR_EOF)
                {
   
                    av_log(NULL, AV_LOG_INFO, "video avcodec_receive_frame(): the decoder has been fully flushed\n");
                    avcodec_flush_buffers(p_codec_ctx);
                    return 0;
                }
                else if (ret == AVERROR(EAGAIN))
                {
   
                    av_log(NULL, AV_LOG_INFO, "video avcodec_receive_frame(): output is not available in this state - "
                            "user must try to send new input\n");
                    break;
               
  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值