基于 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 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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

生产环境Session解决方案、Session服务器之Redis

目录 一、服务器配置 二、安装nginx 三、安装配置Tomcat: 四、配置session Session服务器之Redis Redis与Memcached的区别 安装部署redis 一、服务器配置 IP地址 主机名 软件包列表 192.168.100.131 huyang1 nginx 192.168.100.133 huyang3 JDK Tomca…

[STL]list使用介绍

[STL]list使用 注:本文测试环境是visual studio2019。 文章目录 [STL]list使用1. list介绍2. 构造函数3. 迭代器相关函数begin函数和end函数rbegin函数和rend函数 4. 容量相关函数empty函数size函数 5. 数据修改函数push_back函数和pop_back函数push_front函数和pop…

位图和布隆过滤器+哈希切分思想

文章目录 一.位图(bitset)底层实现: 二.布隆过滤器(bloomFilter)底层实现: 三.哈希切分思想 一.位图(bitset) 位图是一种以一个比特位为数据记录单元的哈希表 ,以无符号整数为key值,采用直接定址法(不存在哈希冲突的问题),其哈希映射函数为 f ( k e y ) k e y ( k e y 的存在…

快速排序qsort讲解

hello大家好,我是c语言boom家宝,今天为大家分享的博客内容是qsort快速排序,简称快排的一个知识点的讲解。 在讲到快排之前,允许博主先提一嘴冒泡排序。大家在c语言的学习过程中,冒泡排序是必不可少会学习到的一个思想&…

Hudi数据湖技术引领大数据新风口(三)解决spark模块依赖冲突

文章目录 解决spark模块依赖冲突2.2.6 执行编译命令2.2.7 编译成功 下一章 核心概念后记 解决spark模块依赖冲突 修改了Hive版本为3.1.2,其携带的jetty是0.9.3,hudi本身用的0.9.4,存在依赖冲突。 1)修改hudi-spark-bundle的pom文…

kafka集群

目录 broker ZooKeeper consumer group(消费者组) 分区(Partitions) 副本(Replicas) 主题(Topic) 偏移量(offset) broker 一个kafka进程就是一个broker…

备战秋招 | 笔试强训17

目录 一、选择题 二、编程题 三、选择题题解 四、编程题题解 一、选择题 1、假设A为抽象类,下列声明()是正确的 A. int fun(A); B. A Obj; C. A fun(int); D. A *p; 2、虚函数可不可以重载为内联? A. 可以 B. 不可以 C. 语法…

uni-app踩坑记

打包h5如何配置域名: 在manifest.json中配置域名 配置完成后无论是测试环境还是正式环境都带上/mobile/,否则会报错404 如何引入调试工具erada: 在默认的index.html中直接引入erada,页面样式会整个错乱,解决方案就是引入官方…

低代码开发平台源码

什么是低代码开发平台? 低代码来源于英文“Low Code,它意指一种快速开发的方式,使用最少的代码、以最快的速度来交付应用程序。通俗的来说,就是所需代码数量低,开发人员门槛低,操作难度低。一般采用简单的图…

学习笔记|大模型优质Prompt开发与应用课(二)|第二节:超高产文本生成机,传媒营销人必备神器

文章目录 01 文字写作技能的革新,各行各业新机遇四大类常见文字工作新闻记者的一天新闻记者的一天–写策划prompt 新闻记者的一天–排采访prompt生成结果prompt生成结果 大模型加持,文字写作我们如何提效营销创作营销创作-使用预置法为不同平台生成文案p…

产品开发八大模块交流︱奇瑞新能源汽车产品开发院院长荣升格

奇瑞新能源汽车股份有限公司研发中心/产品开发院院长荣升格先生受邀为由PMO评论主办的2023第十二届中国PMO大会演讲嘉宾,演讲议题:产品开发八大模块交流。大会将于8月12-13日在北京举办,敬请关注! 议题简要: VUCA时代…

从源码分析Handler面试问题

Handler 老生常谈的问题了,非常建议看一下Handler 的源码。刚入行的时候,大佬们就说 阅读源码 是进步很快的方式。 Handler的基本原理 Handler 的 重要组成部分 Message 消息MessageQueue 消息队列Lopper 负责处理MessageQueue中的消息 消息是如何添加…

用WhatsApp开拓和跟进客户,需要注意这些雷点

我们很多新手小白在利用WhatsApp开拓和维护客户的时候,总是容易犯一些错误,踩到雷点,这不利于客户对企业的印象,不利于增长,下面我们来说一些需要注意的点: 1、专业正确的用语 不管外贸人是跟进哪个国家…

29.Git版本控制工具

1.Git简介 Git是一开源的分布式版本控制系统,提供了存储代码、管理版本历史、分支和合并等功能。 版本控制是指对软件开发过程中各种程序代码、配置文件及说明文档等文件变更的管理,是软件配置管理的核心思想之一。它的主要目的是跟踪和记录软件开发过程…

双重for循环优化

项目中有段代码逻辑是个双重for循环,发现数据量大的时候,直接导致数据接口响应超时,这里记录下不断优化的过程,算是抛砖引玉吧~ Talk is cheap,show me your code! 双重for循环优化 1、数据准备2、原始双重for循环3、…

如何利用Requestly提升前端开发与测试的效率

痛点 B站最牛的Python接口自动化测试进阶教程合集(真实企业项目实战) 前端测试 在进行前端页面开发或者测试的时候,我们会遇到这一类场景: 在开发阶段,前端想通过调用真实的接口返回响应在开发或者生产阶段需要验证前…

Vue 中通用的 css 列表入场动画效果

css 代码 .gradientAnimation {animation-name: gradient;animation-duration: 0.85s;animation-fill-mode: forwards;opacity: 0; }/* 不带前缀的放到最后 */ keyframes gradient {0% {opacity: 0;transform: translate(-100px, 0px);}100% {opacity: 1;transform: translate…

算法38:反转链表

一、需求 给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。 示例 1: 输入:head [1,2,3,4,5] 输出:[5,4,3,2,1] 示例 2: 输入:head [1,2] 输出:[2,1] 示例3&#xff…

MSFCS互相联动

MSF&CS互相联动 1. 前言2. CS联动MSF2.1. 案例测试2.1.1. CS设置联动监听器2.1.2. CS设置联动MSF会话2.1.3. MSF设置监听 3. MSF联动CS3.1. 案例测试3.1.1. MSF生成木马3.1.2. 设置监听3.1.3. CS设置监听3.1.4. MSF转移会话3.1.5. 查看上线 4. 其它 1. 前言 在日常渗透测试…

基于K8s环境·使用ArgoCD部署Jenkins和静态Agent节点

今天是「DevOps云学堂」与你共同进步的第 47天 第⑦期DevOps实战训练营 7月15日已开营 实践环境升级基于K8s和ArgoCD 本文节选自第⑦期DevOps训练营 , 对于训练营的同学实践此文档依赖于基础环境配置文档, 运行K8s集群并配置NFS存储。实际上只要有个K8s集…