问题与范围
视频预览里快速切换原始画面和处理模式时,常会出现一个很短却很刺眼的问题:新模式已经被选中,画面又闪回旧模式。把旧处理任务 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 不应只问任务是否取消,还要问结果是否仍获当前状态授权。把取消、版本和身份分开建模,可以同时做到:停止无用工作、拒绝迟到结果、保留稳定画面,并让首帧状态与真实显示结果一致。