【FFmpeg 视频播放】深入理解多媒体播放:同步策略、缓冲技术与性能优化(二)

简介: 【FFmpeg 视频播放】深入理解多媒体播放:同步策略、缓冲技术与性能优化

【FFmpeg 视频播放】深入理解多媒体播放:同步策略、缓冲技术与性能优化(一)https://developer.aliyun.com/article/1467274


6. 数据有效性和一致性的保证

在多媒体播放中,我们经常需要处理大量的数据,例如音频和视频帧。为了提高播放的流畅性,我们可能会使用各种策略,例如缓冲、预处理和双缓冲队列。然而,这些策略可能会带来一些挑战,特别是在保证数据的有效性和一致性方面。在本章中,我们将探讨如何解决这些挑战。

6.1 帧标记的使用

在处理音视频帧时,我们可能需要知道某个帧是否已经被处理过,例如是否已经被转换为 RGB 图像。一种可能的解决方案是在 AVFrame 中添加一个标记,表示该帧是否已经被转换。转换线程在转换一个帧之后,将该帧的标记设置为已转换。播放线程在播放一个帧之前,检查该帧的标记,如果该帧已经被转换,那么就可以直接播放,否则需要等待或者采取其他的处理策略。

这种方法的优点是只需要一个队列,但可能需要修改 AVFrame 的结构或者使用额外的数据结构来存储标记。例如,我们可以使用一个 std::unordered_map,以 AVFrame 的地址或者时间戳(PTS)为键,标记为值。

6.2 锁和条件变量的使用

在多线程环境中,我们需要确保数据的一致性,避免数据竞争。一种可能的解决方案是使用锁(例如,互斥锁)和条件变量。

互斥锁可以保证在同一时间只有一个线程可以访问某个数据。例如,我们可以使用一个互斥锁来保护 AVFrame 队列和 RGB 队列,当一个线程需要访问队列时,它需要首先获得锁,然后才能进行操作。

条件变量可以让一个线程等待某个条件成立。例如,我们可以使用一个条件变量来让播放线程等待 RGB 队列中有可播放的帧。当转换线程转换完成一个帧并将其添加到 RGB 队列后,它可以使用条件变量来通知播放线程。

以下是一个使用 C++11 的 std::mutex 和 std::condition_variable 的示例:

std::queue<AVFrame*> avframe_queue;
std::queue<RGBImage*> rgb_queue;
std::mutex mtx;
std::condition_variable cv;
// 转换线程的主循环
while (true) {
    AVFrame* frame = get_next_avframe();
    RGBImage* image = convert_to_rgb(frame);
    {
        std::lock_guard<std::mutex> lock(mtx);
        avframe_queue.push(frame);
        rgb_queue.push(image);
    }
    cv.notify_one();
}
// 播放线程的主循环
while (true) {
    std::unique_lock<std::mutex> lock(mtx);
    cv.wait(lock, [&]{ return !rgb_queue.empty(); });
    AVFrame* frame = avframe_queue.front();
    avframe_queue.pop();
    RGBImage* image = rgb_queue.front();
    rgb_queue.pop();
    play_image(image);
}

在这个示例中,转换线程和播放线程共享 AVFrame 队列和 RGB 队列。他们使用一个互斥锁来保护队列的操作,避免数据竞争。转换线程在转换完成一个帧后,使用条件变量来通知播放线程。播放线程在收到通知后,从队列中取出并播放帧。

这种方法的优点是可以确保数据的一致性,避免数据竞争。但是,需要注意的是,锁和条件变量可能会引入额外的等待时间,特别是在有多个线程频繁访问队列的情况下。为了减少等待时间,你可以使用其他的同步机制,例如读写锁(例如,std::shared_mutex),这样,多个线程可以同时读取队列,但只有一个线程可以写入队列。

在下图中,我们可以看到双缓冲队列的设计和工作流程:

在这个图中,我们可以看到转换线程和播放线程如何交互地操作 AVFrame 队列和 RGB 队列。转换线程从 AVFrame 队列中取出帧,转换为 RGB 图像,然后将 RGB 图像添加到 RGB 队列。播放线程从 RGB 队列中取出 RGB 图像并播放,然后将播放过的 RGB 图像和对应的 AVFrame 从队列中移除。这个过程通过互斥锁和条件变量来同步,以确保数据的一致性和有效性。

7. 时间戳(PTS)映射的设计与优化

在多媒体播放中,时间戳(Presentation Time Stamp,简称 PTS)是一个关键的概念,它用于标记音频或视频帧的播放时间。在处理音视频数据时,我们通常需要将 AVFrame(音视频帧)和对应的 RGB 图像关联起来,以便在播放时能够找到对应的 RGB 图像。这就需要我们设计一个有效的映射(Mapping)机制。

7.1 时间戳映射的基本概念

时间戳映射是一种数据结构,它将时间戳(PTS)和对应的 RGB 图像关联起来。在 C++ 中,我们可以使用 std::map 或 std::unordered_map 来实现这种映射。例如,我们可以定义一个 std::map<int64_t, RGBImage>,其中 int64_t 是时间戳,RGBImage 是 RGB 图像的数据类型。

