[ffmpeg オーディオとビデオの同期] ffmpeg オーディオとビデオの複数スレッド間のデータ同期の問題を解決します。


1 はじめに

オーディオとビデオの同期 (オーディオとビデオの同期) は、オーディオとビデオの処理における重要な問題であり、特に組み込みシステムやリアルタイム システムでは、オーディオとビデオの同期はユーザー エクスペリエンスを確保するための重要な要素です。実際のアプリケーションでは、タイムベースやレイテンシが異なる可能性があるさまざまなソースからのオーディオ ストリームとビデオ ストリームを処理する必要があることがよくあります。オーディオとビデオを確実に同時に再生するには、これらのストリームを正確に同期する必要があります。

このブログでは、C++ マルチスレッド テクノロジを使用してオーディオとビデオの同期の問題を解決する方法について詳しく説明します。まず、タイムスタンプ (プレゼンテーション タイム スタンプ、PTS) やタイム ベース (タイム ベース) など、オーディオとビデオの同期の主要な概念を紹介します。次に、これらの概念を使用してオーディオとビデオ間の時間差を計算し、ビデオ フレームの再生を遅らせることで同期を実現する方法を示します。最後に、C++ マルチスレッド技術を使用してこの同期戦略を実装する方法を示し、データ競合と期限切れの時間差を回避する方法について説明します。

この記事では、共有データを保護するためのミューテックス ( ) の使用方法、遅延用の関数std::mutexの使用方法std::this_thread::sleep_for、マルチスレッド プログラムのパフォーマンスの最適化方法など、C++ のマルチスレッド プログラミング テクニックに特に注目していきます。これらの高度なトピックをよりよく理解して適用できるように、具体的なコード例と詳細なコメントを通じてこれらのテクニックを説明します。

2. オーディオとビデオの同期の主要な概念

オーディオとビデオの同期のプログラミングを実践する前に、いくつかの基本的な概念と原則を理解する必要があります。これらの概念には、タイム スタンプ (プレゼンテーション タイム スタンプ、PTS)、時間ベース、およびタイム スタンプのデータ タイプの選択が含まれます。

タイムスタンプ (プレゼンテーション タイム スタンプ、PTS)

オーディオおよびビデオの処理では、オーディオまたはビデオの各フレームに、プレゼンテーション タイム スタンプ (プレゼンテーション タイム スタンプ、PTS) と呼ばれるタイム スタンプが関連付けられます。このタイムスタンプは、フレームをいつ再生するかを示します。たとえば、ビデオの最初のフレームのタイムスタンプが 0、2 番目のフレームのタイムスタンプが 0.033 である場合、これは 2 番目のフレームは再生開始から 0.033 秒後に表示される必要があることを意味します。

FFmpeg では、タイムスタンプは通常、double型の変数で表され、単位は秒です。この設計の主な理由の 1 つは、double型の変数がより広範囲の値を格納でき、精度が高いことです。これは、通常、マイクロ秒レベルまで正確である必要があるビデオのタイムスタンプを扱う場合に非常に重要です。

オーディオとビデオのタイムベース

オーディオ データとビデオ データを扱う場合、通常、オーディオとビデオには独自のタイム ベースがあります。このタイムベースは、各フレームの継続時間を秒単位で表す値です。たとえば、30 フレーム/秒のビデオの場合、その時間ベースは 1/30=0.0333333 秒です。

人間の耳は音の遅延に対してより敏感であるため、オーディオとビデオを同期する場合、通常はオーディオのタイムベースを基準として選択します。次に、オーディオとビデオのタイムスタンプに基づいて、それらの間の時間差を計算し、ビデオの再生の進行状況を制御します。

タイムスタンプのデータ型の選択

