基于 FFmpeg 的跨平台视频播放器简明教程(七):使用多线程解码视频和音频

news2024/9/28 5:30:06

系列文章目录

  1. 基于 FFmpeg 的跨平台视频播放器简明教程(一):FFMPEG + Conan 环境集成
  2. 基于 FFmpeg 的跨平台视频播放器简明教程(二):基础知识和解封装(demux)
  3. 基于 FFmpeg 的跨平台视频播放器简明教程(三):视频解码
  4. 基于 FFmpeg 的跨平台视频播放器简明教程(四):像素格式与格式转换
  5. 基于 FFmpeg 的跨平台视频播放器简明教程(五):使用 SDL 播放视频
  6. 基于 FFmpeg 的跨平台视频播放器简明教程(六):使用 SDL 播放音频和视频

文章目录

  • 系列文章目录
  • 前言
  • 线程模型
  • 代码说明
    • 解封装线程
    • 视频解码线程
    • 音频解码线程
    • 定时器线程
  • 小小的优化
  • 参考


前言

在上篇文章中 基于 FFmpeg 的跨平台视频播放器简明教程(六):使用 SDL 播放音频和视频,我们能够同时播放画面和音频。其中 SDL 启动了一个音频线程,每次需要音频数据时都会回调到我们定义的函数。现在,我们需要对视频显示做同样的事情。这么做能让我们的代码更加模块化,更容易使用。

本文参考文章来自 An ffmpeg and SDL Tutorial - Tutorial 04: Spawning Threads。这个系列对新手较为友好,但 2015 后就不再更新了,以至于文章中的 ffmpeg api 已经被弃用了。幸运的是,有人对该教程的代码进行重写,使用了较新的 api,你可以在 rambodrahmani/ffmpeg-video-player 找到这些代码。

本文的代码在 ffmpeg_video_player_tutorial-my_tutorial04_02_threads。

线程模型

回看目前实现的代码,它在主线程做了非常多的事情,包括:

  1. 处理事件循环
  2. 读取 packet,并进行解码
  3. 显示 frame

因此,我们需要做的是让这些工作分开,具体的:

  1. 解封装线程:负责从文件中读取 packet,并把这些 packet 分配到不同的 packet 队列中
  2. 视频解码线程:从 video packet 队列中读取 packet,解码为 frame,然后将解码后的 frame 放入 video frame 队列中
  3. 音频解码线程:从 audio packet 队列中读取 packet,解码为 frame,然后将解码后的 frame 放入 audio frame 队列中
  4. 定时器线程:隔一段时间(例如 30 毫秒)发送一个事件,通知主线程显示视频
  5. SDL 音频线程:由 SDL 创建,通过回调方式获取音频数据进行播放
  6. 主线程:负责各模块的初始化及事件循环。

比较上一章节,虽然线程 1 到 4 使事情看上去似乎更复杂了,但你可以放心,这些线程只是将原来复杂的任务拆分开,整体上并没有比之前的代码更复杂。

在这里插入图片描述

代码说明

让我们看看每个线程都在做些什么,进行代码层面上的解释

解封装线程

std::thread demux_thread([&]() {
  AVPacket *packet{nullptr};
  for (; sdl_app.running;) {
    std::tie(ret, packet) = decoder_ctx.demuxer.readPacket();
    ON_SCOPE_EXIT([&packet] { av_packet_unref(packet); });
    // read end of file, just exit this thread
    if (ret == AVERROR_EOF || packet == nullptr) {
      break;
    }

    if (packet->stream_index == decoder_ctx.video_stream_index) {
      decoder_ctx.video_packet_queue.cloneAndPush(packet);
    } else if (packet->stream_index == decoder_ctx.audio_stream_index
      decoder_ctx.audio_packet_queue.cloneAndPush(packet);
    }
  }
});

它不停地从 demuxer 中读取 packet,并将 packet 放入不同的 packet queue 中

视频解码线程

