Files
ft/Client/Assets/Scripts/Common/Timeline/LeviathanVideo/开发记录.md
2026-06-29 21:18:33 +08:00

86 lines
4.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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` 建议设为视频实际时长