Flutter在OrangePi 5 Plus上视频播放锁死问题

246 阅读16分钟

错误:程序直接卡死,用gdb调试发现是锁死

(gdb) bt #0 futex_wait (private=0, expected=2, futex_word=0xaaaaf5c81b10) at ../sysdeps/nptl/futex-internal.h:146 #1 __GI___lll_lock_wait (futex=futex@entry=0xaaaaf5c81b10, private=private@entry=0) at ./nptl/lowlevellock.c:49 #2 0x0000ffffa7188c74 in lll_mutex_lock_optimized (mutex=0xaaaaf5c81b10) at ./nptl/pthread_mutex_lock.c:48 #3 ___pthread_mutex_lock (mutex=0xaaaaf5c81b10) at ./nptl/pthread_mutex_lock.c:93 #4 0x0000ffff73dffcf4 in ??? () at /usr/lib/aarch64-linux-gnu/dri/rockchip_dri.so #5 0x0000ffff73debea4 in ??? () at /usr/lib/aarch64-linux-gnu/dri/rockchip_dri.so #6 0x0000ffff73d05634 in ??? () at /usr/lib/aarch64-linux-gnu/dri/rockchip_dri.so #7 0x0000ffff7348487c in ??? () at /usr/lib/aarch64-linux-gnu/dri/rockchip_dri.so #8 0x0000ffff7348e254 in ??? () at /usr/lib/aarch64-linux-gnu/dri/rockchip_dri.so #9 0x0000ffff733e0bcc in ??? () at /usr/lib/aarch64-linux-gnu/dri/rockchip_dri.so #10 0x0000ffff847ead14 in ??? () at /lib/aarch64-linux-gnu/libEGL_mesa.so.0 #11 0x0000ffff847e2a3c in ??? () at /lib/aarch64-linux-gnu/libEGL_mesa.so.0 #12 0x0000ffffb4108a18 in ??? () at /lib/aarch64-linux-gnu/libgdk-3.so.0 #13 0x0000ffffb39b6874 in g_object_unref () at /lib/aarch64-linux-gnu/libgobject-2.0.so.0 #14 0x0000ffffb5884290 in video_output_dispose(_GObject*) () at /home/holomotion/local/bin/ntsports/client_versions/lib/libmedia_kit_video_plugin.so

