当前位置: 首页 > article >正文

播放器开发(七):音视频同步实现

目录

学习课题:逐步构建开发播放器【QT5 + FFmpeg6 + SDL2】

原理

简单分析:
下图简单描述了在一个播放过程中,假设我们先播放音频,对比一个公共时间轴,视频就会始终比音频慢0.003s
我们在日常中用一些播放器播放视频资源时,可能会遇见“画面中人先说话,声音后面才听到” 或者是“声音先出来了,画面中人物嘴巴还没动”的情况,这些情况的发生就是“音频播放当前帧的时间轴位置>视频播放当前帧时间轴的位置 或者是<”。

所以需要进行音视频同步,这里说的同步并不是完全同步,只是在“音频”>"视频"时,让"视频"加速追上"音频";在“音频”<"视频"时,让"视频"减速等待"音频"
就像是有两个叫”音频“和”视频“的同学在操场上跑一千米,两个人的差距非常小”音频“超过了”视频“,”视频“就会加速追上去,两人几乎保持全程”同步“最后冲刺终点时两个人同时冲线。

如何实现:

通过分析,我们现在知道的就是要做【在“音频”>"视频"时,让"视频"加速追上"音频"】【在“音频”<"视频"时,让"视频"减速等待"音频"】 这个事情,因为我们使用了FFmpeg框架,在AVFrame[帧结构体]中,定义了字段“pts
解释:Presentation timestamp in time_base units (time when frame should be shown to user)
就是指这一帧需要显示给用户的“时间点”。


即在“音频pts”>"视频pts"时,让"视频"加速追上"音频"

    在“音频pts”<"视频pts"时,让"视频"减速等待"音频"。

步骤

MediaSync模块

1、在audio写入实际播放数据之前记录对应当前帧数据的pts

2、在video读取帧数据开始进行缩放渲染之前判断“视频pts”是否小于"音频pts",根据结果进行加速或者是等待。

AudioOutPut模块

添加代码在合适的位置设置pts

VideoOutPut模块

添加代码在帧读取后判断“视频pts”是否小于"音频pts"

添加的部分

AudioOutPut

1、添加了一个存放音频pts的队列

2、在SDL回调中把pts设置进MediaSync,提供给video获取进行“加速” or “等待


为什么使用队列

我看过很多的文章,他们在实现音视频同步的时候都不会用到队列去存放pts,而是使用一些样本计算公式去算出pts,然后设置进时钟。
例如:
“时长=音频数据长度(bytes)/(声道数∗采样率∗位深度/8)”
”时长=采样数/采样率
 

        这两条公式在理论上是可行的,但是在一些特殊情况下就会变得不同步,比如根据公式计算出来的帧时长与实际帧的时长不同,即使是非常细微的差距也会导致音视频同步出现异常。
        我在做音视频同步的时候就刚好遇到了这种特殊情况,根据公式计算出来的pts一直是固定的0.02322,而实际帧(AVFrame)结构体下pts字段所表示的每一帧的pts差距并不固定是0.02322,有时候会小于,有时候会大于,我的理解是这个“帧差距”代表的就是这一音频帧实际的持续时间,如果我们用公式计算出来的值与实际的不符,就会导致在video进行同步时获得了错误的音频pts,导致同步出现异常。

//AudioOutPut.h
std::queue<double>*ptsQueue;//音频帧pts队列
MediaSync *sync;
AVRational streamTimeBase;
// 设置音频流的TimeBase
void setStreamTimeBase(AVRational &streamTimeBase);
// 添加同步对象
void setSync(MediaSync *sync);

//AudioOutPut.cpp
int AudioOutPut::init(int mode) {
    ...
    fifo = av_audio_fifo_alloc(playSampleFmt, playChannels, spec.samples * 5);
    ptsQueue = new std::queue<double>();
    ...
}

void AudioOutPut::AudioCallBackFromQueue(Uint8 *stream, int len) {
    ...
    //lock

    sync->setAudioPts(ptsQueue->front());
    ptsQueue->pop();

    //read
    ...
}


void AudioOutPut::run() {
    ...
    while (true) {
                SDL_LockMutex(mtx);
                if (av_audio_fifo_space(fifo) >= playSamples) {
                    //保存pts
                    pts = frame->pts * av_q2d(streamTimeBase);
                    ptsQueue->push(pts);

                    av_audio_fifo_write(fifo, (void **) &audioBuffer, playSamples);
                    SDL_UnlockMutex(mtx);
                    av_frame_unref(frame);
                    break;
                }
                SDL_UnlockMutex(mtx);
                //队列可用空间不足则延时等待
                SDL_Delay((double) playSamples / playSampleRate);
            }
    ...
}    

void AudioOutPut::setSync(MediaSync *sync) {
    this->sync = sync;
}
void AudioOutPut::setStreamTimeBase(AVRational &streamTimeBase) {
    this->streamTimeBase = streamTimeBase;
}

VideoOutPut

获取从audio中拿到的pts,计算vidio_pts与audio_pts的差距,判断进行“加速“还是”等待

//VideoOutPut.h
MediaSync *sync;
AVRational streamTimeBase;

// 设置视频流的streamTimeBase
void setStreamTimeBase(AVRational &streamTimeBase);
// 添加同步对象
void setSync(MediaSync *sync);



