FFmpeg 播放器实现音视频同步的三种方式

这篇具有很好参考价值的文章主要介绍了FFmpeg 播放器实现音视频同步的三种方式。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

前言

我们基于 FFmpeg 利用 OpenGL ES 和 OpenSL ES 分别实现了对解码后视频和音频的渲染,本文将实现播放器的最后一个重要功能:音视频同步。

老人们经常说,播放器对音频和视频的播放没有绝对的静态的同步,只有相对的动态的同步,实际上音视频同步就是一个“你追我赶”的过程。

音视频的同步方式有 3 种,即:音视频分别向系统时钟同步、音频向视频同步及视频向音频同步

效果图

FFmpeg 播放器实现音视频同步的三种方式

播放器结构

在实现音视频同步之前,我们先简单说下本文播放器的大致结构,方便后面实现不同的音视频同步方式。

FFmpeg 播放器实现音视频同步的三种方式

如上图所示,音频解码和视频解码分别占用一个独立线程,线程里有一个解码循环,解码循环里不断对音视频编码数据进行解码,音视频解码帧不设置缓存 Buffer , 进行实时渲染,极大地方便了音视频同步的实现。

音视频解码线程独立分离的播放器模式,简单灵活,代码量小,面向初学者,可以很方便实现音视频同步。

音视和视频解码流程非常相似,所以我们可以将二者的解码器抽象为一个基类:


class DecoderBase : public Decoder {
public:
    DecoderBase()
    {};
    virtual~ DecoderBase()
    {};
    //开始播放
    virtual void Start();
    //暂停播放
    virtual void Pause();
    //停止
    virtual void Stop();
    //获取时长
    virtual float GetDuration()
    {
        //ms to s
        return m_Duration * 1.0f / 1000;
    }
    //seek 到某个时间点播放
    virtual void SeekToPosition(float position);
    //当前播放的位置,用于更新进度条和音视频同步
    virtual float GetCurrentPosition();
    virtual void ClearCache()
    {};
    virtual void SetMessageCallback(void* context, MessageCallback callback)
    {
        m_MsgContext = context;
        m_MsgCallback = callback;
    }
    //设置音视频同步的回调
    virtual void SetAVSyncCallback(void* context, AVSyncCallback callback)
    {
        m_AVDecoderContext = context;
        m_AudioSyncCallback = callback;
    }

protected:
    //解码数据的回调
    virtual void OnFrameAvailable(AVFrame *frame) = 0;

    AVCodecContext *GetCodecContext() {
        return m_AVCodecContext;
    }
private:
    int InitFFDecoder();
    void UnInitDecoder();
    //启动解码线程
    void StartDecodingThread();
    //音视频解码循环
    void DecodingLoop();
    //更新显示时间戳
    void UpdateTimeStamp();
    //音视频同步
    void AVSync();
    //解码一个packet编码数据
    int DecodeOnePacket();
    //线程函数
    static void DoAVDecoding(DecoderBase *decoder);

    //封装格式上下文
    AVFormatContext *m_AVFormatContext = nullptr;
    //解码器上下文
    AVCodecContext  *m_AVCodecContext = nullptr;
    //解码器
    AVCodec         *m_AVCodec = nullptr;
    //编码的数据包
    AVPacket        *m_Packet = nullptr;
    //解码的帧
    AVFrame         *m_Frame = nullptr;
    //数据流的类型
    AVMediaType      m_MediaType = AVMEDIA_TYPE_UNKNOWN;
    //文件地址
    char       m_Url[MAX_PATH] = {0};
    //当前播放时间
    long             m_CurTimeStamp = 0;
    //播放的起始时间
    long             m_StartTimeStamp = -1;
    //总时长 ms
    long             m_Duration = 0;
    //数据流索引
    int              m_StreamIndex = -1;
    //锁和条件变量
    mutex               m_Mutex;
    condition_variable  m_Cond;
    thread             *m_Thread = nullptr;
    //seek position
    volatile float      m_SeekPosition = 0;
    volatile bool       m_SeekSuccess = false;
    //解码器状态
    volatile int  m_DecoderState = STATE_UNKNOWN;
    void* m_AVDecoderContext = nullptr;
    AVSyncCallback m_AudioSyncCallback = nullptr;//用作音视频同步
};

篇幅有限,代码贴多了容易导致视觉疲劳,完整实现代码见阅读原文,这里只贴出几个关键函数。

解码循环。