在使用时间戳映射时,我们需要注意以下几点:

  • 映射的键值(时间戳)应该是唯一的。如果有两个帧具有相同的时间戳,那么它们不能同时存在于映射中。
  • 映射的值(RGB 图像)可能会占用大量的内存。因此,我们需要设计一种有效的内存管理策略,例如,定期清理不再需要的条目。
  • 映射可能会被多个线程同时访问,例如,一个线程负责将 AVFrame 转换为 RGB 图像并添加到映射中,另一个线程负责从映射中读取 RGB 图像并播放。因此,我们需要使用某种同步机制,例如互斥锁,来保护映射,避免数据竞争。

下面是一个简单的示例,展示了如何使用 std::map 来存储时间戳映射:

#include <map>
// 假设 RGBImage 是 RGB 图像的数据类型
typedef std::vector<uint8_t> RGBImage;
// 创建一个映射
std::map<int64_t, RGBImage> pts_mapping;
// 添加一个条目
int64_t pts = ...;  // 时间戳
RGBImage image = ...;  // RGB 图像
pts_mapping[pts] = image;
// 读取一个条目
RGBImage image = pts_mapping[pts];
// 删除一个条目
pts_mapping.erase(pts);

7.2 映射的更新和管理策略

在使用时间戳映射时,我们需要定期更新映射,添加新的条目,删除不再需要的条目。这是因为随着播放的进行,我们会不断地处理新的帧,生成新的 RGB 图像,同时,已经播放过的帧和 RGB 图像就不再需要了。

一种可能的策略是使用一个固定大小的缓冲区来存储映射的条目。当缓冲区满时,我们可以覆盖最旧的条目。这样,我们就可以保持映射的大小固定,避免消耗过多的内存。例如,我们可以定义一个 std::map<int64_t, RGBImage>,并限制它的大小不超过一定的值,例如 100。当我们需要添加一个新的条目,但映射已经满了时,我们可以删除最旧的条目,然后再添加新的条目。

另一种可能的策略是定期清理映射,移除过期或者不再需要的条目。例如,我们可以在每次播放一个帧之后,检查映射,删除所有时间戳小于当前播放时间的条目。这样,我们就可以保证映射中只保留还未播放的帧。

下面是一个简单的示例,展示了如何更新和管理映射:

#include <map>
// 假设 RGBImage 是 RGB 图像的数据类型
typedef std::vector<uint8_t> RGBImage;
// 创建一个映射
std::map<int64_t, RGBImage> pts_mapping;
// 添加一个条目
int64_t pts = ...;  // 时间戳
RGBImage image = ...;  // RGB 图像
if (pts_mapping.size() >= 100) {
    // 如果映射已经满了,删除最旧的条目
    pts_mapping.erase(pts_mapping.begin());
}
pts_mapping[pts] = image;
// 清理映射
int64_t current_pts = ...;  // 当前播放时间
for (auto it = pts_mapping.begin(); it != pts_mapping.end(); ) {
    if (it->first < current_pts) {
        // 如果条目的时间戳小于当前播放时间,删除该条目
        it = pts_mapping.erase(it);
    } else {
        ++it;
    }
}

7.3 处理找不到对应时间戳的情况

在使用时间戳映射时,我们可能会遇到找不到对应时间戳的情况。这可能是因为 RGB 图像还没有准备好,或者已经被移除。在这种情况下,我们需要设计一个默认的处理策略。

一种可能的策略是返回一个默认的 RGB 图像。例如,我们可以定义一个特殊的 RGB 图像,当我们找不到对应时间戳的 RGB 图像时,就返回这个特殊的 RGB 图像。这样,我们就可以保证播放线程总是能够得到一个 RGB 图像,避免播放中断。

另一种可能的策略是等待直到 RGB 图像准备好。例如,我们可以使用一个条件变量来同步播放线程和转换线程。当播放线程找不到对应时间戳的 RGB 图像时,它就等待条件变量。当转换线程生成一个新的 RGB 图像并添加到映射中时,它就通知条件变量。这样,播放线程就可以在 RGB 图像准备好时立即得到它。

下面是一个简单的示例,展示了如何处理找不到对应时间戳的情况:

#include <map>
#include <condition_variable>
#include <mutex>
// 假设 RGBImage 是 RGB 图像的数据类型
typedef std::vector<uint8_t> RGBImage;
// 创建一个映射
std::map<int64_t, RGBImage> pts_mapping;
// 创建一个互斥锁和一个条件变量
std::mutex mtx;
std::condition_variable cv;
// 读取一个条目
int64_t pts = ...;  // 时间戳
RGBImage image;
{
    std::unique_lock<std::mutex> lock(mtx);
    while (pts_mapping.find(pts) == pts_mapping.end()) {
        // 如果找不到对应时间戳的 RGB 图像,等待条件变量
        cv.wait(lock);
    }
    image = pts_mapping[pts];
}
// 添加一个条目
int64_t pts = ...;  // 时间戳
RGBImage image = ...;  // RGB 图像
{
    std::lock_guard<std::mutex> lock(mtx);
    pts_mapping[pts] = image;
}
// 通知条件变量
cv.notify_all();

