Files
ft/Client/Assets/Scripts/Common/Timeline/LeviathanVideo/LeviathanVideoPlayableAsset.cs
2026-06-29 21:18:33 +08:00

178 lines
8.3 KiB
C#
Raw Permalink 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.
using System;
using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.Timeline;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace FT.Timeline
{
/// <summary>多线程 Timeline 驱动策略(与 <see cref="LeviathanVideoPlayableAsset.timelineMultithreadedDriveMode"/> 对应)。</summary>
public enum LeviathanVideoTimelineMtDriveMode
{
/// <summary>不外接时钟子线程持续解码ProcessFrame 只消费就绪帧追上 TimelineBossFight 等高帧率推荐)。序列化默认 0 以便旧资源无字段时自动走此模式。</summary>
DecodeThreadCatchUp = 0,
/// <summary>外接时钟 + <c>SyncMultithreadedToTimelineFrame</c>(每拍 Pause/握手,仅当 CatchUp 有异常时选用)。</summary>
ProcessFrameLockstep = 1,
}
[Serializable]
#if UNITY_EDITOR
[System.ComponentModel.DisplayName("Leviathan Video Clip")]
#endif
/// <summary>
/// 在 Leviathan 视频轨上<strong>新建</strong>片段时,若 Timeline 窗口中已为该轨绑定解码器,会自动把 <see cref="videoOverride"/> 与 <see cref="clipDurationSeconds"/> 同步为当前视频资源与时长。
/// 在 Inspector 中更换 <see cref="videoOverride"/> 时也会按 bytes 内元数据(含 Duration / 帧推算)自动写入 <see cref="clipDurationSeconds"/>;亦可使用检视面板「从视频同步时长」手动对齐 Timeline 片段长度。
/// 性能分析:代码里设 <c>LeviathanLogConfig.EnableTimelineVideoPerfLog = true</c>,或 PlayerPrefs <c>LeviathanTimelinePerf=1</c> 后重启,看 Logcat 中 <c>[LeviathanTimelinePerf]</c>。
/// </summary>
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;
/// <summary>编辑器:上次已根据 <see cref="videoOverride"/> 自动同步过时长的资源实例 ID避免反复覆盖用户手改的长度。</summary>
[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;
/// <summary>必须包含 <see cref="ClipCaps.ClipIn"/>,否则 Timeline 会禁止掐头(<c>clipIn</c> 无法写入、始终为 0。</summary>
public ClipCaps clipCaps => ClipCaps.ClipIn;
public override double duration => clipDurationSeconds;
public override Playable CreatePlayable(PlayableGraph graph, GameObject owner)
{
var playable = ScriptPlayable<LeviathanVideoBehaviour>.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;
}
/// <summary>从容积 bytes<see cref="LeviathanVideoDecoderBase.TryProbeVideoMetaFromBytes"/>)解析时长,与解码器 Duration / 帧数推算一致。</summary>
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;
}
/// <summary>优先使用解码器已得到的 Duration / 帧间隔,否则回退解析 <see cref="LeviathanVideoDecoderBase.bytesFile"/>。</summary>
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);
}
/// <summary>编辑器:将父级 Timeline 中引用本资源的 <see cref="TimelineClip"/> 长度与 <see cref="clipDurationSeconds"/> 对齐。</summary>
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
}
}