FFmpeg 开发(06):FFmpeg 播放器实现音视频同步的三种方式

て烟熏妆下的殇ゞ 提交于 2020-10-25 12:50:53

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

该文章首发于微信公众号:字节流动

FFmpeg 开发系列连载:

FFmpeg 开发(01):FFmpeg 编译和集成
FFmpeg 开发(02):FFmpeg + ANativeWindow 实现视频解码播放
FFmpeg 开发(03):FFmpeg + OpenSLES 实现音频解码播放
FFmpeg 开发(04):FFmpeg + OpenGLES 实现音频可视化播放
FFmpeg 开发(05):FFmpeg + OpenGLES 实现视频解码播放和视频滤镜



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

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

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

音视频解码器结构

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

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

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

音视和视频解码流程非常相似,所以我们可以将二者的解码器抽象为一个基类:
```c++
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:
void m_MsgContext = nullptr;
MessageCallback m_MsgCallback = nullptr;
virtual int Init(const char

url, AVMediaType mediaType);
virtual void UnInit();
virtual void OnDecoderReady() = 0;
virtual void OnDecoderDone() = 0;
//解码数据的回调
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;//用作音视频同步

};

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

解码循环。
```c++
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");
}

获取当前时间戳。
```c++
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 的编码数据。
```c++
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;
}

音视频向系统时钟同步

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

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

音视频向系统时钟同步。
```c++
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 确定每帧的渲染时长,然后以此来确定视频的时间戳。

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

```c++
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);
        }
    }
}

音频向视频同步时,解码器设置。
```c++
//创建解码器
m_VideoDecoder = new VideoDecoder(url);
m_AudioDecoder = new AudioDecoder(url);



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


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

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

但是由于人耳对声音相对眼睛对图像更为敏感,音频在与视频对齐时,插入静音帧、丢帧或者变速播放操作,用户可以轻易察觉,体验较差。
# 视频向音频同步
视频向音频同步的方式比较常用,刚好利用了人耳朵对声音变化比眼睛对图像变化更为敏感的特点。

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

```c++
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);
        }
    }
}

音频向视频同步时,解码器设置。
```c++
//创建解码器
m_VideoDecoder = new VideoDecoder(url);
m_AudioDecoder = new AudioDecoder(url);



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


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


# 结语
播放器实现音视频同步的这三种方式中,选择哪一种方式合适要视具体的使用场景而定,比如你对画面流畅度要求很高,可以选择音频向视频同步;你要单独实现视频或音频播放,直接向系统时钟同步更为方便。

# 联系与交流 #
技术交流获取源码可以添加我的微信:Byte-Flow
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!