using System; using UnityEngine; using UnityEngine.Playables; using UnityEngine.Timeline; #if UNITY_EDITOR using UnityEditor; #endif namespace FT.Timeline { /// 多线程 Timeline 驱动策略(与 对应)。 public enum LeviathanVideoTimelineMtDriveMode { /// 不外接时钟;子线程持续解码,ProcessFrame 只消费就绪帧追上 Timeline(BossFight 等高帧率推荐)。序列化默认 0 以便旧资源无字段时自动走此模式。 DecodeThreadCatchUp = 0, /// 外接时钟 + SyncMultithreadedToTimelineFrame(每拍 Pause/握手,仅当 CatchUp 有异常时选用)。 ProcessFrameLockstep = 1, } [Serializable] #if UNITY_EDITOR [System.ComponentModel.DisplayName("Leviathan Video Clip")] #endif /// /// 在 Leviathan 视频轨上新建片段时,若 Timeline 窗口中已为该轨绑定解码器,会自动把 同步为当前视频资源与时长。 /// 在 Inspector 中更换 时也会按 bytes 内元数据(含 Duration / 帧推算)自动写入 ;亦可使用检视面板「从视频同步时长」手动对齐 Timeline 片段长度。 /// 性能分析:代码里设 LeviathanLogConfig.EnableTimelineVideoPerfLog = true,或 PlayerPrefs LeviathanTimelinePerf=1 后重启,看 Logcat 中 [LeviathanTimelinePerf]。 /// public class LeviathanVideoPlayableAsset : PlayableAsset, ITimelineClipAsset { [Tooltip("留空则使用绑定物体上 LeviathanVideoDecoder 的 bytesFile")] [SerializeField] public TextAsset videoOverride; [Tooltip("片段在 Timeline 上的长度,建议与视频时长一致;指定 Video Override 后会自动按资源解析时长")] [SerializeField] [Min(0.01f)] public double clipDurationSeconds = 5.0; /// 编辑器:上次已根据 自动同步过时长的资源实例 ID,避免反复覆盖用户手改的长度。 [SerializeField, HideInInspector] int _editorLastAutoSyncVideoOverrideInstanceId; [SerializeField] public bool syncDecodeFirstFrame = true; [SerializeField] public bool fastDecode = true; [Tooltip("多线程解码并由 ProcessFrame 对齐 Timeline;主线程只做握手与显示,解码在子线程。顺序播时会预解下一帧;编辑器预览同样生效(需可写 Pause/Resume)。")] [SerializeField] public bool useMultithreadedDecode = true; [Tooltip("多线程时:CatchUp=子线程持续解码、ProcessFrame 只取帧(近 Match 待机,BossFight 推荐);Lockstep=外接时钟+每帧 Sync(旧行为)。")] [SerializeField] public LeviathanVideoTimelineMtDriveMode timelineMultithreadedDriveMode = LeviathanVideoTimelineMtDriveMode.DecodeThreadCatchUp; [Tooltip("仅当开启多线程并由 Timeline 驱动时生效:等解码线程交出下一帧的最长等待(毫秒)。过小易频繁 Seek 更卡;过大阻塞主线程。顺序播放建议 100~200。")] [SerializeField] [Min(8)] public int timelineMultithreadedWaitMs = 150; [Tooltip("CatchUp:单次 ProcessFrame 最多消费几帧就绪(与「整帧预算」取较小值)。≤0 表示 8。")] [SerializeField] [Min(0)] public int timelineCatchUpMaxConsumePerCall = 0; [Tooltip("CatchUp:同一 Unity 帧内所有 ProcessFrame 合计最多几次 DisplayFrame(抑 60Hz 求值×多遍 Graph)。≤0 时 Android=6,其它=8。")] [SerializeField] [Min(0)] public int timelineCatchUpUnityFrameDisplayBudget = 0; [Tooltip("勾选后整帧 Display 预算再封顶为 3,适合约 24fps 片源 + 60Hz Timeline。")] [SerializeField] public bool timelineCatchUpConservativeLowFpsBudget; /// 必须包含 ,否则 Timeline 会禁止掐头(clipIn 无法写入、始终为 0)。 public ClipCaps clipCaps => ClipCaps.ClipIn; public override double duration => clipDurationSeconds; public override Playable CreatePlayable(PlayableGraph graph, GameObject owner) { var playable = ScriptPlayable.Create(graph); var behaviour = playable.GetBehaviour(); behaviour.videoOverride = videoOverride; behaviour.syncDecodeFirstFrame = syncDecodeFirstFrame; behaviour.fastDecode = fastDecode; behaviour.useMultithreadedDecode = useMultithreadedDecode; behaviour.timelineMultithreadedDriveMode = timelineMultithreadedDriveMode; behaviour.timelineMultithreadedWaitMs = timelineMultithreadedWaitMs; behaviour.sourcePlayableAsset = this; return playable; } /// 从容积 bytes()解析时长,与解码器 Duration / 帧数推算一致。 public static bool TryGetClipDurationSecondsFromTextAsset(TextAsset file, out double seconds) { seconds = 0; if (file == null) return false; if (!LeviathanVideoDecoderBase.TryProbeVideoMetaFromBytes(file, out seconds, out _, out _)) return false; return seconds > 0.0001; } /// 优先使用解码器已得到的 Duration / 帧间隔,否则回退解析 public static bool TryGetClipDurationSecondsFromDecoder(LeviathanVideoDecoderBase decoder, out double seconds) { seconds = 0; if (decoder == null) return false; if (decoder.Duration > 0) seconds = decoder.Duration / 1_000_000.0; else if (decoder.FrameCount > 0 && decoder.FrameInterval > 0) seconds = decoder.FrameCount * decoder.FrameInterval; if (seconds > 0.0001) return true; return decoder.bytesFile != null && TryGetClipDurationSecondsFromTextAsset(decoder.bytesFile, out seconds); } #if UNITY_EDITOR void OnValidate() { if (videoOverride == null) { _editorLastAutoSyncVideoOverrideInstanceId = 0; return; } var id = videoOverride.GetInstanceID(); if (id == _editorLastAutoSyncVideoOverrideInstanceId) return; _editorLastAutoSyncVideoOverrideInstanceId = id; if (TryGetClipDurationSecondsFromTextAsset(videoOverride, out var sec)) { clipDurationSeconds = sec; EditorSyncParentTimelineClipDurations(this); } EditorUtility.SetDirty(this); } /// 编辑器:将父级 Timeline 中引用本资源的 长度与 对齐。 public static void EditorSyncParentTimelineClipDurations(LeviathanVideoPlayableAsset asset) { if (asset == null) return; var path = AssetDatabase.GetAssetPath(asset); if (string.IsNullOrEmpty(path)) return; if (AssetDatabase.LoadMainAssetAtPath(path) is not TimelineAsset timelineAsset) return; var dirty = false; var targetDur = asset.duration; foreach (var track in timelineAsset.GetOutputTracks()) { foreach (var clip in track.GetClips()) { if (clip.asset != asset) continue; if (System.Math.Abs(clip.duration - targetDur) > 1e-6) { clip.duration = targetDur; dirty = true; } } } if (dirty) EditorUtility.SetDirty(timelineAsset); } #endif } }