用途
该技能面向在 Compose Desktop 中播放原生媒体引擎视频帧的场景,主张不把帧画面贴到 AWT/Swing 组件再嵌入 Compose,而是把已完成的帧作为不可变图像发布到 StateFlow,用普通 Image 绘制。引擎侧的渲染方式不变,而嵌入式面板导致的一整类缺陷随之消失。
为什么不使用嵌入式面板
文中列出三种结构性(而非可修复)的失败:
- 重量级嵌入式组件是叠加层,无论 z 序如何都绘制在所有 Compose 节点之上,因此无法在其上放置任何内容。
- 页面滚动时它晚一帧重新定位,表现为闪烁并露出窗口后面的内容,透明窗口下更明显。
- 工具包给一个组件只允许一个父容器:两个界面同时组合该播放器会争抢同一实例,失败的一方渲染黑屏,直到切换上一首/下一首,即“视频随机消失”的报告。
而 Image 参与正常的 Compose 渲染,任意多个界面可以同时收集同一帧源。
陷阱与做法
- 像素格式:选择与图像类型匹配的引擎像素格式。文中示例引擎按递增地址上的字节序命名格式(bgr0 类名称表示偏移 0 为 B、1 为 G、2 为 R、3 未使用),但强调这只是该引擎的约定而非通用规范;其他渲染 API 常按最高有效字节在前命名,使用前应查引擎文档。JVM 的 packed-integer RGB 图像类型把像素存为 0x00RRGGBB,在小端主机(桌面 Java 发布的所有平台)上内存布局即为 B、G、R、未使用,与 bgr0 类布局相同,因此整块读取无需逐像素修正;选错约定正是红蓝互换的原因。
- 不使用 alpha 的 packed-integer 类型:引擎将第四分量记为未初始化,常为 0 但不保证。在 ARGB 图像中该字节是 alpha 通道,垃圾零值会让每帧完全透明;非 alpha 类型会忽略高字节。
- 遵循引擎要求的行跨度对齐:把行跨度补齐到请求的对齐值(常见 64 字节)以保持引擎快速路径,并让暂存缓冲区也按跨度宽度分配,使整帧为一段连续内存、一次批量拷贝读出;只发布最左侧 width 列,因为一行末尾到下一行开头之间的填充内容明确未定义。
- 每帧发布新图像:StateFlow 会合并相等值,重新发出被修改的图像会被丢弃导致画面冻结。每帧新建快照同时保证不可变,任何收集者可在任意线程转换或绘制而不撕裂,替代原先 UI 线程上的逐帧 blit,总拷贝量不变。示例代码用 BufferedImage(TYPE_INT_RGB)逐行 System.arraycopy 后写入
_frames.value。 - 在 UI 线程外转换:用
withContext(Dispatchers.Default)收集帧并调用toComposeImageBitmap(),同时通过onSizeChanged把容器尺寸回报给帧源。 - 适配由引擎决定:引擎将画面缩放并加黑边到所报尺寸,因此 Compose 看到帧时黑边已是像素,任何 ContentScale 都无法去除。裁剪使用引擎自身的 pan-scan 属性,在以句柄和标志为键的 LaunchedEffect 中调用
setPanscan(if (cropToBounds) 1.0 else 0.0),重新缩放运行中的视频而非销毁重建句柄。ContentScale 仅在尺寸调整后到下一张正确尺寸帧渲染前短暂起作用。 - 回调线程处理:引擎更新回调在外部线程触发且可能只允许发信号;渲染 API 通常禁止在其内部调用任何函数。因此发信号量,在自有线程上只调用渲染函数:使用有界等待以便及时响应停止标志,然后排空许可(积压应合并为最新帧而非逐帧补跑),并且除非引擎报告新帧或尺寸变化使缓冲区失效,否则跳过渲染(渲染是处理器密集型)。
- 无条件切换帧源并在分离时清空:对“为新句柄设置帧源”加空值保护会使即将释放的句柄的旧画面留在屏幕上(黑视频直到切换上一首/下一首的第二种表现);分离时置空可让界面回退到封面图而非保留陈旧帧。
- 在渲染线程上分配与重分配:尺寸变化来自 Compose 布局,将分配保持在渲染线程可使其远离 UI;整体替换目标对象而非原地修改宽高,避免渲染循环与销毁过程在拷贝中途对尺寸产生分歧。
- 保持原生内存存活:渲染参数块通常存有指向小分配(尺寸、跨度、格式字符串)的裸指针,引擎每次渲染都会解引用;看似未使用的字段实际承重,丢失引用会让绑定在原生代码使用期间释放它们。
验证方式
在仓库根目录(下述 core/media/... 路径可解析处)运行:
grep -n "val frames: StateFlow<BufferedImage?>\|bgr0\|TYPE_INT_RGB" core/media/media-jvm/src/main/java/com/simpmusic/media_jvm/mpv/MpvVideoFrameSource.ktgrep -rn "SwingPanel(" core/media/media-jvm-ui core/media/media-jvm
通过条件:第一条打印全部三项,第二条无输出,即视频路径中已无 SwingPanel、帧发布在 StateFlow、格式与非 alpha 图像类型配对。
grep -n "fun renderLoop\|fun ensureSurface\|BufferedImage(target.width" core/media/media-jvm/src/main/java/com/simpmusic/media_jvm/mpv/MpvVideoFrameSource.kt
通过条件:分配语句位于 renderLoop 与下一个函数之间,绝不在仅处理尺寸变化的 ensureSurface 内。
grep -n "LaunchedEffect(mpvPlayer, cropToBounds)\|setPanscan\|ContentScale.Fit" core/media/media-jvm-ui/src/main/java/com/maxrave/media_jvm_ui/ui/MediaPlayerView.kt
通过条件:setPanscan 紧跟在 LaunchedEffect 匹配行之后,ContentScale.Fit 是 Image 调用上的独立匹配。
适用场景与限制
适用于 Compose Desktop 中使用暴露软件渲染路径(提供像素缓冲区与尺寸)的原生媒体引擎播放视频、且需要叠加 UI、滚动、多界面复用的场景。像素格式命名、对齐请求、回调线程规则均依具体引擎而定,需查阅对应文档;文中示例针对特定媒体引擎与代码库,具体行为与效果暂未提供通用保证。