视频预览切换为何会闪回旧帧:用 generation + mode identity 管住异步回调

问题与范围

视频预览里快速切换原始画面和处理模式时,常会出现一个很短却很刺眼的问题:新模式已经被选中,画面又闪回旧模式。把旧处理任务 cancel() 看似合理,但旧任务可能已经提交了异步处理,甚至已经进入 Metal command buffer 的完成回调。

本文讨论 AVFoundation 视频预览的呈现时序:怎样让迟到帧不能覆盖当前模式,同时避免新模式首帧抵达前的黑屏。它不讨论视频编码、录制、真机帧率或视觉效果调校。

先给结论

  • 取消负责停止后续工作,不能单独证明已在途结果失去展示资格。
  • 每次模式切换都应生成新的 generation,并和模式本身的 identity 共同构成展示授权。
  • Surface 可以保留上一张稳定画面,却必须拒绝旧 generation 的新提交。
  • ready、failed 等状态回调也要按同一授权检查;一个 generation 的首个 ready 只应报告一次。
  • 实时预览要限制待处理帧数量;版本化授权解决乱序,latest-only 合并解决积压。

取消任务不等于取消已经在途的结果

一次预览帧通常会经过输入、处理 lane、GPU 提交、Surface 呈现和首帧状态通知。模式从 A 切到 B 时,A 可能已经走到后半段。此时取消 A 能阻止新的处理,但不能让每一个已排队的异步回调自动消失。

所以关键问题不是"旧工作是否结束",而是"这个结果是否仍被允许改写当前界面"。这是一条展示层合同,而不是单靠后台任务状态就能表达的事实。

用两部分授权识别当前结果

Kakapos 的 VideoPreviewSession.setMode(_:) 会为新模式创建新的 VideoPreviewProcessingLane,生成单调递增的 VideoPreviewGeneration,同时保存 VideoPreviewModeIdentity。可以把当前授权理解为:

text 复制代码
当前授权 = generation + mode identity

generation 回答"这是第几次模式切换";identity 回答"这次切换到底是哪一种模式与处理计划"。新 lane 安装后会取消旧 lane;但更重要的是,向 Surface 交付帧前,Session 仍会核对两项授权。

Surface 的 submit 只接受与 acceptedGeneration、acceptedIdentity 同时匹配的帧。Metal command buffer 完成后,在通知首帧已呈现之前还会再检查一次。迟到的旧结果可以自然完成资源收口,但既不显示,也不能误报当前模式的 ready。

text 复制代码
模式 A / generation 10 ──异步结果晚到──┐
模式 B / generation 11 ──当前授权──────┼─> 只接收 B 的结果
                                       └─> A 结果丢弃

保留旧画面,不等于继续接收旧帧

直接切黑等待新模式首帧,会带来明显闪烁;继续接收旧 lane 的帧,又会导致画面回流。VideoPreviewSurface.awaitFirstFrame 把两件事拆开:它更新接受中的 generation 和 identity,清空等待提交的帧,但保留已经显示的 displayedSubmission。

因此,旧画面只是暂时留在屏幕上的稳定结果,并没有继续提交的资格。新 generation 的首帧抵达后,再接管展示。这个区分也适用于图片滤镜切换、搜索建议和地图加载:保留上一个可见结果,与接受上一次请求的新返回值,必须是两种状态。

首帧状态也要跟着版本走

Kakapos 把预览 readiness 分为 awaitingFirstFrame、ready、failed 和 cancelled。Session 只会为当前 generation 的第一个有效首帧报告一次 ready。否则,旧模式的延迟回调可能错误隐藏 loading,或让上层把旧画面当成新模式已就绪。

这里的原则很简单:凡是会改变交互状态的异步通知,都要沿用与像素帧相同的授权令牌;不能只给实际图像做版本检查。

版本校验之外,还要处理实时积压

版本校验解决的是旧模式污染当前展示;实时预览还要处理处理速度落后于输入的情况。VideoPreviewProcessingLane 在正在处理时只保留一个 pending frame,后来的帧会替换更旧的 pending frame。

现有测试向 lane 输入 3 帧,最终交付的帧索引是 [1, 3],中间帧被合并并记为一次 coalesced。另一项测试确认:lane 取消后,即使慢处理稍后回报成功,也不再输出帧。前者守住新鲜度,后者守住取消边界;generation 与 identity 则守住模式切换时的呈现正确性。

验证范围与职责边界

本轮在 macOS arm64e 运行 swift test --filter 'MediaEngineTests.testVideoPreviewLaneDropsSlowOutputAfterCancellation|MediaEngineTests.testVideoPreviewLanePropagatesCancellationAndFinalizesMetrics|MediaEngineTests.testVideoPreviewLaneCoalescesBacklogToTheLatestPendingFrame',3 项通过、0 失败。它验证了 lane 的延迟输出拒绝、取消传播与最新 pending 帧合并。

VideoPreviewSurface 的 UIKit/Metal 实际呈现时序仍需在目标设备上验证;本文不把上述 XCTest 外推为真机视觉或性能结论。Kakapos 在这里负责媒体生命周期、预览模式和展示时序;Harbeth 可以作为按帧 GPU 渲染后端提供处理结果,但不拥有预览模式或媒体生命周期。

小结

异步 UI 不应只问任务是否取消,还要问结果是否仍获当前状态授权。把取消、版本和身份分开建模,可以同时做到:停止无用工作、拒绝迟到结果、保留稳定画面,并让首帧状态与真实显示结果一致。

源码与参考实现

相关推荐
老李IT笔记3 小时前
从备份恢复会把管理状态带回来吗:iOS 27 之后答案变了|MDM.Plus
ios·智能手机
谢亮_vipxieliang3 小时前
Go 接口设计原则核心知识点
开发语言·ios·golang
Hello_Pyhx7 小时前
iPhoneMirror:把 iPhone 接入 Windows,先分清投屏与反控
windows·ios·iphone
茶底世界之下13 小时前
类型擦除之后,Metal 滤镜组合为什么不能只执行一个 Filter?
ios·swift
传奇开心果编程15 小时前
【SwiftUI提高练中学】第8课 网络层架构:拦截器、重试、缓存与离线优先
学习·macos·ui·ios·swiftui
传奇开心果编程1 天前
【Compose Multiplatform 跨端开发学与练】第3课 布局与组件
android·windows·学习·ui·ios·kotlin·composer
tink1 天前
告别 Xcode IDE:用 VS Code + SweetPad + XcodeGen 开发 iOS 应用的完整指南
ios·swiftui·xcode
传奇开心果编程1 天前
【Compose Multiplatform 跨端开发学与练】第8课 资源管理与主题
android·windows·学习·ios·kotlin·web·composer
传奇开心果编程1 天前
【Compose Multiplatform 跨端开发学与练】第9课 测试与调试
android·学习·macos·ios·kotlin·web·composer