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

4.9 KiB
Raw Blame History

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 恢复或脚本重编译后出现,导致 PlayVideoif (_isPlaying) return 跳过初始化,而 ScrubToFrameAndDisplay_decoder==null 直接返回。

InternalPlay 返回值修正

LeviathanSoftwareDecoder.InternalPlay 中,当 syncDecodeFirstFrameDecodeNextFrame 失败后,改为返回错误码(原来无论成功失败均返回 0PlayVideo 仅在 syncDecodeFirstFrame && ret == 0 时调用 DisplayFrame,避免无有效帧时越界。

DisplayFrame / InitTextures 加固

  • DisplayFrame 增加 _decoderVideoFrame 空指针判断,以及 _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.effectiveWeightFrameData 上经过上层混合后的有效权重)。

2. IndexOutOfRangeException in DisplayFrame

原因InternalPlay 首帧解码失败时仍返回 0PlayVideo 照常调用 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。PlayVideoif (_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。

性能优化

  • 顺序帧免 SeekProcessFrame 中检测到 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 建议设为视频实际时长