[synchronisation audio et vidéo ffmpeg] Résoudre le problème de synchronisation des données entre plusieurs threads dans l'audio et la vidéo ffmpeg


1. Introduction

La synchronisation audio-vidéo (synchronisation audio-vidéo) est un problème clé dans le traitement audio et vidéo, en particulier dans les systèmes embarqués et les systèmes temps réel, la synchronisation audio-vidéo est un facteur important pour assurer l'expérience utilisateur. Dans les applications pratiques, nous devons souvent traiter des flux audio et vidéo provenant de différentes sources, qui peuvent avoir des bases de temps et des latences différentes. Pour assurer la lecture simultanée de l'audio et de la vidéo, nous avons besoin d'une synchronisation précise de ces flux.

Dans ce blog, nous discuterons en profondeur de l'utilisation de la technologie multithreading C++ pour résoudre les problèmes de synchronisation audio et vidéo. Nous allons d'abord introduire les concepts clés de la synchronisation audio et vidéo, tels que l'horodatage (Presentation Time Stamp, PTS) et la base de temps (time base). Nous montrons ensuite comment utiliser ces concepts pour calculer la différence de temps entre l'audio et la vidéo et réaliser la synchronisation en retardant la lecture des images vidéo. Enfin, nous montrerons comment implémenter cette stratégie de synchronisation à l'aide des techniques de multithreading C++ et discuterons de la manière d'éviter les courses de données et les différences de temps expirées.

Dans cet article, nous accorderons une attention particulière aux techniques de programmation multithread en C++, y compris comment utiliser les mutex ( std::mutex) pour protéger les données partagées, comment utiliser std::this_thread::sleep_forles fonctions pour les retards et comment optimiser les performances des programmes multithread. Nous expliquerons ces techniques à travers des exemples de code spécifiques et des commentaires détaillés, dans l'espoir de vous aider à mieux comprendre et appliquer ces sujets avancés.

2. Concepts clés de la synchronisation audio et vidéo

Avant la pratique de la programmation de la synchronisation audio et vidéo, nous devons comprendre certains concepts et principes de base. Ces concepts incluent l'horodatage (Presentation Time Stamp, PTS), la base de temps et la sélection du type de données de l'horodatage.

Horodatage (horodatage de présentation, PTS)

Dans le traitement audio et vidéo, chaque image audio ou vidéo sera associée à un horodatage, appelé horodatage de présentation (Presentation Time Stamp, PTS). Cet horodatage indique quand l'image doit être lue. Par exemple, l'horodatage de la première image d'une vidéo peut être 0 et l'horodatage de la deuxième image peut être 0,033, ce qui signifie que la deuxième image doit être affichée 0,033 seconde après le début de sa lecture.

Dans FFmpeg, l'horodatage est généralement doublereprésenté par une variable de type et l'unité est la seconde. L'une des principales raisons de cette conception est que doubleles variables de type peuvent stocker une plage de valeurs plus large et avoir une plus grande précision. Ceci est très important lorsqu'il s'agit d'horodatages pour les vidéos, qui doivent généralement être précis jusqu'au niveau de la microseconde.

Base de temps pour l'audio et la vidéo

Lorsqu'il s'agit de données audio et vidéo, l'audio et la vidéo ont généralement leur propre base de temps. Cette base de temps est une valeur représentant la durée de chaque trame, également en secondes. Par exemple, pour une vidéo de 30 images/seconde, sa base de temps est de 1/30=0,0333333 seconde.

Lors de la synchronisation audio et vidéo, nous choisissons généralement la base de temps audio comme référence, car l'oreille humaine est plus sensible au retard du son. Ensuite, sur la base des horodatages de l'audio et de la vidéo, nous calculons la différence de temps entre eux pour contrôler la progression de la lecture de la vidéo.

Sélection du type de données d'horodatage

En programmation, nous pouvons choisir différents types de variables pour stocker et traiter les données. Dans FFmpeg, pts(Presentation Time Stamp) est conçu comme doubletype au lieu de uint64_t(entier non signé 64 bits), principalement pour les raisons suivantes :

  1. Précision et plage : doubleLes variables de type peuvent stocker une plage de valeurs plus large et avoir une plus grande précision. Ceci est très important lorsqu'il s'agit d'horodatages pour les vidéos, qui doivent généralement être précis jusqu'au niveau de la microseconde.
  2. Unité de temps : Dans FFmpeg, ptsc'est en secondes, ce qui signifie qu'il doit pouvoir représenter des parties fractionnaires. Et uint64_tne peut représenter que des entiers, pas des décimaux.
  3. Facilité d'utilisation : doubleles variables de type sont plus pratiques que lorsqu'il s'agit d'opérations telles que l'addition, la soustraction, la multiplication et la division uint64_t, en particulier les opérations impliquant des nombres à virgule flottante.
  4. Compatibilité : certaines bibliothèques de codecs ou certains périphériques matériels peuvent nécessiter l'utilisation doubled'horodatages de type. Pour être compatible avec ces périphériques, FFmpeg doit utiliser doublele type pour représenter pts.