image.png 卡死点非常清晰: media_kit_video_plugin.sovideo_output_dispose() 里做销毁(#14),往下进入g_object_unred->libEGL_mesa.sp->rockchip_dri.so,最终卡在pthread_metex_lock(futex_wait)。 这说明正在销毁EGL/GL相关资源时,驱动里还有渲染中的帧/上下文在用,典型“还在画,先被拆”情形。

GL:提供一套图形渲染API,允许开发者用软件控制GPU绘制三维或二维的图形。开发者可以用GL来创建顶点、纹理、光照、着色器(shander)等。

EGL:负责图形上下文管理和显示系统交互。在OpenGL ES中,你需要一个EGL上下文才能开始渲染。它负责把GL渲染的内容输出到屏幕(framebuffer、窗口、离屏缓冲等)。

  1. 创建渲染上下文(EGLContext)
  2. 创建渲染表面(EGLSurface),例如屏幕窗口、离屏纹理;
  3. 绑定上下文和表面,使GL的命令能渲染到目标;
  4. 同步和交换缓冲区(swap buffers)

根据卡死原因,本人排查了所有实现video播放的逻辑。 发现主要有两个原因: 1、关闭硬件解码,资源暴涨; 2、短时间可能销毁、创建、静默多个video,资源占用。

开启硬件解码

Video(
  controller: VideoController(widget.player,
      configuration: VideoControllerConfiguration(
        enableHardwareAcceleration:
            Config.instance.enableHardwareAcceleration,
        hwdec: 'no',
      )),
  fill: widget.fill,
  fit: widget.fit,
  controls: media_kit_video_controls.NoVideoControls,
);

hwdec:'no'代表强制禁用了硬件解码,这会导致高分辨率视频用软件解码把CPU卡死。 在Linux桌面上,如果视频是1080p/4K,高码率的话,CPU很容易跑满,Flutter UI就会卡死。 直接去掉。

创建全局单例 Player

class GlobalVideoPlayer {
  GlobalVideoPlayer._internal() {
    player = Player();
  }

  late final Player player;

  static final GlobalVideoPlayer instance = GlobalVideoPlayer._internal();
}

class ReusableVideo extends StatefulWidget {
  const ReusableVideo({
    super.key,
    this.fit = BoxFit.contain,
    this.fill = Colors.white,
  });
  final BoxFit fit;
  final Color fill;
  @override
  State<ReusableVideo> createState() => _ReusableVideoState();
}

class _ReusableVideoState extends State<ReusableVideo> {
  late final Player _player;

  @override
  void initState() {
    super.initState();
    _player = GloablVideoPlayer.instance.player;
    _closeVolume();
  }

  Future<void> _closeVolume() async {
    await _player.setVolume(0);
  }

  @override
  Widget build(BuildContext context) {
    return Video(
      controller: VideoController(_player,
          configuration: VideoControllerConfiguration(
              enableHardwareAcceleration:
                  Config.instance.enableHardwareAcceleration)),
      fill: widget.fill,
      fit: widget.fit,
      controls: media_kit_video_controls.NoVideoControls,
    );
  }
}

直接调用就好了。

效果实现了,程序还在测试中...

测试了两天后,没在发生 libmedia_kit_video_plugin.so的卡死 但是最常见的卡死有发生,如下:

(gdb) bt #0 futex_wait (private=0, expected=2, futex_word=0xaaaae522e360) at ../sysdeps/nptl/futex-internal.h:146 #1 __GI___lll_lock_wait (futex=futex@entry=0xaaaae522e360, private=private@entry=0) at ./nptl/lowlevellock.c:49 
#2 0x0000ffff7c7a8c74 in lll_mutex_lock_optimized (mutex=0xaaaae522e360) at ./nptl/pthread_mutex_lock.c:48 
#3 ___pthread_mutex_lock (mutex=0xaaaae522e360) at ./nptl/pthread_mutex_lock.c:93 
#4 0x0000ffff4908fcf4 in ??? () at /usr/lib/aarch64-linux-gnu/dri/rockchip_dri.so 
#5 0x0000ffff4907bea4 in ??? () at /usr/lib/aarch64-linux-gnu/dri/rockchip_dri.so 
#6 0x0000ffff48fb1114 in ??? () at /usr/lib/aarch64-linux-gnu/dri/rockchip_dri.so 
#7 0x0000ffff48fb19d8 in ??? () at /usr/lib/aarch64-linux-gnu/dri/rockchip_dri.so 
#8 0x0000ffff48fb2fe4 in ??? () at /usr/lib/aarch64-linux-gnu/dri/rockchip_dri.so 
#9 0x0000ffff48f952b4 in ??? () at /usr/lib/aarch64-linux-gnu/dri/rockchip_dri.so 
#10 0x0000ffff487e3118 in ??? () at /usr/lib/aarch64-linux-gnu/dri/rockchip_dri.so 
#11 0x0000ffff48670f9c in ??? () at /usr/lib/aarch64-linux-gnu/dri/rockchip_dri.so 
#12 0x0000ffff60b6cd34 in ??? () at /lib/aarch64-linux-gnu/libEGL_mesa.so.0 
#13 0x0000ffff60b62ed4 in ??? () at /lib/aarch64-linux-gnu/libEGL_mesa.so.0 
#14 0x0000ffff75b14efc in ??? () at /lib/aarch64-linux-gnu/libEGL.so.1 
#15 0x0000ffff8972c808 in ??? () at /lib/aarch64-linux-gnu/libgdk-3.so.0 
#16 0x0000ffff896efe94 in gdk_gl_context_make_current () at /lib/aarch64-linux-gnu/libgdk-3.so.0 
#17 0x0000ffff896ffdd0 in ??? () at /lib/aarch64-linux-gnu/libgdk-3.so.0 
#18 0x0000ffff896fffa0 in gdk_window_end_draw_frame () at /lib/aarch64-linux-gnu/libgdk-3.so.0 
#19 0x0000ffff89b5216c in ??? () at /lib/aarch64-linux-gnu/libgtk-3.so.0 
#20 0x0000ffff899e070c in gtk_main_do_event () at /lib/aarch64-linux-gnu/libgtk-3.so.0 
#21 0x0000ffff896e89b0 in ??? () at /lib/aarch64-linux-gnu/libgdk-3.so.0 
#22 0x0000ffff896fc590 in ??? () at /lib/aarch64-linux-gnu/libgdk-3.so.0 
#23 0x0000ffff897011fc in ??? () at /lib/aarch64-linux-gnu/libgdk-3.so.0 
#24 0x0000ffff89701400 in ??? () at /lib/aarch64-linux-gnu/libgdk-3.so.0
#25 0x0000ffff88fe8010 in ??? () at /lib/aarch64-linux-gnu/libgobject-2.0.so.0 
#26 0x0000ffff88fe8178 in g_signal_emit_valist () at /lib/aarch64-linux-gnu/libgobject-2.0.so.0 
#27 0x0000ffff88fe8234 in g_signal_emit () at /lib/aarch64-linux-gnu/libgobject-2.0.so.0 
#28 0x0000ffff896f6ff8 in ??? () at /lib/aarch64-linux-gnu/libgdk-3.so.0 
#29 0x0000ffff896e140c in ??? () at /lib/aarch64-linux-gnu/libgdk-3.so.0 
#30 0x0000ffff88e928c8 in ??? () at /lib/aarch64-linux-gnu/libglib-2.0.so.0 
#31 0x0000ffff88e91790 in ??? () at /lib/aarch64-linux-gnu/libglib-2.0.so.0 
#32 0x0000ffff88ef37a8 in ??? () at /lib/aarch64-linux-gnu/libglib-2.0.so.0 
#33 0x0000ffff88e90b68 in g_main_context_iteration () at /lib/aarch64-linux-gnu/libglib-2.0.so.0 
#34 0x0000ffff891388c0 in g_application_run () at /lib/aarch64-linux-gnu/libgio-2.0.so.0 
#35 0x0000aaaae48e2204 in main () 
(gdb)
warning: 146	../sysdeps/nptl/futex-internal.h: 没有那个文件或目录
(gdb) bt
#0  futex_wait (private=0, expected=2, futex_word=0xaaaae0c46ae0) at ../sysdeps/nptl/futex-internal.h:146
#1  __GI___lll_lock_wait (futex=futex@entry=0xaaaae0c46ae0, private=private@entry=0) at ./nptl/lowlevellock.c:49
#2  0x0000ffffae278c74 in lll_mutex_lock_optimized (mutex=0xaaaae0c46ae0) at ./nptl/pthread_mutex_lock.c:48
#3  ___pthread_mutex_lock (mutex=0xaaaae0c46ae0) at ./nptl/pthread_mutex_lock.c:93
#4  0x0000ffff7a3afcf4 in ??? () at /usr/lib/aarch64-linux-gnu/dri/rockchip_dri.so
#5  0x0000ffff7a39bea4 in ??? () at /usr/lib/aarch64-linux-gnu/dri/rockchip_dri.so
#6  0x0000ffff7a2d1114 in ??? () at /usr/lib/aarch64-linux-gnu/dri/rockchip_dri.so
#7  0x0000ffff7a2d19d8 in ??? () at /usr/lib/aarch64-linux-gnu/dri/rockchip_dri.so
#8  0x0000ffff7a2d2fe4 in ??? () at /usr/lib/aarch64-linux-gnu/dri/rockchip_dri.so
#9  0x0000ffff7a2b52b4 in ??? () at /usr/lib/aarch64-linux-gnu/dri/rockchip_dri.so
#10 0x0000ffff79a4f97c in ??? () at /usr/lib/aarch64-linux-gnu/dri/rockchip_dri.so
#11 0x0000ffff79986664 in ??? () at /usr/lib/aarch64-linux-gnu/dri/rockchip_dri.so
#12 0x0000ffff98b8c434 in ??? () at /lib/aarch64-linux-gnu/libEGL_mesa.so.0
#13 0x0000ffff98b7d0ec in ??? () at /lib/aarch64-linux-gnu/libEGL_mesa.so.0
#14 0x0000ffff98b6d2fc in ??? () at /lib/aarch64-linux-gnu/libEGL_mesa.so.0
#15 0x0000ffffbb1f8968 in ??? () at /lib/aarch64-linux-gnu/libgdk-3.so.0
#16 0x0000ffffbb1cfe70 in ??? () at /lib/aarch64-linux-gnu/libgdk-3.so.0
#17 0x0000ffffbb1cffa0 in gdk_window_end_draw_frame () at /lib/aarch64-linux-gnu/libgdk-3.so.0
#18 0x0000ffffbb62216c in ??? () at /lib/aarch64-linux-gnu/libgtk-3.so.0
#19 0x0000ffffbb4b070c in gtk_main_do_event () at /lib/aarch64-linux-gnu/libgtk-3.so.0
#20 0x0000ffffbb1b89b0 in ??? () at /lib/aarch64-linux-gnu/libgdk-3.so.0
#21 0x0000ffffbb1cc590 in ??? () at /lib/aarch64-linux-gnu/libgdk-3.so.0
#22 0x0000ffffbb1d11fc in ??? () at /lib/aarch64-linux-gnu/libgdk-3.so.0
#23 0x0000ffffbb1d1400 in ??? () at /lib/aarch64-linux-gnu/libgdk-3.so.0
#24 0x0000ffffbaab8010 in ??? () at /lib/aarch64-linux-gnu/libgobject-2.0.so.0
#25 0x0000ffffbaab8178 in g_signal_emit_valist () at /lib/aarch64-linux-gnu/libgobject-2.0.so.0
#26 0x0000ffffbaab8234 in g_signal_emit () at /lib/aarch64-linux-gnu/libgobject-2.0.so.0
#27 0x0000ffffbb1c6ff8 in ??? () at /lib/aarch64-linux-gnu/libgdk-3.so.0
#28 0x0000ffffbb1b140c in ??? () at /lib/aarch64-linux-gnu/libgdk-3.so.0
#29 0x0000ffffba9628c8 in ??? () at /lib/aarch64-linux-gnu/libglib-2.0.so.0
#30 0x0000ffffba961790 in ??? () at /lib/aarch64-linux-gnu/libglib-2.0.so.0
#31 0x0000ffffba9c37a8 in ??? () at /lib/aarch64-linux-gnu/libglib-2.0.so.0
#32 0x0000ffffba960b68 in g_main_context_iteration () at /lib/aarch64-linux-gnu/libglib-2.0.so.0
#33 0x0000ffffbac088c0 in g_application_run () at /lib/aarch64-linux-gnu/libgio-2.0.so.0
#34 0x0000aaaac86d2204 in main ()
(gdb) 

这个卡死已经不一样了,不是在销毁media_kit_video的对象时卡主,而是卡在GTK/GDK正在做gdk_gl_context_make_current(把某个GL上下文设为当前)的过程中,底层进入EGL_mesa -> rockchip_dri.so,最终在pthread_mutex_lock上等锁:

  • #16 gdk_gl_context_make_current
  • #12–#4 libEGL_mesa.so / rockchip_dri.so
  • #0–#3 futex 等锁

这通常意味着:同时存在两个(或更多)OpenGL/EGL 上下文在 GTK/Flutter 的同一进程里争用驱动内部的一把锁。典型触发条件:

  • 同屏存在多个 GL 源:例如 Video(media_kit 的 GLArea)+ 摄像头 Texture(你自己的 GL 纹理)+ Flutter 自己的 GL 合成
  • 页面切换/尺寸变化时,旧的 GL widget 正在结束一帧,新的 GL widget 又开始 make_current
  • 还有一种是 在同一帧里仍有帧回调在推纹理(markTextureFrameAvailable / 渲染线程),GTK/GDK 又在做 end_draw_frame / make_current(#18→#16),两者撞锁。

撞锁

问题1:在同一帧里仍有帧回调在推纹理(markTextureFrameAvailable / 渲染线程),GTK/GDK 又在做 end_draw_frame / make_current(#18→#16),两者撞锁的时间线?

问题2:什么时候触发end_draw_frame 什么时候触发 make_current?

1. GTK/GDK 的渲染循环

GTK3/GTK4 基于 GDK,有一个 frame clock,每一帧都会经历:

  1. prepare → 确定需要重绘的区域
  2. paint → widget 渲染内容
  3. end_draw_frame → 最后收尾,把这帧交给后端(EGL/GL)

在 GL backend 下,end_draw_frame() 会:

  • 调用 gdk_gl_context_make_current(context) (#16) 把 EGL 上下文设为当前;
  • 把这帧的 GL 指令提交到 GPU;
  • 最后 eglSwapBuffers 或类似操作。

所以可以理解为:每一帧 GTK 会在渲染结束阶段调用 make_current → swap


2. Flutter 插件推纹理的时间线

Flutter 的 Texture 机制是这样的:

  • 你通过 TextureRegistry.createSurfaceTexture() 得到一个 textureId
  • 插件端有个 native 渲染器(比如摄像头/视频),会在某个线程里绘制到 EGLSurface/FBO 上;
  • 每次有新的一帧渲染完毕,插件要调用 MarkTextureFrameAvailable(textureId)
  • Flutter engine 收到这个回调 → 把对应的 GL 纹理更新到合成树;
  • 下一次 Flutter rasterizer 在 vsync 驱动的 drawFrame 中,会把这个纹理采样绘制进 Scene。

换句话说:插件在推帧的那一刻,必然需要“make_current 到它自己的 EGLContext / Surface” → 做 GL 操作 → release


3. 两个时间线的冲突点

于是有了这样一种情况:

  • GTK 主循环 正在做一帧 → end_draw_frame() 里要 make_current GTK 自己的 EGLContext;
  • 插件渲染线程 在差不多同一时刻也跑起来 → 要 make_current 插件的 EGLContext 去绘制摄像头帧;
  • Rockchip/Mesa 驱动内部对这些 EGLContext 切换用了一个全局 mutex;
  • 两边同时进来 → 死锁或长时间卡在 futex。

所以我说的 “在同一帧里,GTK/GDK 正在收尾,而插件还在推纹理” ,指的就是这两条时间线重叠。


4. 什么时候触发 end_draw_frame / make_current

  • end_draw_frame
    每次 GTK 窗口重绘的收尾阶段都会触发,一般跟系统 vsync 节奏(60Hz 或 30Hz)保持一致。触发原因可能是:

    • widget 更新(你 setState 导致重绘)
    • 窗口暴露/尺寸变化
    • 定时刷新的动画
  • 插件的 make_current
    每次插件要绘制一帧新纹理时触发(比如摄像头 30fps / 视频解码 25fps)。这条时间线独立于 GTK,不保证和 GTK 的 vsync 对齐。

所以两个节奏是异步的,一旦同时命中某一瞬间,就会在 EGL mutex 上撞到。


5. 怎么避免撞锁

  • 错开时间线
    在页面切换/移除 widget 的时候,先停掉帧回调(不再调用 markTextureFrameAvailable),再等 GTK 完成那一帧的 end_draw_frame → 这就是我上次建议加 addPostFrameCallback + 延迟 30–50ms 的原因。
  • 单源化
    同屏只留一个 GL source,减少两条独立渲染线程同时跑的机会。
  • 节流
    插件端推帧不要跟 GTK 主循环一样跑满 60fps,降到 25–30fps 就已经能显著减少撞锁概率。

👉 总结成一句话:
GTK 的 end_draw_frame → make_current 是系统帧循环的收尾;插件的 make_current 是你主动推纹理的时机。这两者如果同时抢驱动的 EGL mutex,就会卡死。解决办法就是错开它们(先停推帧,再等 GTK 收尾),或者避免同时存在多个 GL 源。

GL source是什么?

1. 什么是 GL source?

这里我说的 GL source 其实指的是 一个独立的 OpenGL/EGL 渲染来源,也就是 一条能往 GPU 提交绘制指令的“源头”

比如:

  • GTK/GDK:窗口系统本身会有一个 EGLContext,它负责绘制 GTK widget、合成 Flutter embedder 输出 → 这是一个 GL source。
  • Flutter engine:Flutter 的 rasterizer(Skia + OpenGL 后端)本身也会占用一个 EGLContext → 这是另一个 GL source。
  • 插件(video/camera/media_kit) :如果插件内部用 EGL/FBO 渲染视频帧,那它自己也会创建一个 EGLContext / Surface 来绘制 → 这是第三个 GL source。

2. 为什么多个 GL source 会有冲突?

  • EGL 驱动(比如 Rockchip/Mesa)内部,同一个 GPU 设备上的所有 EGLContext 切换都是串行的
  • 每个 makeCurrent() / eglSwapBuffers() 背后都会上一个全局锁(就是你看到的 futex)。
  • 当有多个 GL source(GTK、Flutter、插件)在不同线程里频繁切换时,很容易撞锁 → 卡死。

换句话说:GL source 就像多个厨师同时抢一个灶台,每次换人都要停顿、锁上 → 如果两个同时要炒菜,就卡在门口了。


3. 怎么理解到你的场景?

在 Flutter Linux + GTK + media_kit 里,通常至少有 三个 GL source

  1. GTK/GDK → 负责整个窗口渲染循环(#18 → #16 调用 make_current)。
  2. Flutter engine → rasterizer 绘制 Flutter widget 树。
  3. media_kit/video 插件 → 用 FFmpeg + OpenGL,把视频帧画到 FBO,再推给 Flutter Texture

所以一旦 GTK 在收尾 (end_draw_frame) + 插件刚好推新纹理 (markTextureFrameAvailable) → 两个 GL source 都要切换 EGLContext,就会撞到全局 mutex。


4. 如何减少 GL source 冲突?

  • 尽量减少 GL source 数量
    → 插件能复用 Flutter 的 EGLContext 最好,不要自己新建。
  • 串行化渲染
    → 在 Flutter vsync 循环里调度插件绘制,避免独立线程乱抢。
  • 降频 / 节流
    → 插件推帧不需要 60fps,25–30fps 就够,降低锁竞争。
  • 退出页面时及时停掉插件推帧
    → 避免 “页面 dispose 了,但视频线程还在画”。

✅ 总结:
GL source = 一个独立的 EGL/OpenGL 渲染上下文(渲染来源)。
GTK/GDK、Flutter、插件各自都是 GL source。多个 GL source 在不同线程同时抢 GPU context 时,就会卡在驱动里的 futex 锁。

🧩 GDB 死锁调试笔记(图形驱动相关)


一、常用调试命令

1️⃣ 查看所有线程的调用栈

(gdb) bt                # 当前线程的调用栈
(gdb) info threads      # 查看所有线程
(gdb) thread apply all bt   # 打印所有线程的调用栈

2️⃣ 查看特定锁状态

当怀疑死锁与某个 pthread_mutex_t 有关时,可直接查看锁结构体内容:

(gdb) print *(pthread_mutex_t*)0xaaaaf68f4000
(gdb) print *(pthread_mutex_t*)0xaaaaf68ea938

💡 说明:

  • 0xaaaaf68f40000xaaaaf68ea938 为锁的内存地址。
  • 输出内容可判断锁的 __data.__owner__count__lock 状态,从而判断是否被占用或死锁。

3️⃣ 查看具体线程状态

查看某个线程(如线程号 49)的详细信息与调用堆栈:

(gdb) thread 49
(gdb) bt

二、调试分析结论

  1. 死锁并非源于业务逻辑锁(文件操作锁)。

    • 调查的两个锁地址均指向图形驱动(非应用代码)。
  2. 问题出在图形驱动内部。

    • 死锁发生在 Mesa 驱动GTK/Flutter 图形栈 的锁顺序不一致上。
    • 属于 驱动内部锁竞争/顺序反转问题
  3. 硬件加速是根本原因。

    • 硬件平台:Rockchip
    • 问题来源:Rockchip 平台的 Mesa 实现存在线程锁定 bug,导致 GPU 渲染线程与 GUI 主线程相互等待。

三、进一步建议

  • ⚙️ 临时解决方案:

    • 禁用 GPU 硬件加速,改为软件渲染(CPU)。
    • 可通过环境变量:
      export LIBGL_ALWAYS_SOFTWARE=1
      
      测试发现,禁止后Flutter不在渲染页面。
    • 或 Flutter 启动参数关闭 GPU 加速。
  • 🧠 长期解决方向:

    • 使用更新的 Mesa 驱动版本。
    • 或切换到稳定的图形栈(如 Wayland + EGL 软件栈)。
    • 若为嵌入式 Flutter,可考虑 --enable-software-rendering 参数运行调试。

✅ 小结

问题点结论
死锁线程Mesa + Flutter 图形栈
锁地址图形驱动内部 Mutex
根因硬件加速引起的驱动层锁争用
临时方案禁用 GPU 硬件加速
永久方案升级 Mesa 驱动或切换软件渲染方式