void DecoderBase::DecodingLoop() {
    LOGCATE("DecoderBase::DecodingLoop start, m_MediaType=%d", m_MediaType);
    {
        std::unique_lock<std::mutex> lock(m_Mutex);
        m_DecoderState = STATE_DECODING;
        lock.unlock();
    }

    for(;;) {
        while (m_DecoderState == STATE_PAUSE) {
            std::unique_lock<std::mutex> lock(m_Mutex);
            LOGCATE("DecoderBase::DecodingLoop waiting, m_MediaType=%d", m_MediaType);
            m_Cond.wait_for(lock, std::chrono::milliseconds(10));
            m_StartTimeStamp = GetSysCurrentTime() - m_CurTimeStamp;
        }

        if(m_DecoderState == STATE_STOP) {
            break;
        }

        if(m_StartTimeStamp == -1)
            m_StartTimeStamp = GetSysCurrentTime();

        if(DecodeOnePacket() != 0) {
            //解码结束,暂停解码器
            std::unique_lock<std::mutex> lock(m_Mutex);
            m_DecoderState = STATE_PAUSE;
        }
    }
    LOGCATE("DecoderBase::DecodingLoop end");
}

获取当前时间戳。


void DecoderBase::UpdateTimeStamp() {
    LOGCATE("DecoderBase::UpdateTimeStamp");
    //参照 ffplay 
    std::unique_lock<std::mutex> lock(m_Mutex);
    if(m_Frame->pkt_dts != AV_NOPTS_VALUE) {
        m_CurTimeStamp = m_Frame->pkt_dts;
    } else if (m_Frame->pts != AV_NOPTS_VALUE) {
        m_CurTimeStamp = m_Frame->pts;
    } else {
        m_CurTimeStamp = 0;
    }

    m_CurTimeStamp = (int64_t)((m_CurTimeStamp * av_q2d(m_AVFormatContext->streams[m_StreamIndex]->time_base)) * 1000);

}

解码一个 packet 的编码数据。


int DecoderBase::DecodeOnePacket() {
    int result = av_read_frame(m_AVFormatContext, m_Packet);
    while(result == 0) {
        if(m_Packet->stream_index == m_StreamIndex) {
            if(avcodec_send_packet(m_AVCodecContext, m_Packet) == AVERROR_EOF) {
                //解码结束
                result = -1;
                goto __EXIT;
            }

            //一个 packet 包含多少 frame?
            int frameCount = 0;
            while (avcodec_receive_frame(m_AVCodecContext, m_Frame) == 0) {
                //更新时间戳
                UpdateTimeStamp();
                //同步
                AVSync();
                //渲染
                LOGCATE("DecoderBase::DecodeOnePacket 000 m_MediaType=%d", m_MediaType);
                OnFrameAvailable(m_Frame);
                LOGCATE("DecoderBase::DecodeOnePacket 0001 m_MediaType=%d", m_MediaType);
                frameCount ++;
            }
            LOGCATE("BaseDecoder::DecodeOneFrame frameCount=%d", frameCount);
            //判断一个 packet 是否解码完成
            if(frameCount > 0) {
                result = 0;
                goto __EXIT;
            }
        }
        av_packet_unref(m_Packet);
        result = av_read_frame(m_AVFormatContext, m_Packet);
    }

__EXIT:
    av_packet_unref(m_Packet);
    return result;
}

音视频向系统时钟同步

音视频向系统时钟同步,顾名思义,系统时钟的更新是按照时间的增加而增加,获取音视频解码帧时与系统时钟进行对齐操作。

简而言之就是,当前音频或视频播放时间戳大于系统时钟时,解码线程进行休眠,直到时间戳与系统时钟对齐。

音视频向系统时钟同步。


void DecoderBase::AVSync() {
    LOGCATE("DecoderBase::AVSync");
    long curSysTime = GetSysCurrentTime();
    //基于系统时钟计算从开始播放流逝的时间
    long elapsedTime = curSysTime - m_StartTimeStamp;

    //向系统时钟同步
    if(m_CurTimeStamp > elapsedTime) {
        //休眠时间
        auto sleepTime = static_cast<unsigned int>(m_CurTimeStamp - elapsedTime);//ms
        av_usleep(sleepTime * 1000);
    }
}

音视频向系统时钟同步可以最大限度减少丢帧跳帧现象,但是前提是系统时钟不能受其他耗时任务影响。

音频向视频同步

音频向视频同步,就是音频的时间戳向视频的时间戳对齐。由于视频有固定的刷新频率,即 FPS ,我们根据 PFS 确定每帧的渲染时长,然后以此来确定视频的时间戳。

当音频时间戳大于视频时间戳,或者超过一定的阈值,音频播放器一般插入静音帧、休眠或者放慢播放。反之,就需要跳帧、丢帧或者加快音频播放。


