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

news2024/11/19 13:36:10

目录

学习课题:逐步构建开发播放器【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.coloradmin.cn/o/1275937.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Oracle:左连接、右连接、全外连接、(+)号详解

目录 Oracle 左连接、右连接、全外连接、&#xff08;&#xff09;号详解 1、左外连接&#xff08;LEFT OUTER JOIN/ LEFT JOIN&#xff09; 2、右外连接&#xff08;RIGHT OUTER JOIN/RIGHT JOIN&#xff09; 3、全外连接&#xff08;FULL OUTER JOIN/FULL JOIN&#xff0…

013 OpenCV copyMakeBorder(padding)

目录 一、环境 二、原理 三、完整代码 一、环境 本文使用环境为&#xff1a; Windows10Python 3.9.17opencv-python 4.8.0.74 二、原理 cv.copyMakeBorder是OpenCV库中的一个函数&#xff0c;用于在图像周围添加边框&#xff08;padding&#xff09;。这个函数可以用于图…

​iOS Class Guard github用法、工作原理和安装详解及使用经验总结

iOS Class Guard是一个用于OC类、协议、属性和方法名混淆的命令行工具。它是class-dump的扩展。这个工具会生成一个symbol table&#xff0c;这个table在编译期间会包含进工程中。iOS-Class-Guard能有效的隐藏绝大多数的类、协议、方法、属性和 实例变量 名。iOS-Class-Guard不…

Eclipse导入JavaWeb项目

Eclipse 导入JavaWeb项目教程 运行教程 亲爱的粉丝们&#xff0c;我深知你们对Eclipse导入Web项目教程的迫切需求。在这个充满竞争的时代&#xff0c;每一个项目都离不开高效的沟通。过程中需要对应的环境适配和软件安…

淘宝/天猫商品详情API接口丨京东商品详情丨1688商品详情丨接口key密钥获取方式

要获取淘宝/天猫商品详情API接口、京东商品详情API接口、1688商品详情API接口以及接口密钥&#xff08;Key&#xff09;&#xff0c;可以按照以下步骤进行操作&#xff1a; 注册并登录淘宝/天猫开发者中心或京东开放平台或1688开放平台&#xff0c;并创建应用。在创建应用的过…

cmake和vscode 下的cmake的使用详解(二)

第四讲&#xff1a; GDB 调试器 前言&#xff1a; GDB(GNU Debugger) 是一个用来 调试 C/C 程序 的功能强大的 调试器 &#xff0c;是 Linux 系统开发 C/C 最常用的调试器 程序员可以 使用 GDB 来跟踪程序中的错误 &#xff0c;从而减少程序员的工作量。 Linux 开发 C/C …

postgresql从入门到精通教程 - 第36讲:postgresql逻辑备份

PostgreSQL从小白到专家&#xff0c;是从入门逐渐能力提升的一个系列教程&#xff0c;内容包括对PG基础的认知、包括安装使用、包括角色权限、包括维护管理、、等内容&#xff0c;希望对热爱PG、学习PG的同学们有帮助&#xff0c;欢迎持续关注CUUG PG技术大讲堂。 第36讲&#…

分布式系统中最基础的 CAP 理论及其应用

对于开发或设计分布式系统的架构师、工程师来说&#xff0c;CAP 是必须要掌握的基础理论&#xff0c;CAP 理论可以帮助架构师对系统设计中目标进行取舍&#xff0c;合理地规划系统拆分的维度。下面我们先讲讲分布式系统的特点。 分布式系统的特点 随着移动互联网的快速发展&a…

vue使用皮肤框架element-plus,element-ui是vue2.0的皮肤

vue使用皮肤框架element-plus&#xff0c;element-ui是vue2.0的皮肤&#xff0c;所以现在最新的vue3要使用element-plus皮肤。使用命令行安装element-plus: npm install element-plus安装后&#xff1a; {"name": "vue01","private": true,&quo…

Java连接数据库

数据库 存储数据&#xff1a;集中管理 目的&#xff1a; 文件中的数据能够放在数据库中集中管理 管理方法&#xff1a;一个项目一个库&#xff0c;每个库中包含最小化数据的表 开发&#xff1a; 节省存储空间&#xff0c;节省运行空间&#xff0c;采用数据库&#xff0c;架…