std::thread video_decode_thread([&]() {
  AVFrame *frame = av_frame_alloc();
  if (frame == nullptr) {
    printf("Could not allocate frame.\n");
    return -1;
  }
  ON_SCOPE_EXIT([&frame] {
    av_frame_unref(frame);
    av_frame_free(&frame);
  });
  for (; sdl_app.running;) {
    if (decoder_ctx.video_packet_queue.size() != 0) {
      ret = decodePacketAndPushToFrameQueue(decoder_ctx.video_packet_queue,
                                            decoder_ctx.video_codec, frame,
                                            decoder_ctx.video_frame_queue);
      RETURN_IF_ERROR_LOG(ret, "decode video packet failed\n");
    }
  }
  return 0;
});

它不停地从 video packet queue 中读取 packet 并进行解码,并将解码后的数据放入 video frame queue 中

音频解码线程

std::thread audio_decode_thread([&]() {
  AVFrame *frame = av_frame_alloc();
  if (frame == nullptr) {
    printf("Could not allocate frame.\n");
    return -1;
  }
  ON_SCOPE_EXIT([&frame] {
    av_frame_unref(frame);
    av_frame_free(&frame);
  });
  for (; sdl_app.running;) {
    if (decoder_ctx.audio_packet_queue.size() != 0) {
      ret = decodePacketAndPushToFrameQueue(decoder_ctx.audio_packet_queue,
                                            decoder_ctx.audio_codec, frame,
                                            decoder_ctx.audio_frame_queue);
      printf("%zd \n", decoder_ctx.audio_frame_queue.size());
      RETURN_IF_ERROR_LOG(ret, "decode audio packet failed\n");
    }
  }
  return 0;
});

它不停地从 audio packet queue 中读取 packet 并进行解码,并将解码后的数据放入 audio frame queue 中

定时器线程

我们使用 SDL_AddTimer 来创建一个定时器,参数解释:

  1. interval:定时器的间隔时间,单位为毫秒。
  2. callback:定时器结束时调用的函数。这个函数的原型必须如下:Uint32 callback(Uint32 interval, void *param);
  3. param:传递给回调函数的参数。
static Uint32 sdlRefreshTimerCallback(Uint32 interval, void *param) {
    (void)(interval);

    SDL_Event event;
    event.type = FF_REFRESH_EVENT;
    event.user.data1 = param;

    SDL_PushEvent(&event);

    return 0;
  }

我们的定时器回调函数 sdlRefreshTimerCallback 它向 SDL 发送一个 FF_REFRESH_EVENT 事件,主线程在接收到 FF_REFRESH_EVENT 事件后,将会从 video frame queue 中 pop 一帧数据,进行图像格式转换操作,并使用 SDL Render 将其渲染到屏幕上。最后会再次启动一个定时器,用来刷新下一帧。

小小的优化

现在各自线程处理各自的事情,解封装线程是数据源头,该线程在一个 for 循环中源源不断地读取 packet,后续的解码线程也在源源不断地解码数据。我们播放一个 30fps 的视频,大约每 33.33ms 播放一帧视频,而解码的速度比 33.33 快多了,也就是说现在的线程模型会会囤积非常多视频数据,等待被播放。这是对内存的一种浪费,我们不需要缓存这么多的视频帧。

解封装线程是所有数据的源头,我们只要控制住源头的速度,就能够控制整个 Pipeline 的速度。因此我们在解封装时对 packet queue 中的数据存量进行检查,如果超过某个阈值,那么就让解封装线程 sleep 一会,控制下 pipeline 的速度。

std::thread demux_thread([&]() {
    AVPacket *packet{nullptr};

    for (; sdl_app.running;) {

      // sleep if packet size in queue is very large
      if (decoder_ctx.video_packet_sync_que.totalPacketSize() >=
              DecoderContext::MAX_VIDEOQ_SIZE ||
          decoder_ctx.audio_packet_sync_que.totalPacketSize() >=
              DecoderContext::MAX_AUDIOQ_SIZE) {
        std::this_thread::sleep_for(10ms);
        continue;
      }

      std::tie(ret, packet) = decoder_ctx.demuxer.readPacket();
      ON_SCOPE_EXIT([&packet] { av_packet_unref(packet); });

      // read end of file, just exit this thread
      if (ret == AVERROR_EOF || packet == nullptr) {
        sdl_app.running = false;
        break;
      }

      if (packet->stream_index == decoder_ctx.video_stream_index) {
        decoder_ctx.video_packet_sync_que.tryPush(packet);
      } else if (packet->stream_index == decoder_ctx.audio_stream_index) {
        decoder_ctx.audio_packet_sync_que.tryPush(packet);
      }
    }
  });

