QT + FFMPEG实现基本播放器(五):音频视频同步

QT + FFMPEG实现基本播放器(五):音频视频同步

该文章主要参考:https://www.cnblogs.com/leisure_chn/p/10284653.html

对于音视频播放来说,如果不进行同步的话,即使在视频开头是同步的,但是播放到后面肯定会出现不同步的现象。

视频是按帧播放,图像显示设备每次显示一帧画面,视频播放速度由帧率确定,帧率指示每秒显示多少帧;音频按采样点播放,声音播放设备每次播放一个采样点,声音播放速度由采样率确定,采样率指示每秒播放多少个采样点。如果仅仅是视频按帧率播放,音频按采样率播放,二者没有同步机制,即使最初音视频是基本同步的,随着时间的流逝,音视频会逐渐失去同步,并且不同步的现象会越来越严重。这是因为:一、播放时间难以精确控制,二、异常及误差会随时间累积。所以,必须要采用一定的同步策略,不断对音视频的时间差作校正,使图像显示与声音播放总体保持一致。

音视频同步的方式基本是确定一个时钟(音频时钟、视频时钟、外部时钟)作为主时钟,非主时钟的音频或视频时钟为从时钟。在播放过程中,主时钟作为同步基准,不断判断从时钟与主时钟的差异,调节从时钟,使从时钟追赶(落后时)或等待(超前时)主时钟。按照主时钟的不同种类,可以将音视频同步模式分为如下三种:
音频同步到视频,视频时钟作为主时钟。
视频同步到音频,音频时钟作为主时钟。
音视频同步到外部时钟,外部时钟作为主时钟。

模仿ffpaly的音视频同步机制,在这里实现最简单的音视频同步机制,可以基本的实现音视频的同步播放:

实现主时钟的结构体

typedef struct
{
    double m_Pts;                     // 当前帧(待播放)显示时间戳,播放后,当前帧变成上一帧
    double m_PtsDrift;               // 当前帧显示时间戳与当前系统时钟时间的差值
    double m_LastUpdated;            // 当前时钟(如视频时钟)最后一次更新时间,也可称当前时钟时间
    double m_Speed;                   // 时钟速度控制,用于控制播放速度
    int m_Serial;                     // 播放序列,所谓播放序列就是一段连续的播放动作,一个seek操作会启动一段新的播放序列
    int m_Paused;                     // 暂停标志
}PALY_CLOCK_S;

以下的主时钟操作函数是从ffplay中拷贝得来

// 返回值:返回上一帧的pts更新值(上一帧pts+流逝的时间)
double get_clock(PALY_CLOCK_S *c)
{

    if (c->m_Paused)
    {
        return c->m_Pts;
    }
    else
    {
        double time = av_gettime_relative() / 1000000.0;
        double ret = c->m_PtsDrift + time;   // 展开得: c->pts + (time - c->last_updated)
        return ret;
    }
}

void set_clock_at(PALY_CLOCK_S *c, double pts, int serial, double time)
{
    c->m_Pts = pts;
    c->m_LastUpdated = time;
    c->m_PtsDrift = c->m_Pts - time;
    c->m_Serial = serial;
}

void set_clock(PALY_CLOCK_S *c, double pts, int serial)
{
    double time = av_gettime_relative() / 1000000.0;
    set_clock_at(c, pts, serial, time);
}


void init_clock(PALY_CLOCK_S *c)
{
    c->m_Speed = 1.0;
    c->m_Paused = 0;

    set_clock(c, NAN, -1);
}

修改AudioThread和VideoThread的run函数:

