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

系列文章目录

  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 threadif (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_indexdecoder_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 largeif (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 threadif (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.mzph.cn/news/17179.shtml

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

相关文章

leetcode每日一题Day2——344. 反转字符串

✨博主:命运之光 🦄专栏:算法修炼之练气篇(C\C版) 🍓专栏:算法修炼之筑基篇(C\C版) 🐳专栏:算法修炼之练气篇(Python版) …

【面试题】与通义千问的芯片前端设计模拟面试归纳

这里是尼德兰的喵芯片设计相关文章,欢迎您的访问! 如果文章对您有所帮助,期待您的点赞收藏! 让我们一起为芯片前端全栈工程师而努力! 前言 两个小时,与chatGPT进行了一场数字IC前端设计岗的面试_尼德兰的喵的博客-CSDN博客 和GPT-3.5的回答可以对比品尝,味道更好。 模…

Jenkins pipeline 脚本语言学习支持

1 引言 Groovy是用于Java虚拟机的一种敏捷的动态语言,它是一种成熟的面向对象编程语言,既可以用于面向对象编程,又可以用作纯粹的脚本语言。 使用该种语言不必编写过多的代码,同时又具有闭包和动态语言中的其他特性。 Groovy是一…

光学基础知识

本文介绍光学基础知识。 1.可见光光谱 可见光光谱范围:400-700nm 组成可见光的各种颜色光光谱如下表: 名称波长(nm)频率(MHz)紫光400~435790-680蓝光450~480680-620青光480~490600-620绿光500~560600-5…

用Python写了一个下载网站所有内容的软件,可见即可下

目录标题 前言环境介绍:代码实战获取数据获取视频采集弹幕采集评论 GUI部分尾语 前言 嗨喽~大家好呀,这里是魔王呐 ❤ ~! 今天我们分享一个用Python写下载视频弹幕评论的代码。 顺便把这些写成GUI,把这些功能放到一起让朋友用起来更方便~ 环境介绍: py…

【Tensorboard+Pytorch】使用注意事项

安装 tensorboard/tensorboardx版本需要与tensorflow保持一致(本人使用2.2) 调用 环境变量 在终端或CMD中使用时,常见报错“tensorboard 不是内部或外部命令……”,需要添加环境变量路径path。具体为tensorboard.exe所在目录(A…

tinkerCAD案例:29. 摇头娃娃

Research Your Favorite Bobblehead 摇头娃娃 Project Overview: 项目概况: Design and create your favorite Minecraft 3D bobble head. All you need is a computer, 3D printer, spring and your creativity to your favorite Minecraft character in the for…

SolidUI社区-Snakemq 通信源码分析

背景 随着文本生成图像的语言模型兴起,SolidUI想帮人们快速构建可视化工具,可视化内容包括2D,3D,3D场景,从而快速构三维数据演示场景。SolidUI 是一个创新的项目,旨在将自然语言处理(NLP)与计算机图形学相…

PTA 1030 Travel Plan

个人学习记录,代码难免不尽人意。 A traveler’s map gives the distances between cities along the highways, together with the cost of each highway. Now you are supposed to write a program to help a traveler to decide the shortest path between his/h…

dreamStudio试用教程【AI绘画】

文章目录 dreamStudio 简介打开官网如下邮箱登录即可切换随机提示词新用户的试用次数目前只有25张图像📙 预祝各位 前途似锦、可摘星辰 dreamStudio 简介 https://github.com/Stability-AI/StableStudio StabilityAI在官网上重磅宣布——旗下的文生图应用DreamStu…

Kubernetes那点事儿——存储之存储卷

Kubernetes那点事儿——存储之存储卷 前言一、K8s数据卷一、临时存储卷emptyDir二、节点存储卷hostPath三、网络存储NFS 前言 在K8s中用Volume为容器提供了外部的存储能力。 Pod需要设置卷来源(spec.volume)和挂载点(spec.containers.volumeM…

智能提词器有哪些?了解一下这款提词工具

智能提词器有哪些?使用智能提词器可以帮助你更好地准备和交付演讲、报告或其他提词场合。它可以提高你的效率,节省你的时间,并让你更加自信地与听众沟通。另外,智能提词器还可以提供一些有用的功能,如语音识别、智能建…

Spring Boot实践三 --数据库

一,使用JdbcTemplate访问MySQL数据库 1,确认本地已正确安装mysql 按【winr】快捷键打开运行;输入services.msc,点击【确定】;在打开的服务列表中查找mysql服务,如果没有mysql服务,说明本机没有…

迁移学习、微调、计算机视觉理论(第十一次组会ppt)

@TOC 数据增广 迁移学习 微调 目标检测和边界框 区域卷积神经网络R—CNN

IDEA开启并配置services窗口

前言: 一般一个spring cloud项目中大大小小存在几个十几个module编写具体的微服务项目。此时,如果要调试测需要依次启动各个项目比较麻烦。 方法一: 默认第一次打开项目的时候,idea会提示是否增加这个选项卡,如果你没…

SpringBoot第25讲:SpringBoot集成MySQL - MyBatis 注解方式

SpringBoot第25讲:SpringBoot集成MySQL - MyBatis 注解方式 本文是SpringBoot第25讲,上文主要介绍了Spring集成MyBatis访问MySQL,采用的是XML配置方式;我们知道除了XML配置方式,MyBatis还支持注解方式。本文主要介绍Sp…

《golang设计模式》第一部分·创建型模式-03-建造者模式(Builder)

文章目录 1. 概念1.1 角色1.2 类图 2. 代码示例2.1 设计2.2 代码2.3 类图 1. 概念 1.1 角色 Builder(抽象建造者):给出一个抽象接口,以规范产品对象的各个组成成分的建造。ConcreteBuilder(具体建造者)&a…

NOsql之MongoDB入门分享

目录 一、MongoDB简介 1、概念理解 2、yum安装部署 3、二进制安装部署 4、配置文件解析 二、MongoDB基本管理 1、登录操作 2、管理命令 3、用户管理 一、MongoDB简介 1、概念理解 关系型数据库(RDBMS:Relational Database Management System) MySql、Ora…

打造灵活可复用的Web应用:Vue组件化开发指南!

一、组件的简介 1.1、官方概念 ​ 组件(Component)是Vue最强大的功能之一。组件可以扩展HTML元素,封装可重用的代码。在较高层面上,组件是自定义元素,Vue的编译器为它添加特殊功能。在有些情况下,组件也可以表现为用is特性进行了…

Linux 终端操作高效率快捷键!

今天给大家分享一下 Linux 下终端中命令操作常用的快捷键。 作为一名 Linux 下的开发人员,和 Linux 系统打交道是每天必做的事情,通过 Linux 终端下命令行与 Linux 进行交互。 熟练掌握 Linux 终端下命令行的操作可以让我们的工作达到事半功倍的效果&a…