当前位置: 首页 > news >正文

FFmpeg 中的多线程解码

ffmpeg 中使用到的多线程的概念:

共享变量的互斥

互斥锁(mutex-lock)是一种信号量,用来防止两个线程在同一时刻访问相同的共享资源,它有锁定状态和非锁定状态。

在任意时刻,一个线程要想存取共享数据,线程必须首先获得mutex-lock,当此线程释放此共享数据的时候必须对mutex-lock解锁,在一个任意的时间内,只有一个线程能锁定互斥锁,通过函数pthread_mutex_lock上锁,通过函数pthread_mutex_unlock解锁。

同步条件变量

条件变量用来提供另一种线程同步的方法,其基于实际的变量值来实现线程的同步操作,设置了条件变量的情况下,线程就不需要通过不停的轮询来查询条件是否满足,也不需要不停的忙等,从而能够节省很多系统资源。

一个条件变量总是和一个mutex-lock对应,系统通过pthread_cond_await函数来阻塞调用的线程,一直到条件变量得到满足。

当这个线程阻塞的时候对应的mutex-lock会自动解锁,但当该线程运行的时候,其对应的mutex-lock会被加锁。

使用函数pthread_cond_signal来唤醒等待在条件变量的另一个线程,当用来唤醒多个处于阻塞状态线程时通过pthread_cond_broadcast函数来完成。

ffmpeg实现多线程方案:

1a882b5057986e10390ecf0893e54f47.png

Thread List,线程列表,线程列表中的每一项都映射一个解码线程。

主线程会从线程列表中按照序号由小到大(循环)提取解码线程,并把解码任务提交到该解码线程。

同时主线程在提交完解码任务后也会从线程列表中按照序号由小到大(循环)提取解码线程,并尝试从该解码线程获取解码完成的帧。

M,主线程,主要目的有两个:

  • 向解码线程提交解码任务。FFmpeg中是以packet为单位进行解码任务的提交的,按照前一小节的描述,FFmpeg就是以frame为单位进行解码任务的提交的。

  • 从解码线程获取解码所得的帧并进行返回。

    不过在第一轮进行任务提交的时候是不会去获取帧,在第一轮任务提交完成后,此时所有解码线程都已经开始进行了解码作业,那么主线程就可以开始等待第一个线程解码完成,然后尝试去获得解码完成的帧。

    这里的“尝试”,是因为就像单线程解码时那样,并不一定是每次调用解码API都会返回一帧的。由于h264编码的视频中常常包含B帧,这会使得码流的解码顺序并非帧的播放顺序,但是解码API必须按照帧的播放顺序进行返回,因此在进行帧的返回时会进行相应的调整。

    接下来每次向一个线程提交一个解码任务后,都需要等待下一个线程空闲并尝试返回帧。

    c1519b6447a5fa6eb28e64d60b257e13.png

  • T,解码线程,接收解码任务并进行解码。解码线程是以frame为单位进行处理的。解码线程解码主线程所提交的packet,执行与单线程时一样的解码作业。

fc781252974dfb57711832bdef9533ec.png

ffmpeg中的多线程解码主要分为片 Slice级别的多线程解码 和 帧Frame级别的多线程解码:

Slice级的解码效率比Frame级的解码效率要低,故,本文先考虑Frame级的多线程解码。

Frame级的多线程解码

通过查看ffmpeg源码,了解到Frame级的多线程解码流程大致如下:

3af0a0f5f4dad2aff1e4c681146f556d.png

其中右侧的frame_worker_thread为解码线程,在open解码器时就已经创建,随后阻塞在pthread_cond_wait(&p->input_cond, &p->mutex)函数。等待被主线程唤醒。

c9b800050f7c7f6c78b437d3f6a02670.png

当主线程运行到ff_thread_decode_frame函数时,会调用submit_packet函数,这个函数的目的就是将packet包交给解码线程。

submit_packet函数会调用pthread_cond_signal(&p->input_cond)函数,这个函数就是为唤醒刚才阻塞的解码线程。

当主线程唤醒解码线程后,其pthread_cond_wait(&p->output_cond, &p->progress_mutex)函数会进入阻塞状态,等待解码线程唤醒。

  1. 如果Codec未实现update_thread_context()和线程安全的get_buffer(),则必须在解码完成后才能将状态转换为STATUS_SETUP_FINISHED,意味着下一个线程只能在当前线程解码完成后才能开始解码。当解码线程解码完成后,会用pthread_cond_signal(&p->output_cond)将主线程唤醒,其中会通过回调函数将解码线程解码出来的frame获取,从而输出。

706bfa242d446e0958c1e2905adba7b2.png

2. 如果Codec实现update_thread_context()和线程安全的get_buffer(),线程状态可以在解码开始之前转换为STATUS_SETUP_FINISHED,这样,主线程就能够被唤醒,就能够去读包进行下一个包的解码,因此下一个线程就可能与当前线程并行。

a377ee7b1716218b005fe9897af49cab.png

Slice级的多线程解码:

ffmpeg的slice级并行只能在帧内并行。

因此,如果在某个视频在编码时,一帧图像分为多个slice进行编码的话,那么在使用ffmpeg解码时调用slice级并行解码就会得到不错的效果。