void DecoderBase::AVSync() {
    LOGCATE("DecoderBase::AVSync");
    if(m_AVSyncCallback != nullptr) {
        //音频向视频同步,传进来的 m_AVSyncCallback 用于获取视频时间戳
        long elapsedTime = m_AVSyncCallback(m_AVDecoderContext);
        LOGCATE("DecoderBase::AVSync m_CurTimeStamp=%ld, elapsedTime=%ld", m_CurTimeStamp, elapsedTime);

        if(m_CurTimeStamp > elapsedTime) {
            //休眠时间
            auto sleepTime = static_cast<unsigned int>(m_CurTimeStamp - elapsedTime);//ms
            av_usleep(sleepTime * 1000);
        }
    }
}

音频向视频同步时,解码器设置。


//创建解码器
m_VideoDecoder = new VideoDecoder(url);
m_AudioDecoder = new AudioDecoder(url);

//设置渲染器
m_VideoDecoder->SetVideoRender(OpenGLRender::GetInstance());
m_AudioRender = new OpenSLRender();
m_AudioDecoder->SetAudioRender(m_AudioRender);

//设置视频时间戳回调
m_AudioDecoder->SetAVSyncCallback(m_VideoDecoder, VideoDecoder::GetVideoDecoderTimestampForAVSync);

音频向视频同步方式的优点是,视频可以将每一帧播放出来,画面流畅度最优。

但是由于人耳对声音相对眼睛对图像更为敏感,音频在与视频对齐时,插入静音帧、丢帧或者变速播放操作,用户可以轻易察觉,体验较差。

视频向音频同步

视频向音频同步的方式比较常用,刚好利用了人耳朵对声音变化比眼睛对图像变化更为敏感的特点。

音频按照固定的采样率播放,为视频提供对齐基准,当视频时间戳大于音频时间戳时,渲染器不进行渲染或者重复渲染上一帧,反之,进行跳帧渲染。


void DecoderBase::AVSync() {
    LOGCATE("DecoderBase::AVSync");
    if(m_AVSyncCallback != nullptr) {
        //视频向音频同步,传进来的 m_AVSyncCallback 用于获取音频时间戳
        long elapsedTime = m_AVSyncCallback(m_AVDecoderContext);
        LOGCATE("DecoderBase::AVSync m_CurTimeStamp=%ld, elapsedTime=%ld", m_CurTimeStamp, elapsedTime);

        if(m_CurTimeStamp > elapsedTime) {
            //休眠时间
            auto sleepTime = static_cast<unsigned int>(m_CurTimeStamp - elapsedTime);//ms
            av_usleep(sleepTime * 1000);
        }
    }
}

音频向视频同步时,解码器设置。


//创建解码器
m_VideoDecoder = new VideoDecoder(url);
m_AudioDecoder = new AudioDecoder(url);

//设置渲染器
m_VideoDecoder->SetVideoRender(OpenGLRender::GetInstance());
m_AudioRender = new OpenSLRender();
m_AudioDecoder->SetAudioRender(m_AudioRender);

//设置音频时间戳回调
m_VideoDecoder->SetAVSyncCallback(m_AudioDecoder, AudioDecoder::GetAudioDecoderTimestampForAVSync);

结语

播放器实现音视频同步的这三种方式中,选择哪一种方式合适要视具体的使用场景而定,比如你对画面流畅度要求很高,可以选择音频向视频同步;你要单独实现视频或音频播放,直接向系统时钟同步更为方便。文章来源地址https://www.toymoban.com/news/detail-455237.html

编译好的FFmpeg下载地址

FFmpeg_4.3.2支持Android的音视频处理库-Android文档类资源-CSDN文库

到了这里,关于FFmpeg 播放器实现音视频同步的三种方式的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包赞助服务器费用