Voici quelques concepts clés de la synchronisation audio et vidéo. Après avoir compris ces concepts, nous pouvons commencer à mettre en œuvre la stratégie de synchronisation audio et vidéo. Dans la section suivante, nous présenterons en détail comment utiliser la technologie multithreading C++ pour réaliser la synchronisation audio et vidéo.

3. Stratégies de base pour la synchronisation audio et vidéo

Lors du traitement des flux audio et vidéo, la synchronisation audio-vidéo (AV Sync en abrégé) est un problème central. Les données audio et vidéo sont généralement encodées et décodées séparément, nous devons donc contrôler raisonnablement leur vitesse de lecture pour nous assurer que l'audio et la vidéo peuvent être synchronisées. Il y a plusieurs étapes impliquées dans ce processus, que nous décrivons en détail dans ce chapitre.

3.1 Lecture basée sur l'horodatage audio

Dans un système multimédia, nous jouons généralement de l'audio en fonction de l'horodatage. Parce que l'oreille humaine est plus sensible au retard du son, s'il y a un retard dans l'audio, le public le remarquera immédiatement. Nous définissons donc l'audio comme référence pour la lecture, puis ajustons la vitesse de lecture de la vidéo pour l'aligner sur l'audio.

Voici un exemple simple montrant comment baser la lecture sur des horodatages audio :

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)));
}

Dans cet exemple, nous obtenons d'abord l'image audio du tampon, puis calculons l'horodatage de l'image audio et le convertissons en millisecondes. Ensuite, nous jouons la trame audio et le retardons en fonction de l'horodatage de la trame audio. De cette façon, nous pouvons nous assurer que les images audio sont lues à la bonne vitesse.

3.2 Calculer la différence de temps entre l'audio et la vidéo

Lors de la lecture basée sur l'horodatage audio, nous devons également calculer la différence de temps entre l'audio et la vidéo. La différence de temps est la différence entre l'horodatage de la trame audio et l'horodatage de la trame vidéo. Nous pouvons utiliser cette différence pour ajuster la vitesse de lecture de la vidéo afin de la synchroniser avec l'audio.

Voici un exemple simple montrant comment calculer la différence de temps entre l'audio et la vidéo :

// 获取音频和视频帧
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;

Dans cet exemple, nous récupérons d'abord les images audio et les images vidéo du tampon, puis calculons leurs horodatages et les convertissons en millisecondes. Ensuite, nous calculons la différence de temps entre l'audio et la vidéo, et cette différence est le temps dont nous avons besoin pour nous ajuster.

3.3 Synchronisation en retardant la lecture des trames vidéo

Après avoir obtenu la différence de temps entre l'audio et la vidéo, nous pouvons réaliser une synchronisation audio et vidéo en retardant la lecture des images vidéo. Si l'horodatage de l'image vidéo est plus rapide que l'horodatage de l'image audio, nous devons retarder la lecture de l'image vidéo ; sinon, si l'horodatage de l'image vidéo est plus lent que l'horodatage de l'image audio, nous besoin de lire l'image vidéo immédiatement.

Voici un exemple simple montrant comment synchroniser l'audio et la vidéo en retardant la lecture des images vidéo :

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

Dans cet exemple, nous vérifions d'abord la différence de temps entre l'audio et la vidéo. Si la différence de temps est supérieure à 0, la lecture de l'image vidéo doit être retardée. Nous utilisons std::this_thread::sleep_forla fonction pour retarder, et le temps de retard est la différence de temps entre l'audio et la vidéo. Ensuite, nous jouons l'image vidéo. De cette façon, nous pouvons nous assurer que la lecture des images vidéo est synchronisée avec la lecture des images audio.

4. Utilisez le multithreading C++ pour réaliser la synchronisation audio et vidéo

在音视频处理中,音频(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主页
在这里插入图片描述

Je suppose que tu aimes

Origine blog.csdn.net/qq_21438461/article/details/131989667
conseillé
Classement