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

嵌入式音视频开发(二)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_baseAVStream流的时间基用于将 PTS 和 DTS 转换为实际时间
time_baseAVCodecContext编码器或解码器的时间基用于内部处理和同步
video_codec_timebase audio_codec_timebaseAVFormatContext格式上下文的时间基用于整体管理和同步

  值得注意的是,虽然 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 实现思路

  以音频为基准进行同步的基本思路是:

  1. 选择音频流作为同步基准
  2. 解码音频数据并更新当前音频时间戳
  3. 解码视频数据并根据音频时间戳调整视频帧的显示时间,确保音视频同步
  4. 通过适当的缓冲控制,确保播放的流畅性和稳定性

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 实现思路

  以外部时钟为基准进行同步的基本思路是:

  1. 使用外部时钟(如系统时钟)作为基准
  2. 解码音频数据包,根据外部时钟调整音频播放时间
  3. 解码视频数据包,根据外部时钟调整视频帧的显示时间
  4. 通过适当的缓冲控制,确保播放的流畅性和稳定性

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音视频同步

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

Mongodb数据管理

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

Django 创建表 choices的妙用:get_<field_name>_display()

1.定义choices 我在创建表时&#xff0c;对于性别这个字段&#xff0c;定义了choices 选项&#xff0c;1代表男&#xff0c;2代表女 mysql中表的数据如下&#xff0c;里面存储的是1或2 如果我们在网页上展示的时候&#xff0c;想展示“男”或“女”&#xff0c;而不是数字1或2…...

Spring Boot 集成 Kettle

Kettle 简介 Kettle 最初由 Matt Casters 开发&#xff0c;是 Pentaho 数据集成平台的一部分。它提供了一个用户友好的界面和丰富的功能集&#xff0c;使用户能够轻松地设计、执行和监控 ETL 任务。Kettle 通过其强大的功能和灵活性&#xff0c;帮助企业高效地处理大规模数据集…...

自学Java-面向对象高级(final、单例类、枚举类、抽象类、接口)

自学Java-面向对象高级&#xff08;final、单例类、枚举类、抽象类、接口&#xff09; 一、final关键字1、认识final关键字2、final修饰变量的注意3、常量 二、单例类&#xff08;设计模式&#xff09;1、设计模式的概念2、单例设计模式3、单例类有很多形式4、懒汉式单例类5、小…...

Hutool - Cache:简单而强大的缓存实现

目录 1. 缓存简介 2. 引入依赖 3. 常见缓存类型及使用示例 3.1 FIFO 缓存&#xff08;先进先出缓存&#xff09; 3.2 LRU 缓存&#xff08;最近最少使用缓存&#xff09; 3.3 定时缓存 4. 缓存的基本操作 5. 总结 1. 缓存简介 在软件开发中&#xff0c;缓存是一种常用的…...

DeepSeek 通过 API 对接第三方客户端 告别“服务器繁忙”

本文首发于只抄博客&#xff0c;欢迎点击原文链接了解更多内容。 前言 上一期分享了如何在本地部署 DeepSeek R1 模型&#xff0c;但通过命令行运行的本地模型&#xff0c;问答的交互也要使用命令行&#xff0c;体验并不是很好。这期分享几个第三方客户端&#xff0c;涵盖了桌…...

Python 基础-循环

目录 简介 break continue 小结 简介 要计算123&#xff0c;我们可以直接写表达式&#xff1a; >>> 1 2 3 6要计算123...10&#xff0c;勉强也能写出来。 但是&#xff0c;要计算123...10000&#xff0c;直接写表达式就不可能了。 为了让计算机能计算成千上…...

Java和SQL测试、性能监控中常用工具

下面我会详细列举一些在Java和SQL测试、调试、性能监控中常用的工具&#xff0c;并结合项目中提到的各个技术点说明如何选择合适的工具和方法。 一、Java项目常用的测试、调试与性能监控工具 单元测试与集成测试&#xff1a;JUnit/TestNG&#xff1a; 用于编写单元测试和集成测…...