プログラミングでは、データを保存および処理するためにさまざまなタイプの変数を選択できます。FFmpeg では、主に次の理由から、(64 ビット符号なし整数)ではなくpts(プレゼンテーション タイム スタンプ) 型として設計されています。doubleuint64_t

  1. 精度と範囲:double型の変数は、より広い範囲の値を格納でき、精度が高くなります。これは、通常、マイクロ秒レベルまで正確である必要があるビデオのタイムスタンプを扱う場合に非常に重要です。
  2. 時間単位: FFmpeg ではpts秒単位です。つまり、小数部分を表現できる必要があります。またuint64_t、小数ではなく、整数のみを表すことができます。
  3. 操作の容易さ:型の変数は、加算、減算、乗算、除算などの演算、特に浮動小数点数を含む演算doubleを扱う場合よりも便利です。uint64_t
  4. 互換性: 一部のコーデック ライブラリまたはハードウェア デバイスでは、タイプ タイムスタンプの使用が必要な場合がありますdouble。これらのデバイスと互換性を保つために、FFmpeg はdoubleタイプを使用して を表す必要がありますpts

上記は、オーディオとビデオの同期におけるいくつかの重要な概念です。これらの概念を理解した後、オーディオとビデオの同期戦略の実装を開始できます。次のセクションでは、C++ マルチスレッド テクノロジを使用してオーディオとビデオの同期を実現する方法を詳しく紹介します。

3. オーディオとビデオの同期の基本戦略

オーディオおよびビデオ ストリームを処理する場合、オーディオとビデオの同期 (略して AV Sync) が重要な問題になります。通常、オーディオ データとビデオ データは別々にエンコードおよびデコードされるため、オーディオとビデオを同期できるように再生速度を合理的に制御する必要があります。このプロセスには複数の手順が含まれており、この章で詳しく説明します。

3.1 オーディオのタイムスタンプに基づいて再生する

マルチメディア システムでは、通常、タイムスタンプに基づいてオーディオを再生します。人間の耳は音の遅延に対して敏感であるため、音声に遅延があると、聴衆はすぐにそれに気づきます。そこで、再生のベースラインとして音声を設定し、音声に合わせてビデオの再生速度を調整します。

以下は、オーディオのタイムスタンプに基づいて再生する方法を示す簡単な例です。

while (true) {
    
    
    // 获取音频帧
    AVFrame & audio_frame = audio_buffer->front();
    // 计算音频帧的时间戳(毫秒)
    double audio_pts = audio_frame.pts * m_audio_time_base * 1000;
    // 播放音频帧
    play_audio_frame(audio_frame);
    // 根据音频帧的时间戳进行延迟
    std::this_thread::sleep_for(std::chrono::milliseconds(static_cast<int>(audio_pts)));
}

この例では、まずバッファからオーディオ フレームを取得し、次にオーディオ フレームのタイムスタンプを計算してミリ秒に変換します。次に、オーディオ フレームを再生し、オーディオ フレームのタイムスタンプに従って遅延します。こうすることで、オーディオ フレームが正しい速度で再生されることを保証できます。

3.2 音声とビデオの時間差を計算する

オーディオのタイムスタンプに基づいて再生する際、オーディオとビデオの時間差も計算する必要があります。時間差は、オーディオ フレームのタイムスタンプとビデオ フレームのタイムスタンプの差であり、この差を使用してビデオの再生速度を調整し、オーディオと同期させることができます。

以下は、オーディオとビデオ間の時間差を計算する方法を示す簡単な例です。

// 获取音频和视频帧
AVFrame & video_frame = video_buffer->front();
AVFrame & audio_frame = audio_buffer->front();
// 计算音频和视频帧的时间戳(毫秒)
double video_pts = video_frame.pts * m_video_time_base * 1000;
double audio_pts = audio_frame.pts * m_audio_time_base * 1000;
// 计算音频和视频的时间差
double diff = video_pts - audio_pts;

この例では、最初にオーディオ フレームとビデオ フレームをバッファからフェッチし、次にそれらのタイムスタンプを計算してミリ秒に変換します。次に、オーディオとビデオの時間差を計算し、この差が調整する必要がある時間となります。

3.3 ビデオフレームの遅延再生による同期

オーディオとビデオの時間差を取得した後、ビデオ フレームの再生を遅延させることでオーディオとビデオの同期を実現できます。ビデオ フレームのタイムスタンプがオーディオ フレームのタイムスタンプよりも速い場合は、ビデオ フレームの再生を遅らせる必要があります。そうでない場合、ビデオ フレームのタイムスタンプがオーディオ フレームのタイムスタンプよりも遅い場合は、ビデオ フレームをすぐに再生する必要があります。

以下は、ビデオ フレームの再生を遅延させてオーディオとビデオを同期する方法を示す簡単な例です。

