86 lines
4.9 KiB
Markdown
86 lines
4.9 KiB
Markdown
# 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` 建议设为视频实际时长
|