SQL 注入攻击详解[基础篇]:Web 应用程序安全漏洞与防御策略

目录 SQL注入的简介 现代 Web 应用程序中的数据库交互与 SQL 注入攻击 数据库管理系统&#xff08;DBMS&#xff09;架构与 SQL 注入 什么是 SQL 注入&#xff1f; SQL 注入的工作原理 SQL 注入的用例与影响 如何预防 SQL 注入&#xff1f; 数据库分类 数据库类型&am…...

【ArcGIS Pro二次开发】(87):样式_Style的用法

.Stylx类型的文件即为样式库文件&#xff0c;保存了符号样式。 1、根据名字获取当前工程中的style //获取当前工程中的所有style var ProjectStyles Project.Current.GetItems<StyleProjectItem>();//根据名字找出指定的style StyleProjectItem style ProjectStyles.F…...

DEX-EE三指灵巧手:扩展AI与机器人研究的边界

DEX-EE三指灵巧手&#xff0c;由Shadow Robot与Google DeepMind合作开发&#xff0c;以其先进技术和设计&#xff0c;正在引领AI与机器人研究的新趋势。其高精度传感器和灵活的机械手指&#xff0c;能够捕捉复杂的环境数据&#xff0c;为强化学习实验提供了可靠支持。 Shadow R…...

简站主题:简洁、实用、SEO友好、安全性高和后期易于维护的wordpress主题

简站主题以其简洁的设计风格、实用的功能、优化的SEO性能和高安全性而受到广泛好评。 简洁&#xff1a;简站主题采用扁平化设计风格&#xff0c;界面简洁明了&#xff0c;提供多种布局和颜色方案&#xff0c;适合各种类型的网站&#xff0c;如个人博客和企业网站。 实用&…...

23种设计模式 - 责任链

模式定义 责任链模式&#xff08;Chain of Responsibility Pattern&#xff09;是一种行为型设计模式&#xff0c;允许多个对象按链式顺序处理请求&#xff0c;直到其中一个对象处理为止。该模式将请求的发送者和接收者解耦&#xff0c;使多个对象都有机会处理请求。 模式结构…...

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 业务场景说明 …...

算法——舞蹈链算法

一&#xff0c;基本概念 算法简介 舞蹈链算法&#xff08;Dancing Links&#xff0c;简称 DLX&#xff09;是一种高效解决精确覆盖问题的算法&#xff0c;实际上是一种数据结构&#xff0c;可以用来实现 X算法&#xff0c;以解决精确覆盖问题。由高德纳&#xff08;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&#xff1…...

若依Flowable工作流版本监听器使用方法

1.前言 本文详细介绍如何在若依Flowable工作流版本&#xff08;RuoYi-Vue-Flowable&#xff09;中配置执行监听器和任务监听器。是以我二次开发的代码为基础&#xff0c;介绍如何配置监听器&#xff0c;已解决源码在新增或删除监听器出现的问题&#xff0c;如果需要二次开发的…...

机器视觉--图像的运算(乘法)

一、引言 在图像处理领域&#xff0c;Halcon 是一款功能强大且广泛应用的机器视觉软件库。它提供了丰富的算子和工具&#xff0c;能够满足各种复杂的图像处理需求。图像的乘法运算作为其中一种基础操作&#xff0c;虽然不像一些边缘检测、形态学处理等操作那样被频繁提及&…...

突破反爬困境:从服务端渲染到客户端SPA,爬虫环境的演变与新挑战(一)

声明 本文所讨论的内容及技术均纯属学术交流与技术研究目的&#xff0c;旨在探讨和总结互联网数据流动、前后端技术架构及安全防御中的技术演进。文中提及的各类技术手段和策略均仅供技术人员在合法与合规的前提下进行研究、学习与防御测试之用。 作者不支持亦不鼓励任何未经授…...

Swift 协议扩展精进之路:解决 CoreData 托管实体子类的类型不匹配问题(下)

