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

系列文章目录

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



前言

  前文中已经讲述了音视频处理的流程,需要我们将音频数据和视频数据分开处理,这个时候我们就需要音视频同步操作。

一、音视频同步

  我们平常看视频的时候最烦恼的就是各种音画不同步,例如音频是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();
    }
    // 其他操作
}

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

### 实现字符串逆序输出的方法 #### 使用 Python 切片方法 Python 提供了一种简洁的方式来反转字符串,即使用切片操作。这种方法简单易懂且效率高。 ```python def reverse_string_slice(s): return s[::-1] input_str = input('请输入字符串:') reversed_str = reverse_string_slice(input_str) print(reversed_str) ``` 此代码接收用户输入的一个字符串并将其逆序打印[^2]。 #### 循环方式实现字符串逆序输出 另一种常见的方式是利用循环结构逐个字符读取原字符串,并构建一个新的逆序字符串。 ```python def reverse_string_loop(s): reversed_s = '' for char in s: reversed_s = char + reversed_s return reversed_s input_str = input('请输入字符串:') reversed_str = reverse_string_loop(input_str) print(reversed_str) ``` 这段代码同样可以完成字符串的逆向排列工作。 #### 采用栈数据结构来处理 还可以借助栈这种后进先出的数据结构特性来进行字符串翻转: ```python from collections import deque def reverse_string_stack(s): stack = deque() for char in s: stack.append(char) reversed_s = '' while stack: reversed_s += stack.pop() return reversed_s input_str = input('请输入字符串:') reversed_str = reverse_string_stack(input_str) print(reversed_str) ``` 这里展示了如何运用 `deque` 来作为栈容器以达到目的。 #### C++ 中递归算法的应用实例 对于C++而言,则可以通过递归来解决这个问题,在每次递归调用前保存当前字符直到遇到终止符再依次回溯输出这些字符形成最终的结果[^3]: ```cpp void Inverse(char A[]) { static int i = 0; char ch; std::cin >> ch; if (ch != '.') { Inverse(A); A[i++] = ch; } A[i] = '\0'; } ``` 需要注意的是上述例子是以'.'作为结束标志而非常见的'\n',实际应用可根据需求调整.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值