if (diff > 0) {
    
    
    // 如果视频帧的时间戳快于音频帧的时间戳,那么就需要延迟视频帧的播放
    std::this_thread::sleep_for(std::chrono::milliseconds(static_cast<int>(diff)));
}
// 播放视频帧
play_video_frame(video_frame);

この例では、まず音声と映像の時間差を確認します。時間差が 0 より大きい場合、ビデオ フレームの再生を遅らせる必要があります。遅延させる関数を使用しますstd::this_thread::sleep_for。遅延時間は音声とビデオの時間差です。次に、ビデオ フレームを再生します。このようにして、ビデオ フレームの再生がオーディオ フレームの再生と同期していることを確認できます。

4. C++ マルチスレッドを使用してオーディオとビデオの同期を実現します。

在音视频处理中,音频(Audio)和视频(Video)通常被单独处理和播放,这就需要我们实现一种机制,使得音频和视频能够同步播放。C++ 的多线程(Multithreading)技术为我们提供了一种实现这种机制的方法。在本章中,我们将详细介绍如何使用C++多线程来实现音视频同步。

创建独立的音频和视频播放线程

在多线程编程中,线程(Thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运算单位。在同一个进程中的多个线程之间,线程是彼此独立的,但它们共享进程的内存空间。

我们可以创建两个线程,一个用于播放音频,另一个用于播放视频。这两个线程可以并行运行,从而实现音频和视频的同步播放。

以下是创建音频和视频播放线程的示例代码:

// 创建音频播放线程
std::thread audioThread(&PlayMangent::playAudio, this);

// 创建视频播放线程
std::thread videoThread(&PlayMangent::playVideo, this);

// 等待音频播放线程结束
audioThread.join();

// 等待视频播放线程结束
videoThread.join();

在这段代码中,我们使用 std::thread 类的构造函数来创建线程。这个构造函数接受一个成员函数指针和一个类对象指针,然后创建一个新的线程,并在这个线程中调用指定的成员函数。playAudioplayVideo 函数应该包含音频和视频播放的相关代码。

使用互斥锁保护共享数据

在多线程环境下,数据竞争(Data Race)是一个常见的问题。当多个线程同时访问同一块内存区域,并且至少有一个线程在进行写操作,而且这些线程没有进行任何同步操作,这就会导致数据竞争。

为了避免数据竞争,我们需要使用某种同步机制来保护共享数据。在C++中,互斥锁(Mutex)是一种常用的同步机制。互斥锁可以保证在任何时刻,最多只有一个线程能够访问被保护的数据。

在我们的例子中,音频和视频播放的时间差(milliseconds_diff)是被两个线程共享的数据,所以我们需要使用互斥锁来保护它。

以下是使用互斥锁保护 milliseconds_diff 的示例代码:

std::mutex mtx;  // 创建一个互斥锁

// 在音频播放线程中更新milliseconds_diff
{
    
    
    std::lock_guard<std::mutex> lock(mtx);  // 锁定互斥锁
    milliseconds_diff = calculateDiff();  // 计算音频和视频的时间差
}  // 互斥锁在lock_guard对象销毁时自动解锁

// 在视频播放线程中读取milliseconds_diff
int diff;
{
    
    
    std::lock_guard<std::mutex> lock(mtx);  // 锁定互斥锁
    diff = milliseconds_diff;  // 读取音频和视频的时间差
}  // 互斥锁在lock_guard对象销毁时自动解锁

在这段代码中,我们使用 std::lock_guard 对象来管理互斥锁的锁定和解锁。当创建 std::lock_guard 对象时,互斥锁会被锁定;当 std::lock_guard 对象超出其作用范围时,互斥锁会被自动解锁。这样可以确保即使在异常情况下,互斥锁也能被正确地解锁。

使用 std::this_thread::sleep_for 函数进行延迟

为了实现音视频同步,我们需要能够控制视频播放的速度。一种简单的方法是在播放每一帧视频之后,让线程暂时休眠一段时间。在C++中,我们可以使用 std::this_thread::sleep_for 函数来实现这个功能。

std::this_thread::sleep_for 函数会阻塞当前线程一段时间。这个

函数接受一个表示时间长度的参数,然后阻塞当前线程直到这段时间过去。我们可以用这个函数来实现视频播放的延迟。

以下是使用 std::this_thread::sleep_for 函数进行延迟的示例代码:

int diff;
{
    
    
    std::lock_guard<std::mutex> lock(mtx);  // 锁定互斥锁
    diff = milliseconds_diff;  // 读取音频和视频的时间差
}  // 互斥锁在lock_guard对象销毁时自动解锁

std::this_thread::sleep_for(std::chrono::milliseconds(diff));  // 休眠一段时间

在这段代码中,我们首先获取音频和视频的时间差(diff),然后使用 std::this_thread::sleep_for 函数让线程休眠 diff 毫秒。这样就可以实现视频播放的延迟,从而实现音视频同步。

5. 避免数据竞争和过期的时间差值

在多线程环境下,我们需要特别注意数据竞争(Data Race)和过期的时间差值(Stale Difference)的问题。下面,我们将详细讨论这两个问题,并给出解决方案。

5.1 数据竞争

数据竞争(Data Race)是指多个线程同时访问同一块内存区域,且至少有一个线程在进行写操作,而这些线程没有进行任何同步操作。数据竞争会导致不确定的结果,可能使程序的行为变得难以预测。

在我们的音视频同步程序中,音频和视频线程都需要访问 milliseconds_diff 这个共享数据。如果我们不进行任何同步操作,那么就可能发生数据竞争。为了解决这个问题,我们可以使用互斥锁(Mutex)来保护 milliseconds_diff

互斥锁是一种同步原语,可以用来保护共享数据,避免数据竞争。当一个线程锁定互斥锁时,其他线程就不能锁定这个互斥锁,必须等待这个互斥锁被解锁后才能继续执行。这样就可以确保在任何时刻,只有一个线程能够访问被互斥锁保护的数据。

在 C++ 中,我们可以使用 std::mutex 类来创建互斥锁,使用 std::lock_guard 类来管理互斥锁的生命周期。std::lock_guard 是一个 RAII 风格的类,它在构造函数中锁定互斥锁,在析构函数中解锁互斥锁。这样可以确保即使在异常情况下,互斥锁也能被正确地解锁。

以下是一个示例代码:

std::mutex mtx;  // 创建互斥锁

void update_data() {
    
    
    std::lock_guard<std::mutex> lock(mtx);  // 锁定互斥锁
    // 更新共享数据
}

void read_data() {
    
    
    std::lock_guard<std::mutex> lock(mtx);  // 锁定互斥锁
    // 读取共享数据
}

在这个示例中,我们使用 std::lock_guard 来保证互斥锁在需要的时候被正确地锁定和解锁,从而避免了数据竞争。

5.2 过期的时间差值

过期的时间差值(Stale Difference)是指我们在读取 milliseconds_diff 的值后,但在使用这个值之前,milliseconds_diff 的值已经被其他线程更新了。这样就可能导致我们使用了过期的 milliseconds_diff 值进行延迟。

为了解决这个问题,我们可以在互斥锁的保护下读取和更新 milliseconds_diff。这样可以确保我们读取的 milliseconds_diff 值总是最新的。

以下是一个示例代码:

std::mutex mtx;  // 创建互斥锁
int milliseconds_diff;  // 共享数据

void update_diff() {
    
    
    std::lock_guard<std::mutex> lock(mtx);  // 锁定互斥锁
    // 更新milliseconds_diff的值
}

void use_diff() {
    
    
    int diff;
    {
    
    
        std::lock_guard<std::mutex> lock(mtx);  // 锁定互斥锁
        diff = milliseconds_diff;  // 读取milliseconds_diff的值
    }
    // 使用diff进行延迟
}

在这个示例中,我们在互斥锁的保护下读取 milliseconds_diff 的值,并把它赋给局部变量 diff,然后在没有持有锁的情况下使用 diff 进行延迟。这样就可以避免使用过期的 milliseconds_diff 值,而且不会过长时间地持有锁,从而提高了程序的性能。

总的来说,数据竞争和过期的时间差值都是多线程环境下需要注意的问题。通过使用互斥锁,我们可以有效地解决这两个问题,从而实现音视频同步。

6. 优化多线程程序的性能

在音视频同步的处理中,我们通常需要创建独立的音频和视频播放线程,并使用多线程同步的技术来保护共享的数据。在这个过程中,正确和高效地使用多线程编程的技术是非常重要的。本章我们将讨论如何优化多线程程序的性能。

6.1 缩小互斥锁的保护范围

在多线程编程中,互斥锁(std::mutex)是一种常用的线程同步技术,它可以保护共享的数据不被多个线程同时访问,从而避免数据竞争的问题。然而,互斥锁的使用也会带来一些性能开销。当一个线程持有互斥锁时,其他需要访问受保护数据的线程将被阻塞,直到锁被释放。因此,我们应该尽可能地缩小互斥锁的保护范围,以减少阻塞的时间和提高程序的并行度。

考虑以下代码示例:

std::chrono::milliseconds duration;
{
    
    
    std::lock_guard<std::mutex> lock(m_sync_mutex);
    duration = std::chrono::milliseconds(milliseconds_diff);
}
std::this_thread::sleep_for(duration);

在这段代码中,我们只在互斥锁的保护下读取 milliseconds_diff 的值,并把它赋给 duration。然后我们立即释放锁,这样其他线程就可以访问 milliseconds_diff 了。最后,我们在没有持有锁的情况下休眠。这样可以确保我们在休眠期间不会阻止其他线程访问 milliseconds_diff

这种技术通常被称为“最小化锁持有时间”(Minimize Lock Duration),它是一个广泛接受的多线程编程的最佳实践。Bjarne Stroustrup 在他的《C++ Programming Language》一书中也特别强调了这一点。

6.2 在 std::this_thread::sleep_for 函数中直接使用 std::chrono::milliseconds

在C++中,std::this_thread::sleep_for 函数用于阻塞当前线程一段时间。它接受一个 std::chrono::duration 类型的参数,表示阻塞的时间长度。

在我们的音视频同步处理中,我们需要根据音视频的时间差来延迟视频帧的播放。这个时间差是一个 double 类型的值,表示时间的长度(以毫秒为单位)。为了将这个时间差转换为 std::chrono::duration 类型的值,我们使用了 std::chrono::milliseconds 类型。考虑以下代码示例:

std::this_thread::sleep_for(std::chrono::milliseconds(milliseconds_diff));

在这段代码中,我们在 std::this_thread::sleep_for 函数中直接使用了 std::chrono::milliseconds,将 milliseconds_diff 的值转换为 std::chrono::duration 类型的值。这样就省去了一步额外的赋值操作,使代码更为简洁。

这种技术是基于C++的强大类型系统和灵活的函数重载机制。在Scott Meyers的《Effective Modern C++》一书中,他也推荐使用这种方法来简化代码和提高性能。

下表总结了本章介绍的两种优化技术的对比:

技术 优点 缺点
缩小互斥锁的保护范围 减少阻塞的时间,提高程序的并行度 需要更细致的设计和编程
std::this_thread::sleep_for 函数中直接使用 std::chrono::milliseconds 简化代码,提高性能 可能会降低代码的可读性

在实际的编程中,我们应该根据具体的情况和需求,选择最适合的优化技术。

结语

在我们的编程学习之旅中,理解是我们迈向更高层次的重要一步。然而,掌握新技能、新理念,始终需要时间和坚持。从心理学的角度看,学习往往伴随着不断的试错和调整,这就像是我们的大脑在逐渐优化其解决问题的“算法”。

这就是为什么当我们遇到错误,我们应该将其视为学习和进步的机会,而不仅仅是困扰。通过理解和解决这些问题,我们不仅可以修复当前的代码,更可以提升我们的编程能力,防止在未来的项目中犯相同的错误。

我鼓励大家积极参与进来,不断提升自己的编程技术。无论你是初学者还是有经验的开发者,我希望我的博客能对你的学习之路有所帮助。如果你觉得这篇文章有用,不妨点击收藏,或者留下你的评论分享你的见解和经验,也欢迎你对我博客的内容提出建议和问题。每一次的点赞、评论、分享和关注都是对我的最大支持,也是对我持续分享和创作的动力。


阅读我的CSDN主页,解锁更多精彩内容:泡沫的CSDN主页
在这里插入图片描述

おすすめ

転載: blog.csdn.net/qq_21438461/article/details/131989667