windows判断exe应用程序是否在使用的bat脚本

脚本 REM 查询进程是否存在 tasklist|findstr /i "mysqld.exe">nul &&echo y >2.log ||echo n >2.log REM 读取文本内容赋值给变量 set /P resu<2.log if %resu% y (echo process in use ) else (echo process not in use )我们已mysqld.exe…

【算法刷题】Day10

文章目录 15. 三数之和题干&#xff1a;算法原理&#xff1a;1、排序 暴力枚举 利用set 去重2、排序 双指针 代码&#xff1a; 18. 18. 四数之和题干&#xff1a;算法原理&#xff1a;1、排序 暴力枚举 利用set 去重2、排序 双指针 代码&#xff1a; 15. 三数之和 原题链…

利用python连接MySQL数据库并执行相关sql操作

一、新建MySQL数据库 1.启动MySQL服务 打开phpstudy&#xff0c;开启MySQL服务。如果开启失败的话&#xff0c;可以打开任务管理器&#xff0c;把正在运行的mysqld服务的进程进行关闭&#xff0c;再次打开MySQL服务即可启动。 2.新建MySQL数据库 选择数据库&#xff0c;点击…

linux(3)之buildroot配置软件包

Linux(3)之buildroot配置软件包 Author&#xff1a;Onceday Date&#xff1a;2023年11月30日 漫漫长路&#xff0c;才刚刚开始… 参考文档&#xff1a; Buildroot - Making Embedded Linux Easymdev.txt docs - busybox - BusyBox: The Swiss Army Knife of Embedded Linu…

Elasticsearch分词器--空格分词器(whitespace analyzer)

介绍 文本分析&#xff0c;是将全文本转换为一系列单词的过程&#xff0c;也叫分词。analysis是通过analyzer(分词器)来实现的&#xff0c;可以使用Elasticearch内置的分词器&#xff0c;也可以自己去定制一些分词器。除了在数据写入时将词条进行转换&#xff0c;那么在查询的时…

记一次SQL Server磁盘突然满了导致数据库锁死事件is full due to ‘LOG_BACKUP‘.

背景 最近我们的sql server 数据库磁盘在80左右&#xff0c;需要新增磁盘空间。还是处以目前可控的范围内&#xff0c;但是昨天晚上告警是80%&#xff0c;凌晨2:56分告警是90%&#xff0c;今天早上磁盘就满了。 经过 通过阿里云后台查看&#xff0c;磁盘已经占据99%&#xff0c…

Spring @Cacheable缓存注解

一、简介 缓存介绍 缓存&#xff0c;在我们的日常开发中用的非常多&#xff0c;是我们应对各种性能问题支持高并发的一大利器。 Spring 从 3.1 开始就引入了缓存的支持。定义了如下两个接口来统一支持不同的缓存技术。 org.springframework.cache.Cacheorg.springframework.ca…

uniapp中解决swiper高度自适应内容高度

起因&#xff1a;uniapp中swiper组件swiper 标签存在默认高度是 height: 150px &#xff1b;高度无法实现由内容撑开&#xff0c;在默认情况下&#xff0c;swiper盒子高度显示总是 150px 解决办法思路&#xff1a; 动态设置swiper盒子的高度&#xff0c;故需要获取swiper-item盒…

展锐平台回读分区

展锐平台回读分区 1. 回读分区意义2. 操作步骤3. 回读存放的位置 1. 回读分区意义 回读的分区可用作备份&#xff0c;还可将回读到的分区与烧入机器版本的分区进行比较&#xff0c;如果两者不一致则说明烧录出现问题&#xff0c;需要重新烧录。 2. 操作步骤 打开展锐烧录工…

深度解析 Dockerfile:构建可重复、可扩展的Docker镜像

文章目录 什么是Dockerfile&#xff1f;Dockerfile的基本结构常用Dockerfile指令解析1. FROM2. LABEL3. WORKDIR4. COPY5. RUN6. EXPOSE7. ENV8. CMD 构建可重复、可扩展的Docker镜像1. 指定基础镜像的版本2. 合理使用缓存3. 精简镜像4. 使用多阶段构建 总结 &#x1f388;个人…