7.4 映射的同步问题和解决方案

在使用时间戳映射时,我们需要注意同步问题。因为映射可能会被多个线程同时访问,例如,一个线程负责将 AVFrame 转换为 RGB 图像并添加到映射中,另一个线程负责从映射中读取 RGB 图像并播放。如果我们不同步这些操作,就可能会出现数据竞争,导致程序的行为不确定。

一种解决方案是使用互斥锁来保护映射。我们可以在每次访问映射时都获取互斥锁,然后在访问完成后释放互斥锁。这样,我们就可以保证在任何时刻,只有一个线程能够访问映射。

另一种解决方案是使用读写锁。读写锁允许多个线程同时读取数据,但只允许一个线程写入数据。这比互斥锁更灵活,因为它允许多个线程同时读取映射,只有当一个线程需要写入映射时,才需要阻止其他线程访问映射。

下面是一个简单的示例,展示了如何使用互斥锁来同步映射的访问:

#include <map>
#include <mutex>
// 假设 RGBImage 是 RGB 图像的数据类型
typedef std::vector<uint8_t> RGBImage;
// 创建一个映射
std::map<int64_t, RGBImage> pts_mapping;
// 创建一个互斥锁
std::mutex mtx;
// 添加一个条目
int64_t pts = ...;  // 时间戳
RGBImage image = ...;  // RGB 图像
{
    std::lock_guard<std::mutex> lock(mtx);
    pts_mapping[pts] = image;
}
// 读取一个条目
RGBImage image;
{
    std::lock_guard<std::mutex> lock(mtx);
    image = pts_mapping[pts];
}
// 删除一个条目
{
    std::lock_guard<std::mutex> lock(mtx);
    pts_mapping.erase(pts);
}

在实际的多媒体播放中,时间戳映射的设计和优化是一个复杂的问题,需要考虑许多因素,例如内存管理,同步机制,错误处理等。但是,通过深入理解这些概念和技术,我们可以设计出高效,稳定,易于维护的多媒体播放系统。

以下是一个简单的示意图,描述了时间戳映射的基本工作流程:

在这个示意图中,我们可以看到,转换线程从 AVFrame 队列中读取帧,转换为 RGB 图像,然后将 RGB 图像和对应的时间戳添加到 RGB 图像队列和时间戳映射中。播放线程从 RGB 图像队列和时间戳映射中读取 RGB 图像和对应的时间戳,然后播放 RGB 图像。互斥锁用于保护 RGB 图像队列和时间戳映射的操作,避免数据竞争。

结语

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

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

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

目录
相关文章
|
1月前
|
设计模式 编解码 C++
【ffmpeg 视频播放】深入探索:ffmpeg视频播放优化策略与设计模式的实践应用(一)
【ffmpeg 视频播放】深入探索:ffmpeg视频播放优化策略与设计模式的实践应用
50 0
|
1月前
|
存储 缓存 编解码
【FFmpeg 视频播放】深入理解多媒体播放:同步策略、缓冲技术与性能优化(一)
【FFmpeg 视频播放】深入理解多媒体播放:同步策略、缓冲技术与性能优化
54 0
|
1月前
|
设计模式 存储 缓存
【ffmpeg 视频播放】深入探索:ffmpeg视频播放优化策略与设计模式的实践应用(二)
【ffmpeg 视频播放】深入探索:ffmpeg视频播放优化策略与设计模式的实践应用
27 0
|
1月前
|
设计模式 编解码 算法
【ffmpeg 视频播放】深入探索:ffmpeg视频播放优化策略与设计模式的实践应用(三)
【ffmpeg 视频播放】深入探索:ffmpeg视频播放优化策略与设计模式的实践应用
29 0
|
3月前
|
Linux 编译器 数据安全/隐私保护
Windows10 使用MSYS2和VS2019编译FFmpeg源代码-测试通过
FFmpeg作为一个流媒体的整体解决方案,在很多项目中都使用了它,如果我们也需要使用FFmpeg进行开发,很多时候我们需要将源码编译成动态库或者静态库,然后将库放入到我们的项目中,这样我们就能在我们的项目中使用FFmpeg提供的接口进行开发。关于FFmpeg的介绍这里就不过多说明。
76 0
|
7月前
|
C++ Windows
FFmpeg入门及编译 3
FFmpeg入门及编译
54 0
|
7月前
|
编解码 API 开发工具
FFmpeg入门及编译 1
FFmpeg入门及编译
97 0
|
24天前
|
开发工具
使用FFmpeg4.3.1的SDK官方开发包编译ffmpeg.c(二)
使用FFmpeg4.3.1的SDK官方开发包编译ffmpeg.c(二)
12 0
|
7月前
|
API C语言 C++
FFmpeg入门及编译 2
FFmpeg入门及编译
80 0
|
3月前
|
编解码 Ubuntu C++
WebAssembly01--web 编译FFmpeg(WebAssembly版)库
WebAssembly01--web 编译FFmpeg(WebAssembly版)库
21 0