参考

  • An ffmpeg and SDL Tutorial - Tutorial 04: Spawning Threads
  • ffmpeg_video_player_tutorial-my_tutorial04_02_threads

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

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

相关文章

【AI之路】使用huggingface_hub优雅解决huggingface大模型下载问题

文章目录 前言一、Hugging face是什么?二、准备工作三、下载整个仓库或单个大模型文件1. 下载整个仓库2. 下载单个大模型文件 总结附录 前言 Hugging face 资源很不错,可是国内下载速度很慢,动则GB的大模型,下载很容易超时&#…

c++实现计时功能

#include<iostream> #include<string> #include<iomanip>//setw对应的头文件&#xff0c;用于控制输出流的格式、精度、对齐方式等 #include <thread>//实现延迟输出对应的提供了创建、管理和控制线程的功能 using namespace std;int main() {for (int…

使用镜像搭建nacos集群

安装并配置 docker 1 先安装docker //1.查看操作系统的发行版号 uname -r//2.安装依赖软件包 yum install -y yum-utils device-mapper-persistent-data lvm2//3.设置yum镜像源 //官方源&#xff08;慢&#xff09; yum-config-manager --add-repo http://download.docker.co…

sql server导入.back文件

使用SQL server官方的连接工具 SQL server Management studio 有两种方式 第一种&#xff1a; 前提是&#xff0c;提前知道数据库名称&#xff0c;建好数据库 以数据库 TEST为例子 右键数据库选型&#xff0c;选择新建数据库 输入数据库名字&#xff0c;点击确定 创建完成之…

在线讨论相亲app开发过程功能文档

用户注册与登录&#xff1a; 提供用户注册功能&#xff0c;要求用户填写基本信息&#xff08;如姓名、性别、年龄、身高、职业等&#xff09;。 支持使用手机号码或其他第三方账号&#xff08;如微信、QQ&#xff09;进行快速登录。 实现用户隐私保护机制&#xff0c;确保用…

Springboot之把外部依赖包纳入Spring容器管理的两种方式

前言 在Spring boot项目中&#xff0c;凡是标记有Component、Controller、Service、Configuration、Bean等注解的类&#xff0c;Spring boot都会在容器启动的时候&#xff0c;自动创建bean并纳入到Spring容器中进行管理&#xff0c;这样就可以使用Autowired等注解&#xff0c;…

基础篇:多线程所需知识:

前言&#xff1a; 这里的多线程主要指算法部署时所涉及的多线程内容&#xff0c;对于其他多线程知识需要自行补充常用组件有thread、mutex、promise、future、condition_variable启动线程&#xff0c;thread&#xff0c;以及join、joinable、detach、类函数启动为线程生产者消…

自动化测试 selenium(测试系列7)

目录 前言&#xff1a; 1.什么是自动化测试 2.Selenium是什么 3.Selenium原理 4.SeleniumJava环境搭建 5.Selenium常用的API使用 5.1定位元素findElement 5.1.1css选择器 5.1.2id选择器 5.1.3类选择器 5.1.4xpath选择器 5.2操作测试对象 5.2.1click点击对象 5.2.…

[PyTorch][chapter 45][RNN_2]

目录&#xff1a; RNN 问题 RNN 时序链问题 RNN 词组预测的例子 RNN简洁实现 一 RNN 问题 RNN 主要有两个问题&#xff0c;梯度弥散和梯度爆炸 1.1 损失函数 梯度 其中&#xff1a; 则 1.1 梯度爆炸&#xff08;Gradient Exploding&#xff09; 上面矩阵进行连乘后…

【C++】开源:Boost库常用组件配置使用

&#x1f60f;★,:.☆(&#xffe3;▽&#xffe3;)/$:.★ &#x1f60f; 这篇文章主要介绍Boost库常用组件配置使用。 无专精则不能成&#xff0c;无涉猎则不能通。——梁启超 欢迎来到我的博客&#xff0c;一起学习&#xff0c;共同进步。 喜欢的朋友可以关注一下&#xff0c…