而在实际应用中,大多数h264编码的视频都是一帧只有一个slice,对于这种视频,就算采用了slice级并行,也只有一个线程在进行解码作业。

如果一帧,即一个packet分为几个slice时,会先把这一帧前面的slice加入队列,到最后一个slice时统一对这一帧的所有slice进行并行解码。其中涉及到的关键要素如下:

e9e722c37d09069f6560674829814413.png

Slice Context List,slice的上下文是slice context(FFmpeg中的变量为slice_ctx),如果一帧中有多个slice,那么会把slice上下文组成一个列表。前面所说的入队列操作会对该列表进行填充以供后续解码使用。

M,主线程,如单线程一样的流程,从用户调用解码API一直执行到我们前面所说的入队列,到最后一个slice时会调用一个入口函数启动多线程解码操作。在调用入口函数后,主线程参与的多线程解码过程一共包含三个步骤:

  1. 通过发送启动消息激活其它正在等待的解码线程。

  2. 在启动多线程解码后,主线程也会一同作为其中一个线程进行slice的解码。

  3. 最后等待所有线程完成任务后返回。

T,解码线程,接收到主线程所发起的启动消息后,解码线程会到Slice Context List去提取其中一个slice context(原子操作),然后进行slice解码。

作者:frgfnjrgn
来源:https://blog.csdn.net/yihuanyihuan/article/details/104019536

35310ecb582f3ba3bebf0106f7bafcc4.png

de82ed775abe02cf4298d72d3f1814f2.png

一个音视频领域专业问答的小圈子!

推荐阅读:

音视频开发工作经验分享 || 视频版

OpenGL ES 学习资源分享

开通专辑 | 细数那些年写过的技术文章专辑

Android NDK 免费视频在线学习!!!

你想要的音视频开发资料库来了

推荐几个堪称教科书级别的 Android 音视频入门项目

觉得不错,点个在看呗~

5253bb4e5f07d13a12cb448db6cd088b.gif

相关文章:

  • 视频图像色彩增强的主要方法与落地实践
  • 根据采样频率计算音频时长
  • 什么是闭合GOP和开放GOP?
  • 星球专享 | 关于播放器的一次项目实践~~
  • 可远程办公的神仙公司 招音视频啦!
  • 星球专享 | 播放器 FFmpeg 依赖库的配置
  • 干货分享丨HDR 技术产品实践与探索
  • H264 视频文件如何缩放分辨率?
  • 也许可以少走弯路的职场建议
  • FFmpeg 中 AVPacket 与 AVFrame 中数据的传递与释放
  • 头条都在用的边下边播方案
  • YUV 与 颜色空间转换
  • 原创干货 | 入门或者转行音视频,应该要怎么做?
  • 上手 GAMES 104 课程 Pilot 游戏引擎~~
  • 关于音视频里面的 解码帧率 和 渲染帧率
  • 【译】JS基础算法脚本:字符串结尾
  • [NodeJS] 关于Buffer
  • [译]CSS 居中(Center)方法大合集
  • “寒冬”下的金三银四跳槽季来了,帮你客观分析一下局面
  • Codepen 每日精选(2018-3-25)
  • CODING 缺陷管理功能正式开始公测
  • Cumulo 的 ClojureScript 模块已经成型
  • gops —— Go 程序诊断分析工具
  • Java 11 发布计划来了,已确定 3个 新特性!!
  • JavaScript-Array类型
  • SQLServer之创建显式事务
  • Vue.js-Day01
  • Webpack4 学习笔记 - 01:webpack的安装和简单配置
  • 案例分享〡三拾众筹持续交付开发流程支撑创新业务
  • 聚类分析——Kmeans
  • 七牛云假注销小指南
  • 前端面试之闭包
  • 算法系列——算法入门之递归分而治之思想的实现
  • 王永庆:技术创新改变教育未来
  • 一些关于Rust在2019年的思考
  • 【运维趟坑回忆录】vpc迁移 - 吃螃蟹之路
  • 400多位云计算专家和开发者,加入了同一个组织 ...
  • Android开发者必备:推荐一款助力开发的开源APP
  • HanLP分词命名实体提取详解
  • #android不同版本废弃api,新api。
  • $L^p$ 调和函数恒为零
  • (04)Hive的相关概念——order by 、sort by、distribute by 、cluster by
  • (16)Reactor的测试——响应式Spring的道法术器
  • (第二周)效能测试
  • (附源码)spring boot球鞋文化交流论坛 毕业设计 141436
  • (附源码)ssm码农论坛 毕业设计 231126
  • (亲测成功)在centos7.5上安装kvm,通过VNC远程连接并创建多台ubuntu虚拟机(ubuntu server版本)...
  • (转)Android中使用ormlite实现持久化(一)--HelloOrmLite
  • .bat批处理(八):各种形式的变量%0、%i、%%i、var、%var%、!var!的含义和区别
  • .NET/C# 利用 Walterlv.WeakEvents 高性能地定义和使用弱事件
  • .net反编译工具
  • .Net转Java自学之路—基础巩固篇十三(集合)
  • ::什么意思
  • @Bean, @Component, @Configuration简析
  • @JSONField或@JsonProperty注解使用