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
}
}