该文章首发于微信公众号:字节流动
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 , 进行实时渲染,极大中央便了音视频同步的实现。
音视频解码线程独立拆散的播放器模式,简略灵便,代码量小,面向初学者,能够很不便实现音视频同步。
音视和视频解码流程十分类似,所以咱们能够将二者的解码器形象为一个基类:
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;// 用作音视频同步
};
篇幅无限,代码贴多了容易导致视觉疲劳,残缺实现代码见浏览原文,这里只贴出几个要害函数。
解码循环。
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->SetVideoRender(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->SetVideoRender(m_AudioRender);
// 设置音频工夫戳回调
m_VideoDecoder->SetAVSyncCallback(m_AudioDecoder, AudioDecoder::GetAudioDecoderTimestampForAVSync);
结语
播放器实现音视频同步的这三种形式中,抉择哪一种形式适合要视具体的应用场景而定,比方你对画面晦涩度要求很高,能够抉择音频向视频同步;你要独自实现视频或音频播放,间接向零碎时钟同步更为不便。
分割与交换
技术交换获取源码能够增加我的微信:Byte-Flow