//VideoOutPut.cpp
void VideoOutPut::run() {
    AVFrame *frame;
    double pts;
    double diff;
    double audio_pts;
    while (!isStopped) {
        frame = frameQueue->pop(10);
        if (frame) {
            //同步
            pts = frame->pts * av_q2d(streamTimeBase);
            audio_pts = sync->getAudioPts();
            diff = pts - audio_pts;
            if (diff > 0) {
                av_usleep(diff * 1000000.0);
            }

            //图像缩放、颜色空间转换
            sws_scale(swsContext, (const uint8_t *const *) frame->data, frame->linesize, 0, decCtx->height, playFrame->data, playFrame->linesize);
            av_frame_unref(frame);

            //视频区域
            SDL_Rect sdlRect;
            sdlRect.x = 0;
            sdlRect.y = 0;
            sdlRect.w = decCtx->width;
            sdlRect.h = decCtx->height;
            //渲染到sdl窗口
            emit refreshImage(sdlRect, playFrame);
        }
    }
}

void VideoOutPut::setStreamTimeBase(AVRational &streamTimeBase) {
    this->streamTimeBase = streamTimeBase;
}
void VideoOutPut::setSync(MediaSync *sync) {
    this->sync = sync;
}

完整代码

MediaSync

直接添加get set函数即可,单独新建类存放,后续可能进行优化拓展

//MediaSync.h
#include <mutex>

/**
 * 用于进行音视频同步
 */
class MediaSync {
private:
    std::mutex m_mutex; // 互斥锁
    double m_audioPts=0;
public:
    /**
     * 设置音频pts
     * @param pts 经过frame->pts * av_q2d(time_base)的pts
     */
    void setAudioPts(double pts);

    /**
     * 获取音频经过frame->pts * av_q2d(time_base)的pts
     * @return m_audioPts
     */
    double getAudioPts();
};



//MediaSync.cpp
#include "MediaSync.h"
void MediaSync::setAudioPts(double pts) {
    m_audioPts = pts;
}

double MediaSync::getAudioPts() {
    return m_audioPts;
}

测试运行结果

PlayerMain

//PlayerMain.h
MediaSync *sync;




//PlayerMain.cpp
PlayerMain::PlayerMain(QWidget *parent)
    : QWidget(parent), ui(new Ui::PlayerMain) {
    ui->setupUi(this);

    sync = new MediaSync();

    // 解复用
    demuxThread = new DemuxThread(&audioPacketQueue, &videoPacketQueue);
    demuxThread->setUrl("/Users/mac/Downloads/0911超前派对:于文文孟佳爆笑猜词 王源欧阳靖脑洞大开.mp4");
    //    demuxThread->setUrl("/Users/mac/Downloads/23.mp4");
    demuxThread->start();
    int ret;
    // 解码-音频
    audioDecodeThread = new DecodeThread(
            demuxThread->getCodec(MediaType::Audio),
            demuxThread->getCodecParameters(MediaType::Audio),
            &audioPacketQueue,
            &audioFrameQueue);
    audioDecodeThread->init();
    audioDecodeThread->start();
    // 解码-视频
    videoDecodeThread = new DecodeThread(
            demuxThread->getCodec(MediaType::Video),
            demuxThread->getCodecParameters(MediaType::Video),
            &videoPacketQueue,
            &videoFrameQueue);
    videoDecodeThread->init();
    videoDecodeThread->start();

    //output
    // audio
    audioOutPut = new AudioOutPut(audioDecodeThread->dec_ctx, &audioFrameQueue);
    audioOutPut->init(1);
    audioOutPut->setSync(sync);
    audioOutPut->setStreamTimeBase(*demuxThread->getStreamTimeBase(MediaType::Audio));


    // video
    this->resize(1920 / 2, 1080 / 2);
    videoOutPut = new VideoOutPut(videoDecodeThread->dec_ctx, &videoFrameQueue);
    videoOutPut->init();
    videoOutPut->setSync(sync);
    videoOutPut->setStreamTimeBase(*demuxThread->getStreamTimeBase(MediaType::Video));

    VideoWidget *videoWidget = new VideoWidget(this);
    connect(videoOutPut, &VideoOutPut::refreshImage, videoWidget, &VideoWidget::updateImage);
    videoWidget->show();
    videoWidget->initSDL();
    audioOutPut->start();
    videoOutPut->start();
    //    videoWidget->setParent(this);
}

播放器开发(七):音视频同步实现


http://www.kler.cn/a/153462.html

相关文章:

  • Linux:vim的简单使用
  • JavaScript 内存泄漏的检测与防范:让你的程序更稳定
  • PTA 7-188 水仙花数
  • 计算机网络——数据链路层-封装成帧(帧定界、透明传输-字节填充,比特填充、MTU)
  • centos上安装并持久化配置LVS
  • 深度学习今年来经典模型优缺点总结,包括卷积、循环卷积、Transformer、LSTM、GANs等
  • 关于前端的学习思考-父子盒子溢出问题
  • 山西电力市场日前价格预测【2023-12-02】
  • wordpress安装之Linux ftp传输
  • Kaggle-水果图像分类银奖项目 pytorch Densenet GoogleNet ResNet101 VGG19
  • 鉴源实验室 | 汽车网络安全攻击实例解析(三)
  • Doris 数据导入二:Stream Load 方式
  • 2023年第十二届数学建模国际赛小美赛A题太阳黑子预测求解分析
  • 红队攻防实战之某商城Getshell
  • <Linux>(极简关键、省时省力)《Linux操作系统原理分析之Linux 进程管理 5》(9)
  • 使用Xshell启动远程服务器上的tensorboard:本地浏览器打开
  • Apache Flink(五):Apache Flink快速入门 - 环境准备及入门案例
  • Vue 3.0 组合式API 生命周期钩子
  • Jmeter工具+ant+jenkins实现持续集成
  • VSC++=》 拆解整数对号入座重组