嵌入式音视频开发(二)ffmpeg音视频同步

news2025/4/17 5:30:05

系列文章目录

嵌入式音视频开发(零)移植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();
    }
    // 其他操作
}

免责声明:本文参考了网上公开的部分资料,仅供学习参考使用,若有侵权或勘误请联系笔者

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2301444.html

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

相关文章

SpringBoot速成概括

视频&#xff1a;黑马程序员SpringBoot3Vue3全套视频教程&#xff0c;springbootvue企业级全栈开发从基础、实战到面试一套通关_哔哩哔哩_bilibili 图示&#xff1a;

微信小程序image组件mode属性详解

今天学习微信小程序开发的image组件&#xff0c;mode属性的属性值不少&#xff0c;一开始有点整不明白。后来从网上下载了一张图片&#xff0c;把每个属性都试验了一番&#xff0c;总算明白了。现总结归纳如下&#xff1a; 1.使用scaleToFill。这是mode的默认值&#xff0c;sc…

Matlab写入点云数据到Rosbag

最近有需要读取一个点云并做处理后&#xff0c;重新写回rosbag。网上有很多读取的教程&#xff0c;但没有写入。自己写入时也遇到了很多麻烦&#xff0c;踩了一堆坑进行记录。 1. rosbag中一个lidar的msg有哪些信息&#xff1f; 通过如下代码&#xff0c;先读取一个rosbag的l…

数据分析--数据清洗

一、数据清洗的重要性&#xff1a;数据质量决定分析成败 1.1 真实案例警示 电商平台事故&#xff1a;2019年某电商大促期间&#xff0c;因价格数据未清洗导致错误标价&#xff0c;产生3000万元损失医疗数据分析&#xff1a;未清洗的异常血压值&#xff08;如300mmHg&#xff…

用命令模式设计一个JSBridge用于JavaScript与Android交互通信

用命令模式设计一个JSBridge用于JavaScript与Android交互通信 在开发APP的过程中&#xff0c;通常会遇到Android需要与H5页面互相传递数据的情况&#xff0c;而Android与H5交互的容器就是WebView。 因此要想设计一个高可用的 J S B r i d g e JSBridge JSBridge&#xff0c;不…

Vue 3最新组件解析与实践指南:提升开发效率的利器

目录 引言 一、Vue 3核心组件特性解析 1. Composition API与组件逻辑复用 2. 内置组件与生命周期优化 3. 新一代UI组件库推荐 二、高级组件开发技巧 1. 插件化架构设计 2. 跨层级组件通信 三、性能优化实战 1. 惰性计算与缓存策略 2. 虚拟滚动与列表优化 3. Tree S…

计算机网络(涵盖OSI,TCP/IP,交换机,路由器,局域网)

一、网络通信基础 &#xff08;一&#xff09;网络通信的概念 网络通信是指终端设备之间通过计算机网络进行的信息传递与交流。它类似于现实生活中的物品传递过程&#xff1a;数据&#xff08;物品&#xff09;被封装成报文&#xff08;包裹&#xff09;&#xff0c;通过网络…

JVM-Java程序的运行环境

Java Virtual Machine Java程序的运行环境 JVM组成 程序计数器 线程私有的&#xff0c;内部保存的字节码的行号。用于记录正在执行的字节码指令的地址。 Java堆 线程共享的区域: 主要用来保存对象实例, 数组等, 当堆中没有内存空间可分配给实例也无法再扩展时, 则抛出OutOfMe…

什么是网关,网关的作用是什么?网络安全零基础入门到精通实战教程!

1. 什么是网关 网关又称网间连接器、协议转换器&#xff0c;也就是网段(局域网、广域网)关卡&#xff0c;不同网段中的主机不能直接通信&#xff0c;需要通过关卡才能进行互访&#xff0c;比如IP地址为192.168.31.9(子网掩码&#xff1a;255.255.255.0)和192.168.7.13(子网掩码…

《千恋万花》无广版手游安卓苹果免费下载直装版

自取https://pan.xunlei.com/s/VOJS77k8NDrVawqcOerQln2lA1?pwdn6k8 《千恋万花》&#xff1a;柚子社的和风恋爱杰作 《千恋万花》&#xff08;Senren * Banka&#xff09;是由日本知名美少女游戏品牌柚子社&#xff08;Yuzusoft&#xff09;于2016年推出的一款和风恋爱题材…