Vue触发兄弟级组件中的某个方法

要求&#xff1a;页面上有两个兄弟级组件&#xff0c;假如我点击组件 1 中的按钮&#xff0c;需要触发组件 2 中的某个方法&#xff1b; 在这里&#xff0c;以购物车页面为例&#xff1a;我选择商品列表中的某个商品选中状态&#xff0c;在结算组件中根据列表中是否有未选中状态…

《向量数据库指南》——Milvus Cloud2.2.12 易用性,可视化,自动化大幅提升

Milvus Cloud又迎版本升级,三大新特性全力加持,易用性再上新台阶! 近期,Milvus Cloud上线了 2.2.12 版本,此次更新不仅一次性增加了支持 Restful API、召回原始向量、json_contains 函数这三大特性,还优化了 standalone 模式下的 CPU 使用、查询链路等性能,用一句话总…

三言两语说透process.stdout.write和console.log的区别

Node.js中的process.stdout.write和console.log都是用于向标准输出流(stdout)打印输出的方法&#xff0c;但二者在使用场景和实现方式上有些区别。本文将详细介绍process.stdout.write和console.log的区别。 process.stdout.write介绍 process.stdout.write是Node.js中的一个…

undefined reference to `__android_log_print‘

报错描述 在 Android NDK 相关的工程构建中&#xff0c;出现报错&#xff1a; undefined reference to __android_log_print’ 翻译成 QM 能理解的话&#xff1a; 在链接阶段&#xff0c; 遇到一个需要被链接的符号 __android_log_print, 但是没有在给出的依赖库里面找到 __an…

关于会议OA需求分析与开发功能设计

前言&#xff1a;现如今&#xff0c;企业在会议管理方面对OA系统的需求越来越高。因为会议是企业内部沟通和协作的重要环节&#xff0c;一个高效的会议管理系统可以帮助企业提升会议效率、降低成本&#xff0c;并且提高内部信息共享的效果。 目录 一&#xff0c;以下是OA系统在…

C语言实现通讯录--动态版

一、题目要求 实现一个通讯录&#xff0c;联系人的数量可多可少 二、解题思路 1.在静态版本的基础上改用动态的方法&#xff1a; &#xff08;1&#xff09;默认能够存放三个人的信息 &#xff08;2&#xff09;不够的话&#xff0c;每次增加两个人的信息 2.其他功能不变 三…

学习自动化测试该怎么学?6个步骤轻松拿捏

自动化测试作为脱离手工测试的基本核心内容&#xff0c;其重要性不言而喻了&#xff0c;而且我们来看近期大厂的一些招聘信息显示&#xff0c;基本上自动化测试是必备前提&#xff0c;没有这个基本就不用谈后面的问题了&#xff0c;下面我们通过联想集团的一个软件测试工程师的…

【C#】async和await 续

前言 在文章《async和await》中&#xff0c;我们观察到了一下客观的规律&#xff0c;但是没有讲到本质&#xff0c;而且还遗留了一个问题: 这篇文章中&#xff0c;我们继续看看这个问题如何解决! 我们再看看之前写的代码&#xff1a; static public void TestWait2() {var t…

VR党建主题数字互动虚拟展馆软件开启党建铸魂育人新篇章

当今时代新媒体技术的发展对大学生的学习、生活等产生着深远的影响。高校作为党建育人的重要场所&#xff0c;充分借助VR技术的强大优势&#xff0c;合理运用到育人工作中&#xff0c;能够不断丰富教育内容。VR智慧党建展厅展馆结合VR技术营造的虚拟现实空间&#xff0c;将党的…

layui手机端上传文件时返回404 Not Found的解决方案(client_body_temp权限设置)

关于 1.client_body_temp的作用 client_body_temp是一个指令指定保存客户端请求体临时文件的目录路径&#xff0c;以及是否进行缓存的配置指令。 在Web服务器中&#xff0c;当客户端向服务器发送请求时&#xff0c;请求体中包含了请求的主体部分&#xff0c;比如表单数据、上…