FFmpeg 中的多线程解码

音视频开发进阶

共 2979字,需浏览 6分钟

 ·

2022-03-05 12:08

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实现多线程方案:

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

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

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

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

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

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

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

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

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

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

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

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

Frame级的多线程解码

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

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

当主线程运行到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获取,从而输出。


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

Slice级的多线程解码:

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

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

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

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

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

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

推荐阅读:

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

OpenGL ES 学习资源分享

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

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

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

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

觉得不错,点个在看呗~

浏览 43
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报