javaEE-14.spring MVC练习

目录 1.加法计算器 需求分析: 前端页面代码: 后端代码实现功能: 调整前端页面代码: 进行测试: 2.用户登录 需求分析: 定义接口: 1.登录数据校验接口: 2.查询登录用户接口: 前端代码: 后端代码: 调整前端代码: 测试/查错因 后端: 前端: lombok工具 1.引入依赖…

rabbitmq五种模式的实现——springboot

rabbitmq五种模式的实现——springboot 基础知识和javase的实现形式可以看我之前的博客 代码地址&#xff1a;https://github.com/9lucifer/rabbitmq4j-learning 一、进行集成 &#xff08;一&#xff09;Spring Boot 集成 RabbitMQ 概述 Spring Boot 提供了对 RabbitMQ 的自…

23. AI-大语言模型-DeepSeek赋能开发-Spring AI集成

文章目录 前言一、Spring AI 集成 DeepSeek1. 开发AI程序2. DeepSeek 大模型3. 集成 DeepSeek 大模型1. 接入前准备2. 引入依赖3. 工程配置4. 调用示例5. 小结 4. 集成第三方平台&#xff08;已集成 DeepSeek 大模型&#xff09;1. 接入前准备2. POM依赖3. 工程配置4. 调用示例…

Educational Codeforces Round 174 (Rated for Div. 2)(ABCD)

A. Was there an Array? 翻译&#xff1a; 对于整数数组 ​&#xff0c;我们将其相等特征定义为数组 &#xff0c;其中&#xff0c;如果数组 a 的第 i 个元素等于其两个相邻元素&#xff0c;则 &#xff1b;如果数组 a 的第 i 个元素不等于其至少一个相邻元素&#xff0c;则 …

如何在本机上模拟IP地址

如何在本机上模拟IP地址 前言 在某些开发或测试场景中&#xff0c;我们可能需要在本机上模拟一个指定的 IP 地址&#xff0c;并让局域网内的其他设备能够通过该 IP 访问本机提供的服务&#xff08;如 Web 服务&#xff09;。 本文将详细介绍如何在 Windows 和 macOS 系统上实…

【嵌入式Linux应用开发基础】进程间通信(1):管道

目录 一、管道的基本概念 二、管道的工作原理 三、管道的类型 3.1. 匿名管道&#xff08;Anonymous Pipe&#xff09; 3.2. 命名管道&#xff08;Named Pipe&#xff0c;FIFO&#xff09; 四、管道的读写规则 4.1. 匿名管道的读写规则 4.2. 命名管道的读写规则 五、管…

【DeepSeek】Mac m1电脑部署DeepSeek

一、电脑配置 个人电脑配置 二、安装ollama 简介&#xff1a;Ollama 是一个强大的开源框架&#xff0c;是一个为本地运行大型语言模型而设计的工具&#xff0c;它帮助用户快速在本地运行大模型&#xff0c;通过简单的安装指令&#xff0c;可以让用户执行一条命令就在本地运…

DHCP详解,网络安全零基础入门到精通实战教程!

一、DHCP简介 DHCP(Dynamic Host Configuration Protocol),动态主机配置协议&#xff0c;是一个应用层协议。当我们将客户主机ip地址设置为动态获取方式时&#xff0c;DHCP服务器就会根据DHCP协议给客户端分配IP&#xff0c;使得客户机能够利用这个IP上网。 DHCP前身是BOOTP&am…

【Prometheus】prometheus结合pushgateway实现脚本运行状态监控

✨✨ 欢迎大家来到景天科技苑✨✨ 🎈🎈 养成好习惯,先赞后看哦~🎈🎈 🏆 作者简介:景天科技苑 🏆《头衔》:大厂架构师,华为云开发者社区专家博主,阿里云开发者社区专家博主,CSDN全栈领域优质创作者,掘金优秀博主,51CTO博客专家等。 🏆《博客》:Python全…

立创实战派ESP32-S3烧录小智AI指南

小智 AI 聊天机器人-开源项目介绍 本项目是一个开源项目&#xff0c;主要用于教学目的。我们希望通过这个项目&#xff0c;能够帮助更多人入门 AI 硬件开发&#xff0c;了解如何将当下飞速发展的大语言模型应用到实际的硬件设备中。无论你是对 AI 感兴趣的学生&#xff0c;还是…