热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

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

老人们经常说,播放器对音频和视频的播放没有绝对的静态的同步,只有

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

音视频的同步方式有 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 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;
}

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);//ms
av_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);//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->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);//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->SetVideoRender(m_AudioRender);
//设置音频时间戳回调
m_VideoDecoder->SetAVSyncCallback(m_AudioDecoder, AudioDecoder::GetAudioDecoderTimestampForAVSync);

5结语

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


推荐阅读
  • 时域|波形_语音处理基于matlab GUI音频数据处理含Matlab源码 1734期
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了语音处理基于matlabGUI音频数据处理含Matlab源码1734期相关的知识,希望对你有一定的参考价值。 ... [详细]
  • PHP图片截取方法及应用实例
    本文介绍了使用PHP动态切割JPEG图片的方法,并提供了应用实例,包括截取视频图、提取文章内容中的图片地址、裁切图片等问题。详细介绍了相关的PHP函数和参数的使用,以及图片切割的具体步骤。同时,还提供了一些注意事项和优化建议。通过本文的学习,读者可以掌握PHP图片截取的技巧,实现自己的需求。 ... [详细]
  • 开发笔记:加密&json&StringIO模块&BytesIO模块
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了加密&json&StringIO模块&BytesIO模块相关的知识,希望对你有一定的参考价值。一、加密加密 ... [详细]
  • 计算机存储系统的层次结构及其优势
    本文介绍了计算机存储系统的层次结构,包括高速缓存、主存储器和辅助存储器三个层次。通过分层存储数据可以提高程序的执行效率。计算机存储系统的层次结构将各种不同存储容量、存取速度和价格的存储器有机组合成整体,形成可寻址存储空间比主存储器空间大得多的存储整体。由于辅助存储器容量大、价格低,使得整体存储系统的平均价格降低。同时,高速缓存的存取速度可以和CPU的工作速度相匹配,进一步提高程序执行效率。 ... [详细]
  • Java中包装类的设计原因以及操作方法
    本文主要介绍了Java中设计包装类的原因以及操作方法。在Java中,除了对象类型,还有八大基本类型,为了将基本类型转换成对象,Java引入了包装类。文章通过介绍包装类的定义和实现,解答了为什么需要包装类的问题,并提供了简单易用的操作方法。通过本文的学习,读者可以更好地理解和应用Java中的包装类。 ... [详细]
  • GetWindowLong函数
    今天在看一个代码里头写了GetWindowLong(hwnd,0),我当时就有点费解,靠,上网搜索函数原型说明,死活找不到第 ... [详细]
  • 本文介绍了在开发Android新闻App时,搭建本地服务器的步骤。通过使用XAMPP软件,可以一键式搭建起开发环境,包括Apache、MySQL、PHP、PERL。在本地服务器上新建数据库和表,并设置相应的属性。最后,给出了创建new表的SQL语句。这个教程适合初学者参考。 ... [详细]
  • 本文介绍了设计师伊振华受邀参与沈阳市智慧城市运行管理中心项目的整体设计,并以数字赋能和创新驱动高质量发展的理念,建设了集成、智慧、高效的一体化城市综合管理平台,促进了城市的数字化转型。该中心被称为当代城市的智能心脏,为沈阳市的智慧城市建设做出了重要贡献。 ... [详细]
  • 本文介绍了OC学习笔记中的@property和@synthesize,包括属性的定义和合成的使用方法。通过示例代码详细讲解了@property和@synthesize的作用和用法。 ... [详细]
  • eclipse学习(第三章:ssh中的Hibernate)——11.Hibernate的缓存(2级缓存,get和load)
    本文介绍了eclipse学习中的第三章内容,主要讲解了ssh中的Hibernate的缓存,包括2级缓存和get方法、load方法的区别。文章还涉及了项目实践和相关知识点的讲解。 ... [详细]
  • 本文介绍了操作系统的定义和功能,包括操作系统的本质、用户界面以及系统调用的分类。同时还介绍了进程和线程的区别,包括进程和线程的定义和作用。 ... [详细]
  • Android自定义控件绘图篇之Paint函数大汇总
    本文介绍了Android自定义控件绘图篇中的Paint函数大汇总,包括重置画笔、设置颜色、设置透明度、设置样式、设置宽度、设置抗锯齿等功能。通过学习这些函数,可以更好地掌握Paint的用法。 ... [详细]
  • Todayatworksomeonetriedtoconvincemethat:今天在工作中有人试图说服我:{$obj->getTableInfo()}isfine ... [详细]
  • 上图是InnoDB存储引擎的结构。1、缓冲池InnoDB存储引擎是基于磁盘存储的,并将其中的记录按照页的方式进行管理。因此可以看作是基于磁盘的数据库系统。在数据库系统中,由于CPU速度 ... [详细]
  • keras归一化激活函数dropout
    激活函数:1.softmax函数在多分类中常用的激活函数,是基于逻辑回归的,常用在输出一层,将输出压缩在0~1之间,且保证所有元素和为1,表示输入值属于每个输出值的概率大小2、Si ... [详细]
author-avatar
loring8
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有