概述 在 Swift 开发语言中&#xff0c;各位秃头小码农们可以充分利用语法本身所带来的便利去劈荆斩棘。我们还可以恣意利用泛型、协议关联类型和协议扩展来进一步简化和优化我们复杂的代码需求。 不过&#xff0c;在涉及到多个子类派生于基类进行多态模拟的场景下&#xff0c;…...

关于nvm与node.js

1 安装nvm 安装过程中手动修改 nvm的安装路径&#xff0c; 以及修改 通过nvm安装node后正在使用的node的存放目录【这句话可能难以理解&#xff0c;但接着往下看你就了然了】 2 修改nvm中settings.txt文件配置 nvm安装成功后&#xff0c;通常在该文件中会出现以下配置&…...

视频字幕质量评估的大规模细粒度基准

大家读完觉得有帮助记得关注和点赞&#xff01;&#xff01;&#xff01; 摘要 视频字幕在文本到视频生成任务中起着至关重要的作用&#xff0c;因为它们的质量直接影响所生成视频的语义连贯性和视觉保真度。尽管大型视觉-语言模型&#xff08;VLMs&#xff09;在字幕生成方面…...

论文解读:交大港大上海AI Lab开源论文 | 宇树机器人多姿态起立控制强化学习框架(一)

宇树机器人多姿态起立控制强化学习框架论文解析 论文解读&#xff1a;交大&港大&上海AI Lab开源论文 | 宇树机器人多姿态起立控制强化学习框架&#xff08;一&#xff09; 论文解读&#xff1a;交大&港大&上海AI Lab开源论文 | 宇树机器人多姿态起立控制强化…...

第 86 场周赛:矩阵中的幻方、钥匙和房间、将数组拆分成斐波那契序列、猜猜这个单词

Q1、[中等] 矩阵中的幻方 1、题目描述 3 x 3 的幻方是一个填充有 从 1 到 9 的不同数字的 3 x 3 矩阵&#xff0c;其中每行&#xff0c;每列以及两条对角线上的各数之和都相等。 给定一个由整数组成的row x col 的 grid&#xff0c;其中有多少个 3 3 的 “幻方” 子矩阵&am…...

以光量子为例,详解量子获取方式

光量子技术获取量子比特可在室温下进行。该方式有望通过与名为硅光子学&#xff08;silicon photonics&#xff09;的光波导&#xff08;optical waveguide&#xff09;芯片制造技术和光纤等光通信技术相结合来实现量子计算机。量子力学中&#xff0c;光既是波又是粒子。光子本…...

Spring是如何解决Bean的循环依赖:三级缓存机制

1、什么是 Bean 的循环依赖 在 Spring框架中,Bean 的循环依赖是指多个 Bean 之间‌互相持有对方引用‌,形成闭环依赖关系的现象。 多个 Bean 的依赖关系构成环形链路,例如: 双向依赖:Bean A 依赖 Bean B,同时 Bean B 也依赖 Bean A(A↔B)。链条循环: Bean A → Bean…...

【JVM】Java虚拟机(二)——垃圾回收

目录 一、如何判断对象可以回收 &#xff08;一&#xff09;引用计数法 &#xff08;二&#xff09;可达性分析算法 二、垃圾回收算法 &#xff08;一&#xff09;标记清除 &#xff08;二&#xff09;标记整理 &#xff08;三&#xff09;复制 &#xff08;四&#xff…...

逻辑回归暴力训练预测金融欺诈

简述 「使用逻辑回归暴力预测金融欺诈&#xff0c;并不断增加特征维度持续测试」的做法&#xff0c;体现了一种逐步建模与迭代验证的实验思路&#xff0c;在金融欺诈检测中非常有价值&#xff0c;本文作为一篇回顾性记录了早年间公司给某行做反欺诈预测用到的技术和思路。百度…...

springboot 日志类切面,接口成功记录日志,失败不记录

springboot 日志类切面&#xff0c;接口成功记录日志&#xff0c;失败不记录 自定义一个注解方法 import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target;/***…...