4.9 KiB
Leviathan Video Timeline 轨道 — 开发记录
需求
参照 DOTweenTrack 的写法,为 LeviathanVideoDecoder 实现一套 Unity Timeline 自定义轨道,支持:
- 在 Timeline 中控制视频播放时机
- 编辑器预览模式下拖动进度条时视频画面逐帧跟随
- 运行时播放时按 Timeline 时间驱动解码
新增文件
| 文件 | 作用 |
|---|---|
LeviathanVideoTrack.cs |
TrackAsset 子类,绑定类型为 LeviathanVideoDecoder,注册 Clip 类型 |
LeviathanVideoPlayableAsset.cs |
PlayableAsset 子类,承载片段参数(videoOverride、startOffset、duration 等) |
LeviathanVideoBehaviour.cs |
PlayableBehaviour 子类,每帧计算视频帧号并驱动解码器 Seek/Decode/Display |
LeviathanVideoDecoderBase 修改点
新增 API
ScrubToFrameAndDisplay(long frameIndex):Seek 到指定帧并同步解码显示一帧,供 Timeline 等外部时间源调用。仅支持非多线程解码。TryAdvanceOneFrameForTimeline():顺序解码下一帧(不 Seek),用于连续帧场景下避免每帧 Seek 的开销。IsDecoderValid属性:_isPlaying && _decoder != null,用于检测解码器是否处于可用状态。
PlayVideo 防御修复
PlayVideo 开头增加对 _isPlaying=true 但 _decoder=null 这种不一致状态的检测与修复。该状态可能在编辑器 Preview 恢复或脚本重编译后出现,导致 PlayVideo 因 if (_isPlaying) return 跳过初始化,而 ScrubToFrameAndDisplay 因 _decoder==null 直接返回。
InternalPlay 返回值修正
LeviathanSoftwareDecoder.InternalPlay 中,当 syncDecodeFirstFrame 时 DecodeNextFrame 失败后,改为返回错误码(原来无论成功失败均返回 0)。PlayVideo 仅在 syncDecodeFirstFrame && ret == 0 时调用 DisplayFrame,避免无有效帧时越界。
DisplayFrame / InitTextures 加固
DisplayFrame增加_decoder、VideoFrame空指针判断,以及_textures长度与[0]非空校验。InitTextures销毁旧纹理时按实际长度循环,避免数组越界。
编辑器纹理销毁修复
新增 DestroyOwnedUnityObject 辅助方法:UNITY_EDITOR 下统一使用 DestroyImmediate,非编辑器使用 Destroy。解决 Timeline 预览模式下 Destroy may not be called from edit mode 的错误。
开发过程中遇到的问题与解决
1. IndexOutOfRangeException: inputIndex 0
原因:playable.GetWeight() / GetInputWeight(0) 在叶子 ScriptPlayable 上越界(该 Playable 没有子输入)。
修复:改用 info.effectiveWeight(FrameData 上经过上层混合后的有效权重)。
2. IndexOutOfRangeException in DisplayFrame
原因:InternalPlay 首帧解码失败时仍返回 0,PlayVideo 照常调用 DisplayFrame,此时无有效视频帧数据导致数组越界。
修复:InternalPlay 首帧解码失败时返回错误码;PlayVideo 仅在 ret == 0 时才 DisplayFrame。
3. Destroy may not be called from edit mode
原因:Timeline 编辑器预览触发 InitTextures,在非 Play 模式下调用 Destroy 被 Unity 禁止。
修复:UNITY_EDITOR 下统一 DestroyImmediate。
4. 编辑器预览不生效(_decoder=null 但 _isPlaying=true)
原因:编辑器 Preview 恢复或脚本重编译后,_decoder(非序列化字段)丢失为 null,但 _isPlaying 仍为 true。PlayVideo 因 if (_isPlaying) return 跳过初始化,ScrubToFrameAndDisplay 因 _decoder==null 直接返回。
修复:
PlayVideo开头检测不一致状态并重置_isPlaying = falseProcessFrame中通过IsDecoderValid检测解码器失效后强制重新准备- 新增
IsDecoderValid属性暴露内部状态
5. 清理 Debug.Log 后预览再次失效
原因:Debug.Log 会触发 Console 窗口刷新,间接引发编辑器全局重绘。移除后 InternalEditorUtility.RepaintAllViews() 在当前 GUI 事件内调用,时机过早被 Unity 合并/忽略。
修复:RepaintAllViews 放入 EditorApplication.delayCall,推迟到当前帧结束后执行,确保纹理已上传 GPU。
性能优化
- 顺序帧免 Seek:
ProcessFrame中检测到frame == lastFrame + 1时走TryAdvanceOneFrameForTimeline(仅DecodeNextFrame),跳过SeekToFrame的开销 - 同帧跳过:
frame == _lastScrubFrame时直接返回,不重复解码 - 编辑器重绘:
Canvas.ForceUpdateCanvases()+delayCall延迟RepaintAllViews,仅编辑模式生效,不影响运行时
使用方式
- Timeline 窗口 → Add Track → Leviathan Video Track
- 轨道绑定字段拖入挂有
LeviathanVideoDecoder的 GameObject - 轨道上添加 Leviathan Video Clip,配置视频文件和时长
Multithreaded Decode保持关闭(Timeline 擦洗需要同步解码)Clip Duration建议设为视频实际时长