相关文章

  • Qt之基于QMediaPlayer的音视频播放器(支持常见音视频格式)

    Qt之基于QMediaPlayer的音视频播放器(支持常见音视频格式)

    Qt自带了一个Media Player的例子,如下图所示: 但是运行这个例子机会发现,连最基本的MP4格式视频都播放不了。因为QMediaPlayer是个壳(也可以叫框架),依赖本地解码器,视频这块默认基本上就播放个MP4,甚至连MP4都不能播放,如果要支持其他格式需要下载k-lite或者LAVFilter

    2024年02月02日
    浏览(20)
  • ffplay播放器剖析(6)----音视频同步分析

    ffplay播放器剖析(6)----音视频同步分析

    视频和音频是不同的线程,并且也不会同时解出同一个pts的音视频帧,因此需要音视频同步; 以音频为基准 视频慢了则丢掉部分视频帧(视觉感受就是掉帧) 视频快了就继续渲染上一帧 以视频为基准 音频慢了则加速播放(或者丢帧,丢帧会断音,体验感特别差) 音频快了就放慢数据点

    2024年02月15日
    浏览(12)
  • ffplay播放器剖析(7)----音视频暂停模块分析

    1.通过SDL触发事件调用toggle_pause 2.toggle_pause调用stream_toggle_pause 3.stream_toggle_pause修改暂停变量 这个函数可以知道,如果当前状态是暂停的话,那么就会进入if函数 看一下if函数流程,首先我们知道我们 is-frame_timer += av_gettime_relative() / 1000000.0 - is-vidclk.last_updated; 这个就是在之前的基

    2024年02月16日
    浏览(15)
  • 浏览器网页内嵌Qt-C++音视频播放器的实现,支持软硬解码,支持音频,支持录像截图,支持多路播放等,提供源码工程下载

    浏览器网页内嵌Qt-C++音视频播放器的实现,支持软硬解码,支持音频,支持录像截图,支持多路播放等,提供源码工程下载

        在浏览器中实现播放RTSP实时视频流,⼤体上有如下⼏个⽅案: ⽅案一:浏览器插件⽅案 ActiveX、NPAPI、PPAPI     ActiveX插件适用于IE浏览器,NPAPI与PPAPI插件适用于谷歌浏览器,不过这些插件都已经不被浏览器所支持。 ⽅案二:先转码再转流⽅案     ⼯作原理是架设一

    2024年01月17日
    浏览(50)
  • Qt音视频开发41-文件推流(支持网页和播放器播放并切换进度)

    Qt音视频开发41-文件推流(支持网页和播放器播放并切换进度)

    本功能最初也是有一些人提过类似的需求,就是能不能将本地的音视频文件,通过纯Qt程序推流出去,然后用户可以直接在网页上播放,也可以用各种播放器播放,然后还可以任意切换播放进度,其实说白了就是个文件服务器,用户通过网络地址访问以后,告诉对方当前是媒

    2024年02月01日
    浏览(24)
  • 【MediaPlayerSource】播放器源内部的音视频sender的创建和使用

    来看下声网播放中的sender相关组件设计: MediaPlayerSourceDummy 是一个MediaPlayerSourceImpl ,输入音视频帧到 播放器。

    2024年02月03日
    浏览(15)
  • qt+ffmpeg 实现音视频播放(二)之音频播放

    qt+ffmpeg 实现音视频播放(二)之音频播放

    通过  avformat_open_input () 打开媒体文件并分配和初始化  AVFormatContext   结构体。 函数原型如下: int avformat_open_input(AVFormatContext **ps, const char *url, AVInputFormat *fmt, AVDictionary **options); 参数说明: - `ps`:指向 `AVFormatContext` 结构体指针的指针,用于存储打开的媒体文件的信息。

    2024年04月22日
    浏览(17)
  • FFMpeg-3、基于QT实现音视频播放显示

    FFMpeg-3、基于QT实现音视频播放显示

    1、音视频播放的基础知识 内容来自雷神博客 1、在Windows平台下的视频播放技术主要有以下三种:GDI,Direct3D和OpenGL;音频播放技术主要是DirectSound。 SDL本身并不具有播放显示的功能,它只是封装了底层播放显示的代码 记录三种视频显示技术:GDI,Direct3D,OpenGL。其中Direct3D包

    2024年02月03日
    浏览(15)
  • 音视频开发:ffplay使用ffmpeg滤镜实现倍速播放

    曾经为实现倍速播放使用过ffmpeg,对音频使用atempo滤镜即可实现变速不变调。但是当时效果并不是特别好,和soundtouch相比处理后的音质有明显的区别。最近用新版本的ffmpeg滤镜重新实现了倍速播放,发现效果变好,已经达到可接受的程度,所以在此分享具体实现。 ffmpeg倍速

    2024年02月03日
    浏览(46)
  • FFmpeg参数说明FFmpegAndroid饺子视频播放器

    FFmpegAndroid https://github.com/xufuji456/FFmpegAndroid https://github.com/lipangit/JiaoZiVideoPlayer/tree/develop 饺子视频播放器 \\\'ffmpeg\\\' 不是内部或外部命令,也不是可运行的程序 或批处理文件 http://www.360doc.com/content/21/0204/15/54508727_960674843.shtml 【FFmpeg】ffmpeg 命令查询一 ( 版本 | 编译配置 | 复用格式

    2024年02月11日
    浏览(11)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包