嵌入式音视频开发(二)ffmpeg音视频同步
系列文章目录
嵌入式音视频开发(零)移植ffmpeg及推流测试
嵌入式音视频开发(一)ffmpeg框架及内核解析
嵌入式音视频开发(二)ffmpeg音视频同步
嵌入式音视频开发(三)直播协议及编码器
文章目录
- 系列文章目录
- 前言
- 一、音视频同步
- 1.1 基础概念
- 1.2 三种同步方法
- 二、音视频同步的实现
- 2.1 时间基的转换问题
- 2.2 音频为基准
- 2.2.1 实现思路
- 2.2.2 代码大纲
- 2.3 外部时钟同步
- 2.3.1 实现思路
- 2.3.2 代码大纲
前言
前文中已经讲述了音视频处理的流程,需要我们将音频数据和视频数据分开处理,这个时候我们就需要音视频同步操作。
一、音视频同步
我们平常看视频的时候最烦恼的就是各种音画不同步,例如音频是100ms延时而视频需要150ms延时才能到达,这其中我们就需要进行音视频同步来解决这个问题。 音视频同步是多媒体处理中的一个关键问题,常用方法包括三种不同的同步策略:以视频为基准、以音频为基准和以外部时钟为基准。
1.1 基础概念
在FFmpeg进行音视频解码时,PTS (Presentation Time Stamp) 是一个非常重要的概念,它表示每一帧数据(音频帧或视频帧)的展示时间,即该帧应该在播放设备上显示的精确时间。
时间基(Timebase)是一个分数,表示每秒的时间单位。它用于将 PTS和 DTS(从基于时钟滴答的计数转换为实际的时间(秒)。常见的表示形式为 1/fps 或 1/sample_rate例如,假设视频流的时间基准是1/90000,那么每个时间单位代表1/90000秒。因此,PTS值为90000时,相当于1秒。实际上ffmpeg内部存在多种时间基,在不同的阶段(结构体)中,对应的时间基的值都不相同。
表示方法 | 结构体 | 描述 | 作用 |
---|---|---|---|
time_base | AVStream | 流的时间基 | 用于将 PTS 和 DTS 转换为实际时间 |
time_base | AVCodecContext | 编码器或解码器的时间基 | 用于内部处理和同步 |
video_codec_timebase audio_codec_timebase | AVFormatContext | 格式上下文的时间基 | 用于整体管理和同步 |
值得注意的是,虽然 AVPacket 和 AVFrame 本身没有直接的时间基字段,但它们的时间戳(PTS 和 DTS)是基于其所属流的时间基来解释的。
时间戳可以简单理解为计时器,用于记录或设置对应时间点的操作。在 FFmpeg 中,时间戳用于同步音视频帧的播放时间。时间戳的计算公式如下:
timestamp(ffmpeg 内部时间戳) = PTS * 时间基
time(秒) = PTS * 时间基
例如,假设我们有一个视频流,其时间基为 1/90000,若某帧的 PTS 值为 90000,则该帧的实际展示时间为time(秒) = PTS * 时间基 = 90000 * (1/90000) = 1 秒
1.2 三种同步方法
这里先简单举个例子,例如下图所示,原本的视频应在0.080秒有一帧,但是现在出现了掉帧,此时对应音频就需要加速播放或者也相应丢一帧。简单来说就是,以谁为基准就由谁来维护时间轴。
(1)以视频为基准:视频被视为主要的同步标准,音频的播放时间会根据视频帧的时间戳来进行调整。如果音频的播放时间比视频快,系统会延迟音频的播放,为避免过多积压可能会丢弃部分音频帧;如果音频播放落后于视频,系统会通过延时音频的播放来保证同步。
(2)以音频为基准:以音频为基准时,视频会根据音频的时间戳进行调整。如果视频的播放时间比音频快,系统会延迟视频的播放,直到音频达到相应的时间点;而视频播放落后于音频,系统会加速视频的播放,丢掉部分视频帧,从而保证音视频同步。
(3) 以外部时钟为基准:外部时钟同步是一种更为综合的方式,它使用一个外部时钟(例如系统时钟或硬件时钟)来同时控制音频和视频的播放。外部时钟会提供一个精确的时间基准,视频和音频都需要根据这个时钟进行调整。
二、音视频同步的实现
2.1 时间基的转换问题
前面提到了ffmpeg内部存在多种时间基,在不同的阶段(结构体)中,对应的时间基的值都不相同,此外视频流的时间基和音频流的时间基也不同。通常情况下需要使用av_q2d()函数将AVRational 类型的时间基(Timebase)转换为双精度浮点数(double)。AVRational 是一个表示分数的结构体,通常用于表示时间基、帧率等需要精确表示的比率。
typedef struct AVRational {int num; ///< 分子 (numerator)int den; ///< 分母 (denominator)
} AVRational;// 通过 av_q2d 函数将时间基转换为浮点数后,可以将其乘以 PTS 或 DTS 来得到实际时间
double av_q2d(AVRational q) {return q.num / (double)q.den;
}
2.2 音频为基准
音频为基准和视频为基准在实现逻辑上差不多,这里以音频为例。
2.2.1 实现思路
以音频为基准进行同步的基本思路是:
- 选择音频流作为同步基准
- 解码音频数据并更新当前音频时间戳
- 解码视频数据并根据音频时间戳调整视频帧的显示时间,确保音视频同步
- 通过适当的缓冲控制,确保播放的流畅性和稳定性
2.2.2 代码大纲
int main{// 初始化 FFmpeg 库av_register_all();AVFormatContext *fmt_ctx = NULL;// 打开输入文件并获取流信息if (open_input_file(&fmt_ctx, "input.mp4") < 0) {return -1;}// 查找音视频流并初始化解码器int audio_stream_idx = find_stream(fmt_ctx, AVMEDIA_TYPE_AUDIO);int video_stream_idx = find_stream(fmt_ctx, AVMEDIA_TYPE_VIDEO);AVCodecContext *audio_dec_ctx = init_decoder(fmt_ctx, audio_stream_idx);AVCodecContext *video_dec_ctx = init_decoder(fmt_ctx, video_stream_idx);// 循环读取数据包并同步播放AVPacket pkt;while (read_packet(fmt_ctx, &pkt) >= 0) {if (pkt.stream_index == audio_stream_idx) {process_audio_packet(&pkt, audio_dec_ctx);} else if (pkt.stream_index == video_stream_idx) {process_video_packet(&pkt, video_dec_ctx, audio_dec_ctx->time_base);}av_packet_unref(&pkt);}// 清理资源cleanup(fmt_ctx, audio_dec_ctx, video_dec_ctx);
}// 解码音频数据包并更新当前音频时间戳
void process_audio_packet(AVPacket *pkt, AVCodecContext *dec_ctx) {int ret = avcodec_send_packet(dec_ctx, pkt);if (ret < 0) {fprintf(stderr, "Error sending a packet for decoding\n");return;}while (ret >= 0) {ret = avcodec_receive_frame(dec_ctx, frame);if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)break;else if (ret < 0) {fprintf(stderr, "Error during decoding\n");break;}// 更新当前音频时间戳update_current_audio_pts(frame->pts, dec_ctx->time_base);}
}void update_current_audio_pts(int64_t pts, AVRational time_base) {double pts_in_seconds = pts * av_q2d(time_base);current_audio_pts = pts_in_seconds;
}void process_video_packet(AVPacket *pkt, AVCodecContext *dec_ctx, AVRational audio_time_base) {int ret = avcodec_send_packet(dec_ctx, pkt);if (ret < 0) {fprintf(stderr, "Error sending a packet for decoding\n");return;}while (ret >= 0) {ret = avcodec_receive_frame(dec_ctx, frame);if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)break;else if (ret < 0) {fprintf(stderr, "Error during decoding\n");break;}// 获取视频帧的 PTS 并转换为秒double video_pts_in_seconds = frame->pts * av_q2d(dec_ctx->time_base);// 根据音频时间戳调整视频帧的显示时间sync_video_to_audio(video_pts_in_seconds, audio_time_base);}
}void sync_video_to_audio(double video_pts, AVRational audio_time_base) {while (video_pts > current_audio_pts) {usleep(1000); // 简单的等待机制// 更新当前音频时间戳current_audio_pts = get_current_audio_pts(audio_time_base);// 其他操作}
}double get_current_audio_pts(AVRational audio_time_base) {// 这里应该实现一个函数来获取最新的音频时间戳// 例如通过解码更多的音频帧或使用其他方法return current_audio_pts;
}
2.3 外部时钟同步
2.3.1 实现思路
以外部时钟为基准进行同步的基本思路是:
- 使用外部时钟(如系统时钟)作为基准
- 解码音频数据包,根据外部时钟调整音频播放时间
- 解码视频数据包,根据外部时钟调整视频帧的显示时间
- 通过适当的缓冲控制,确保播放的流畅性和稳定性
2.3.2 代码大纲
这里的代码和上文差不多,只有调整部分的逻辑不太一样:
// 获取当前外部时钟时间(秒)
double get_external_clock() {struct timespec now;clock_gettime(CLOCK_MONOTONIC, &now); // 使用单调递增的时钟避免系统时间变化的影响double elapsed = (now.tv_sec - start_time.tv_sec) + (now.tv_nsec - start_time.tv_nsec) / 1e9;return elapsed;
}// 解码音频数据包并根据外部时钟调整音频播放时间
void process_audio_packet(AVPacket *pkt, AVCodecContext *dec_ctx) {int ret = avcodec_send_packet(dec_ctx, pkt);if (ret < 0) {fprintf(stderr, "Error sending a packet for decoding\n");return;}while (ret >= 0) {ret = avcodec_receive_frame(dec_ctx, frame);if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)break;else if (ret < 0) {fprintf(stderr, "Error during decoding\n");break;}// 将音频帧的时间戳转换为秒double audio_pts_in_seconds = frame->pts * av_q2d(dec_ctx->time_base);// 根据外部时钟调整音频帧的播放时间sync_audio_to_external_clock(audio_pts_in_seconds, dec_ctx->time_base);}
}void sync_audio_to_external_clock(double audio_pts, AVRational time_base) {double external_clock_time = get_external_clock(); // 获取外部时钟时间(秒)// 等待直到音频帧应该播放的时间while (audio_pts > external_clock_time) {usleep(1000); // 简单的等待机制external_clock_time = get_external_clock();}// 其他操作
}void process_video_packet(AVPacket *pkt, AVCodecContext *dec_ctx, AVRational audio_time_base) {int ret = avcodec_send_packet(dec_ctx, pkt);if (ret < 0) {fprintf(stderr, "Error sending a packet for decoding\n");return;}while (ret >= 0) {ret = avcodec_receive_frame(dec_ctx, frame);if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)break;else if (ret < 0) {fprintf(stderr, "Error during decoding\n");break;}// 获取视频帧的 PTS 并转换为秒double video_pts_in_seconds = frame->pts * av_q2d(dec_ctx->time_base);// 根据外部时钟调整视频帧的显示时间sync_video_to_external_clock(video_pts_in_seconds, dec_ctx->time_base);}
}void sync_video_to_external_clock(double video_pts, AVRational video_time_base) {double external_clock_time = get_external_clock(); // 获取外部时钟时间(秒)// 等待直到视频帧应该显示的时间while (video_pts > external_clock_time) {usleep(1000); // 简单的等待机制external_clock_time = get_external_clock();}// 其他操作
}
免责声明:本文参考了网上公开的部分资料,仅供学习参考使用,若有侵权或勘误请联系笔者
相关文章:

嵌入式音视频开发(二)ffmpeg音视频同步
系列文章目录 嵌入式音视频开发(零)移植ffmpeg及推流测试 嵌入式音视频开发(一)ffmpeg框架及内核解析 嵌入式音视频开发(二)ffmpeg音视频同步 嵌入式音视频开发(三)直播协议及编码器…...

Mongodb数据管理
Mongodb数据管理 1.登录数据库,查看默认的库 [rootdb51~]# mongo> show databases; admin 0.000GB config 0.000GB local 0.000GB> use admin switched to db admin > show tables system.version > admin库:admin 是 MongoDB 的管理…...

Django 创建表 choices的妙用:get_<field_name>_display()
1.定义choices 我在创建表时,对于性别这个字段,定义了choices 选项,1代表男,2代表女 mysql中表的数据如下,里面存储的是1或2 如果我们在网页上展示的时候,想展示“男”或“女”,而不是数字1或2…...
Spring Boot 集成 Kettle
Kettle 简介 Kettle 最初由 Matt Casters 开发,是 Pentaho 数据集成平台的一部分。它提供了一个用户友好的界面和丰富的功能集,使用户能够轻松地设计、执行和监控 ETL 任务。Kettle 通过其强大的功能和灵活性,帮助企业高效地处理大规模数据集…...

自学Java-面向对象高级(final、单例类、枚举类、抽象类、接口)
自学Java-面向对象高级(final、单例类、枚举类、抽象类、接口) 一、final关键字1、认识final关键字2、final修饰变量的注意3、常量 二、单例类(设计模式)1、设计模式的概念2、单例设计模式3、单例类有很多形式4、懒汉式单例类5、小…...
Hutool - Cache:简单而强大的缓存实现
目录 1. 缓存简介 2. 引入依赖 3. 常见缓存类型及使用示例 3.1 FIFO 缓存(先进先出缓存) 3.2 LRU 缓存(最近最少使用缓存) 3.3 定时缓存 4. 缓存的基本操作 5. 总结 1. 缓存简介 在软件开发中,缓存是一种常用的…...

DeepSeek 通过 API 对接第三方客户端 告别“服务器繁忙”
本文首发于只抄博客,欢迎点击原文链接了解更多内容。 前言 上一期分享了如何在本地部署 DeepSeek R1 模型,但通过命令行运行的本地模型,问答的交互也要使用命令行,体验并不是很好。这期分享几个第三方客户端,涵盖了桌…...
Python 基础-循环
目录 简介 break continue 小结 简介 要计算123,我们可以直接写表达式: >>> 1 2 3 6要计算123...10,勉强也能写出来。 但是,要计算123...10000,直接写表达式就不可能了。 为了让计算机能计算成千上…...
Java和SQL测试、性能监控中常用工具
下面我会详细列举一些在Java和SQL测试、调试、性能监控中常用的工具,并结合项目中提到的各个技术点说明如何选择合适的工具和方法。 一、Java项目常用的测试、调试与性能监控工具 单元测试与集成测试:JUnit/TestNG: 用于编写单元测试和集成测…...

SQL 注入攻击详解[基础篇]:Web 应用程序安全漏洞与防御策略
目录 SQL注入的简介 现代 Web 应用程序中的数据库交互与 SQL 注入攻击 数据库管理系统(DBMS)架构与 SQL 注入 什么是 SQL 注入? SQL 注入的工作原理 SQL 注入的用例与影响 如何预防 SQL 注入? 数据库分类 数据库类型&am…...
【ArcGIS Pro二次开发】(87):样式_Style的用法
.Stylx类型的文件即为样式库文件,保存了符号样式。 1、根据名字获取当前工程中的style //获取当前工程中的所有style var ProjectStyles Project.Current.GetItems<StyleProjectItem>();//根据名字找出指定的style StyleProjectItem style ProjectStyles.F…...

DEX-EE三指灵巧手:扩展AI与机器人研究的边界
DEX-EE三指灵巧手,由Shadow Robot与Google DeepMind合作开发,以其先进技术和设计,正在引领AI与机器人研究的新趋势。其高精度传感器和灵活的机械手指,能够捕捉复杂的环境数据,为强化学习实验提供了可靠支持。 Shadow R…...
简站主题:简洁、实用、SEO友好、安全性高和后期易于维护的wordpress主题
简站主题以其简洁的设计风格、实用的功能、优化的SEO性能和高安全性而受到广泛好评。 简洁:简站主题采用扁平化设计风格,界面简洁明了,提供多种布局和颜色方案,适合各种类型的网站,如个人博客和企业网站。 实用&…...
23种设计模式 - 责任链
模式定义 责任链模式(Chain of Responsibility Pattern)是一种行为型设计模式,允许多个对象按链式顺序处理请求,直到其中一个对象处理为止。该模式将请求的发送者和接收者解耦,使多个对象都有机会处理请求。 模式结构…...
Flink SQL与Doris实时数仓Join实战教程(理论+实例保姆级教程)
目录 第一章:Regular Joins 深度解析 1.1 核心原理与适用场景 1.2 电商订单 - 商品实时关联案例 1.2.1 数据流设计 1.2.2 Doris 表设计优化 1.2.3 性能调优要点 第二章:Interval Joins 实战应用 2.1 时间区间关联原理 2.2 优惠券使用有效性验证 2.2.1 业务场景说明 …...

算法——舞蹈链算法
一,基本概念 算法简介 舞蹈链算法(Dancing Links,简称 DLX)是一种高效解决精确覆盖问题的算法,实际上是一种数据结构,可以用来实现 X算法,以解决精确覆盖问题。由高德纳(Donald E.…...
【复现DeepSeek-R1之Open R1实战】系列6:GRPO源码逐行深度解析(上)
目录 4 GRPO源码分析4.1 数据类 GRPOScriptArguments4.2 系统提示字符串 SYSTEM_PROMPT4.3 奖励函数4.3.1 accuracy_reward函数4.3.2 verify函数4.3.3 format_reward函数 4.4 将数据集格式化为对话形式4.5 初始化GRPO Trainer 【复现DeepSeek-R1之Open R1实战】系列3࿱…...

若依Flowable工作流版本监听器使用方法
1.前言 本文详细介绍如何在若依Flowable工作流版本(RuoYi-Vue-Flowable)中配置执行监听器和任务监听器。是以我二次开发的代码为基础,介绍如何配置监听器,已解决源码在新增或删除监听器出现的问题,如果需要二次开发的…...
机器视觉--图像的运算(乘法)
一、引言 在图像处理领域,Halcon 是一款功能强大且广泛应用的机器视觉软件库。它提供了丰富的算子和工具,能够满足各种复杂的图像处理需求。图像的乘法运算作为其中一种基础操作,虽然不像一些边缘检测、形态学处理等操作那样被频繁提及&…...

突破反爬困境:从服务端渲染到客户端SPA,爬虫环境的演变与新挑战(一)
声明 本文所讨论的内容及技术均纯属学术交流与技术研究目的,旨在探讨和总结互联网数据流动、前后端技术架构及安全防御中的技术演进。文中提及的各类技术手段和策略均仅供技术人员在合法与合规的前提下进行研究、学习与防御测试之用。 作者不支持亦不鼓励任何未经授…...
vscode里如何用git
打开vs终端执行如下: 1 初始化 Git 仓库(如果尚未初始化) git init 2 添加文件到 Git 仓库 git add . 3 使用 git commit 命令来提交你的更改。确保在提交时加上一个有用的消息。 git commit -m "备注信息" 4 …...
golang循环变量捕获问题
在 Go 语言中,当在循环中启动协程(goroutine)时,如果在协程闭包中直接引用循环变量,可能会遇到一个常见的陷阱 - 循环变量捕获问题。让我详细解释一下: 问题背景 看这个代码片段: fo…...

【2025年】解决Burpsuite抓不到https包的问题
环境:windows11 burpsuite:2025.5 在抓取https网站时,burpsuite抓取不到https数据包,只显示: 解决该问题只需如下三个步骤: 1、浏览器中访问 http://burp 2、下载 CA certificate 证书 3、在设置--隐私与安全--…...
【服务器压力测试】本地PC电脑作为服务器运行时出现卡顿和资源紧张(Windows/Linux)
要让本地PC电脑作为服务器运行时出现卡顿和资源紧张的情况,可以通过以下几种方式模拟或触发: 1. 增加CPU负载 运行大量计算密集型任务,例如: 使用多线程循环执行复杂计算(如数学运算、加密解密等)。运行图…...

前端开发面试题总结-JavaScript篇(一)
文章目录 JavaScript高频问答一、作用域与闭包1.什么是闭包(Closure)?闭包有什么应用场景和潜在问题?2.解释 JavaScript 的作用域链(Scope Chain) 二、原型与继承3.原型链是什么?如何实现继承&a…...

AI书签管理工具开发全记录(十九):嵌入资源处理
1.前言 📝 在上一篇文章中,我们完成了书签的导入导出功能。本篇文章我们研究如何处理嵌入资源,方便后续将资源打包到一个可执行文件中。 2.embed介绍 🎯 Go 1.16 引入了革命性的 embed 包,彻底改变了静态资源管理的…...

均衡后的SNRSINR
本文主要摘自参考文献中的前两篇,相关文献中经常会出现MIMO检测后的SINR不过一直没有找到相关数学推到过程,其中文献[1]中给出了相关原理在此仅做记录。 1. 系统模型 复信道模型 n t n_t nt 根发送天线, n r n_r nr 根接收天线的 MIMO 系…...
代理篇12|深入理解 Vite中的Proxy接口代理配置
在前端开发中,常常会遇到 跨域请求接口 的情况。为了解决这个问题,Vite 和 Webpack 都提供了 proxy 代理功能,用于将本地开发请求转发到后端服务器。 什么是代理(proxy)? 代理是在开发过程中,前端项目通过开发服务器,将指定的请求“转发”到真实的后端服务器,从而绕…...

RabbitMQ入门4.1.0版本(基于java、SpringBoot操作)
RabbitMQ 一、RabbitMQ概述 RabbitMQ RabbitMQ最初由LShift和CohesiveFT于2007年开发,后来由Pivotal Software Inc.(现为VMware子公司)接管。RabbitMQ 是一个开源的消息代理和队列服务器,用 Erlang 语言编写。广泛应用于各种分布…...

接口自动化测试:HttpRunner基础
相关文档 HttpRunner V3.x中文文档 HttpRunner 用户指南 使用HttpRunner 3.x实现接口自动化测试 HttpRunner介绍 HttpRunner 是一个开源的 API 测试工具,支持 HTTP(S)/HTTP2/WebSocket/RPC 等网络协议,涵盖接口测试、性能测试、数字体验监测等测试类型…...