# 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 = false` - `ProcessFrame` 中通过 `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`,仅编辑模式生效,不影响运行时 ## 使用方式 1. Timeline 窗口 → Add Track → **Leviathan Video Track** 2. 轨道绑定字段拖入挂有 `LeviathanVideoDecoder` 的 GameObject 3. 轨道上添加 **Leviathan Video Clip**,配置视频文件和时长 4. `Multithreaded Decode` 保持关闭(Timeline 擦洗需要同步解码) 5. `Clip Duration` 建议设为视频实际时长