基于 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是一…

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

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

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…

dreamStudio试用教程【AI绘画】

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

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

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

Spring Boot实践三 --数据库

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

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

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

IDEA开启并配置services窗口

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

《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…

STM32-风速传感器(ADC)

目录 0 说明 1 传感器介绍 2 代码说明 2.1 ADC.c 2.2 adc.h 2.3 main.c 0 说明 本篇文章主要是说明怎么使用STM32单片机读取风速传感器采集到的数据,读取方式是ADC,并且附带着STM32所需要的全部代码,所使用的风速传感器如下图所示。 附&am…

IDEA的基础使用——【初识IDEA】

IDEA的基础使用——【初识IDEA】 文章目录 IDEA简介前言官网 IDEA的下载与安装选择下载路径勾选自己需要的其余按默认选项进行即可 目录简介安装目录简介 运行Hello WorldIDEA快捷键常用模板模板一:psvm(main)模板二:模板三&#…

PHP-Mysql好运图书管理系统--【白嫖项目】

强撸项目系列总目录在000集 PHP要怎么学–【思维导图知识范围】 文章目录 本系列校训本项目使用技术 首页必要的项目知识ThinkPHP的MVCThinkTemplateThinkPHP 6和ThinkPHP 5 phpStudy 设置导数据库前台展示页面后台的管理界面数据库表结构项目目录如图:代码部分&a…

【c语言初级】c++基础

文章目录 1. C关键字2. 命名空间2.1 命名空间定义2.2 命名空间使用 3. C输入&输出4. 缺省参数4.1 缺省参数概念4.2 缺省参数分类 5. 函数重载5.2 C函数重载的原理--名字修饰采用C语言编译器编译后结果 1. C关键字 C是在C的基础之上,容纳进去了面向对象编程思想…

android studio 找不到符号类 Canvas 或者 错误: 程序包java.awt不存在

android studio开发提示 解决办法是: import android.graphics.Canvas; import android.graphics.Color; 而不是 //import java.awt.Canvas; //import java.awt.Color;

JNPF-一个真正可拓展的低代码全栈框架

一、前言 尽管现在越来越多的人开始对低代码开发感兴趣,但已有低代码方案的一些局限性仍然让大家有所保留。其中最常见的担忧莫过于低代码缺乏灵活性以及容易被厂商锁定。 显然这样的担忧是合理的,因为大家都不希望在实现特定功能的时候才发现低代码平台…

iOS--frame和bounds

坐标系 首先,我们来看一下iOS特有的坐标系,在iOS坐标系中以左上角为坐标原点,往右为X正方向,往下是Y正方向如下图: bounds和frame都是属于CGRect类型的结构体,系统的定义如下,包含一个CGPoint…

ConcurrentHashMap底层具体实现以及实现原理

问题描述 ConcurrentHashMap 底层具体实现以及实现原理 分析维度: 1. ConcurrentHashMap的整体架构 2. ConcurrentHashMap的基本功能 3. ConcurrentHashMap在性能方面的优化 解决方案: ConcurrentHashMap 的整体架构 如图所示,这个是 Concu…