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

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

1播放器结构

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

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

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

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

class DecoderBase : public Decoder {public:DecoderBase(){};virtual~ DecoderBase(){};//开始播放virtual void Start();//暂停播放virtual void Pause();//停止virtual void Stop();//获取时长virtual float GetDuration(){//ms to sreturn 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;//总时长 mslong             m_Duration = 0;//数据流索引int              m_StreamIndex = -1;//锁和条件变量mutex               m_Mutex;condition_variable  m_Cond;thread             *m_Thread = nullptr;//seek positionvolatile 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;
}

2音视频向系统时钟同步

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

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

音视频向系统时钟同步。

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);//msav_usleep(sleepTime * 1000);}
}

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

3音频向视频同步

音频向视频同步,就是音频的时间戳向视频的时间戳对齐。由于视频有固定的刷新频率,即 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);//msav_usleep(sleepTime * 1000);}}
}

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

//创建解码器
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);

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

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

4视频向音频同步

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

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

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);//msav_usleep(sleepTime * 1000);}}
}

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

//创建解码器
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);

5结语

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

音视频从入门到精通——FFmpeg 播放器实现音视频同步的三种方式相关推荐

  1. 【Linux入门到精通系列讲解】Centos 7软件安装的三种方式

    centos 软件安装的三种方式 Linux下面安装软件的常见方法: 一.yum 替你下载软件 替你安装 替你解决依赖关系 点外卖 缺少的东西 外卖解决 1.方便 简单 2.没有办法深入修改 yum ...

  2. 音视频从入门到精通——FFmpeg之swr_convert音频重采样函数分析

    文章目录 音频重采样 swr_alloc函数 swr_alloc_set_opts函数 swr_init函数 swr_convert函数 音频基础 音频开发主要应用有 音频开发具体内容有 音频应用的难 ...

  3. 音视频从入门到精通——FFmpeg数据结构分析

    FFmpeg数据结构分析 FFmpeg解码流程 重要结构体之间的关系 AVFormatContext iformat:输入媒体的AVInputFormat,比如指向AVInputFormat ff_f ...

  4. 音视频从入门到精通——ffmpeg3之打印多媒体文件音视频信息

    ffmpeg3之打印多媒体文件音视频信息 av_dump_format函数 /*** Print detailed information about the input or output form ...

  5. 在抖音APP源码中如何实现播放器的音视频同步

    在抖音APP源码中音频和视频的播放是在不同线程中进行的,而且音频和视频都有自己的时间戳,所以需要同步机制保障音画同步. 抖音APP源码有多种机制可以做到音视频同步:a. 音频同步于视频.b. 视频同步 ...

  6. android 存放音频文件夹里,Android 实现简单的音乐播放器效果(音频文件的三种存放)...

    Android 实现简单的音乐播放器效果(音频文件的三种存放).三种方法主要使用到的类 MediaPlayer.create() getAssets() new Mediaplayer() - 几个控 ...

  7. 音视频从入门到精通——FFmpeg分离出PCM数据实战

    什么是PCM? PCM(Pulse Code Modulation,脉冲编码调制)音频数据是未经压缩的音频采样数据裸流,它是由模拟信号经过采样.量化.编码转换成的标准数字音频数据. 描述PCM数据的6 ...

  8. spring入门:beans.xml不提示、别名、创建对象的三种方式

    spring的版本是2.5 一.beans.xml文件不提示 Location:spring-framework-2.5.6.SEC01\dist\resources\spring-beans-2.5 ...

  9. 3 FFmpeg从入门到精通-FFmpeg转封装

    1 FFmpeg从入门到精通-FFmpeg简介 2 FFmpeg从入门到精通-FFmpeg工具使用基础 3 FFmpeg从入门到精通-FFmpeg转封装 4 FFmpeg从入门到精通-FFmpeg转码 ...

最新文章

  1. this调用语句必须是构造函数中的第一个可执行语句_谈谈JavaScript中的函数构造式和new关键字...
  2. 【深度学习】基于Pytorch的softmax回归问题辨析和应用(一)
  3. 解密汽车全景行车安全系统的前世和今生——第二讲:原理讲解
  4. 大象转身,地表最强投行高盛开启转型之路
  5. 【项目管理】项目问题诊断
  6. (转)Virtual PC 2007虚拟网络设置
  7. mysql 查询排行_通过mysql查询排行榜
  8. java不同进程的相互唤醒_Java线程生命周期与状态切换
  9. android 布局防抖动,Android全屏返回布局抖动问题
  10. 如何在golang代码里面解析容器镜像
  11. LFFD 再升级!新增行人和人头检测模型,还有了优化的C++实现
  12. java栈的底层实现_JVM 底层原理总结
  13. abaqus python二次开发攻略_Python 进行 Abaqus 二次开发的基础知识
  14. 2015.12.24 OC中的装箱
  15. VirtualBox装VBoxGuestAdditions增强工具失败
  16. PDF有限制不能编辑怎么办?
  17. SDN概述,SDN是什么?
  18. 北斗卫星轨道有哪些?
  19. MySQL中的文本处理函数整理,收藏速查
  20. 计算机综合布线考试试题A,综合布线试题A

热门文章

  1. [EVO 3D GPS导航] 完美解决凯立德搜不到星问题
  2. 【php毕业设计】基于php+mysql+apache的教材管理系统设计与实现(毕业论文+程序源码)——教材管理系统
  3. 设计模式学习(四):基于Builder模式的歌词解析器
  4. 浙江大学远程教育计算机应用基础,浙江大学远程教育计算机应用基础2014年秋-2Windows知识题详细分解.docx...
  5. java 字符串是类名.class 如何实例化_根据类名字符串实例化类,并调用类的方法或函数 转...
  6. 端游吃鸡测试服服务器维护,《绝地求生大逃杀》PC版今日服务器更新维护 上线测试服内容!...
  7. win2003下安装不了Inter945g显卡驱动的问题解决
  8. 注册资本和公司章程有关系吗
  9. CAD多线怎么修剪多余部分?CAD多线修剪步骤
  10. 微信域名防封技术常见问题及解答