void AudioThread::run(void)
{
    char AudioDataOut[10000] = {0};
    int data_count = 0;

    qDebug()<<"Start Audio Thread";

    while(m_IsRuning)
    {
        if( g_MedieInfo.m_AudioPacketQueue.empty())
        {
            msleep(1);
            continue;
        }

#ifndef _USE_SDL_
        if (GetFree() < 10000)
        {
            msleep(1);
            continue;
        }
#endif
        AVPacket avPacket;
        g_MedieInfo.m_AudioPacketQueue.wait_and_pop(avPacket);


        //按解码顺序发送packet,将视频文件中的packet序列依次发送给解码器。发送packet的顺序如IPBBPBB
        int ret = avcodec_send_packet(g_MedieInfo.avFormatContext->streams[g_MedieInfo.audioStreamIndex]->codec, &avPacket);
        if (ret != 0)
        {
            qDebug()<<__func__<<__LINE__<<"send packet error";
            continue;
        }

        ret = avcodec_receive_frame(g_MedieInfo.avFormatContext->streams[g_MedieInfo.audioStreamIndex]->codec,audioFrame);
        if (ret < 0)
        {
            if (ret == AVERROR(EAGAIN))
            {
                continue;
            }

            if (ret == AVERROR_EOF)
            {
                qDebug()<<__func__<<__LINE__<<"receive frame error";
            }
        }

        double audioPts = audioFrame->pts * av_q2d(g_MedieInfo.avFormatContext->streams[g_MedieInfo.audioStreamIndex]->time_base);
        
        //将音频的时钟设置为主时钟
        set_clock(&g_MedieInfo.m_PalyClock,audioPts,0);

        qDebug()<<"Audio Time "<streams[g_MedieInfo.audioStreamIndex]->codec,AudioDataOut,audioFrame);

#ifndef _USE_SDL_
        Get()->Write(out, len);
#else
        data_count += len;
        //Set audio buffer (PCM data)
        audio_chunk = (Uint8 *) AudioDataOut;
        //Audio buffer length
        audio_len = len;
        audio_pos = audio_chunk;

        while(audio_len>0)//Wait until finish
            SDL_Delay(1);
#endif

        av_packet_unref(&avPacket);
        av_freep(&avPacket);
    }

    if (m_aCtx)
    {
        swr_free(&m_aCtx);
        m_aCtx = NULL;
    }

    SDL_Quit();

    qDebug()<<"Stop Audio Thread";
}
void VideoThread::run()
{
    qDebug()<<"Start Video Thread";

    while(m_IsRuning)
    {

        if( g_MedieInfo.m_VideoPacketQueue.empty())
        {
            msleep(1);
            continue;
        }

        AVPacket avPacket;
        g_MedieInfo.m_VideoPacketQueue.wait_and_pop(avPacket);


        //按解码顺序发送packet,将视频文件中的packet序列依次发送给解码器。发送packet的顺序如IPBBPBB
        int ret = avcodec_send_packet(g_MedieInfo.avFormatContext->streams[g_MedieInfo.videoStreamIndex]->codec, &avPacket);
        if (ret != 0)
        {
            qDebug()<<__func__<<__LINE__<<"send packet error";
            continue;
        }

        ret = avcodec_receive_frame(g_MedieInfo.avFormatContext->streams[g_MedieInfo.videoStreamIndex]->codec,vidioFrameRaw);
        if (ret < 0)
        {
            if (ret == AVERROR(EAGAIN))
            {
                continue;
            }

            if (ret == AVERROR_EOF)
            {
                qDebug()<<__func__<<__LINE__<<"receive frame error";
            }
        }

        double videoPts = 0;

        double audioTime = 0;

        videoPts = vidioFrameRaw->pts * av_q2d(g_MedieInfo.avFormatContext->streams[g_MedieInfo.videoStreamIndex]->time_base);

        audioTime = get_clock(&g_MedieInfo.m_PalyClock);
        
        //如果读取到无效的时钟时间的话则continue
        if(isnan(audioTime))
        {
            usleep(1000*1000/g_MedieInfo.m_FrameRate);
            av_packet_unref(&avPacket);
            av_freep(&avPacket);
            continue;
        }

        //qDebug()<<"Video Time "<= 0)//视频时钟比主时钟快,则休眠
        {
            msleep(DiffTimeMs);
        }
        else//视频时钟比主时钟慢,则丢弃该帧
        {
            msleep(1);
            av_packet_unref(&avPacket);
            av_freep(&avPacket);
            continue;
        }



        sws_scale(g_MedieInfo.swsContext, (const uint8_t *const *)vidioFrameRaw->data, vidioFrameRaw->linesize, 0, videoHeight, vidioFrameConvert->data, vidioFrameConvert->linesize);

        QImage image(vidioFrameConvert->data[0], videoWidth, videoHeight, QImage::Format_RGB32);

        if (!image.isNull())
        {
            emit receiveImage(image);
        }

        usleep(1000*1000/g_MedieInfo.m_FrameRate);

        av_packet_unref(&avPacket);
        av_freep(&avPacket);
    }

    qDebug()<<"Stop Video Thread";
}

通过以上视频播放线程中对时钟进行误差校验的话,可以初步实现音视频同步的功能。但是这只是最简单的实现方法,在ffplay中音视频同步的实现十分的复杂,等有空再仔细的阅读源码。

你可能感兴趣的:(FFMPEG,ffmpeg,qt)