1592 lines
54 KiB
C#
1592 lines
54 KiB
C#
/*----------------------------------------------------------------
|
||
// Copyright (C) 2025 Beijing All rights reserved.
|
||
//
|
||
// Author: huachangmiao
|
||
// Create Date: 2025/04/11
|
||
// Module Describe: 视频解码器抽象基类
|
||
//----------------------------------------------------------------*/
|
||
|
||
using System.Diagnostics;
|
||
using System.Threading;
|
||
using UnityEngine;
|
||
using Unity.Collections;
|
||
#if UNITY_WEBGL && !UNITY_EDITOR
|
||
using System.Runtime.InteropServices;
|
||
#endif
|
||
|
||
/// <summary>
|
||
/// 视频解码器抽象基类
|
||
/// 包含解码逻辑、播放控制、多线程解码等公共功能
|
||
/// 子类需要实现具体的渲染逻辑
|
||
/// </summary>
|
||
public abstract unsafe class LeviathanVideoDecoderBase : MonoBehaviour
|
||
{
|
||
protected static readonly int ValidWidthRatioIdx = Shader.PropertyToID("_ValidWidthRatio");
|
||
protected static readonly int UTexIdx = Shader.PropertyToID("_UTex");
|
||
protected static readonly int VTexIdx = Shader.PropertyToID("_VTex");
|
||
protected static readonly int AlphaYTexIdx = Shader.PropertyToID("_AYTex");
|
||
protected static readonly int AlphaUTexIdx = Shader.PropertyToID("_AUTex");
|
||
|
||
#if UNITY_WEBGL && !UNITY_EDITOR
|
||
[DllImport("__Internal")]
|
||
private static extern bool IsWechatMiniGameEnvironment();
|
||
#endif
|
||
|
||
// 日志辅助方法
|
||
protected static void Log(string message)
|
||
{
|
||
LeviathanLogConfig.Log("VideoDecoderBase", message);
|
||
}
|
||
|
||
protected static void LogWarning(string message)
|
||
{
|
||
LeviathanLogConfig.LogWarning("VideoDecoderBase", message);
|
||
}
|
||
|
||
protected static void LogError(string message)
|
||
{
|
||
LeviathanLogConfig.LogError("VideoDecoderBase", message);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 静态构造函数 - 在类首次使用时执行平台检测
|
||
/// </summary>
|
||
static LeviathanVideoDecoderBase()
|
||
{
|
||
#if UNITY_WEBGL && !UNITY_EDITOR
|
||
try
|
||
{
|
||
IsWCGame = IsWechatMiniGameEnvironment();
|
||
Log($"微信小游戏平台静态检测: {IsWCGame}");
|
||
}
|
||
catch (System.Exception e)
|
||
{
|
||
LogError($"微信小游戏平台静态检测异常: {e}");
|
||
IsWCGame = false;
|
||
}
|
||
#else
|
||
IsWCGame = false;
|
||
#endif
|
||
}
|
||
|
||
// [LabelText("视频文件(.bytes)")]
|
||
public TextAsset bytesFile;
|
||
|
||
#if UNITY_EDITOR
|
||
[System.NonSerialized]
|
||
protected TextAsset _lastBytesFile;
|
||
#endif
|
||
|
||
private ILeviathanDecoder _decoder;
|
||
|
||
/// <summary>
|
||
/// 获取当前视频帧指针(子类使用)
|
||
/// </summary>
|
||
protected LeviathanVideo.Abstractions.AVFrame* VideoFrame => _decoder != null ? _decoder.VideoFrame : null;
|
||
|
||
#if !UNITY_2022_1_OR_NEWER
|
||
// Unity 2020/2021 兼容:保存数据副本以供解码器使用
|
||
protected NativeArray<byte> _videoDataBuffer;
|
||
protected bool _hasVideoDataBuffer = false;
|
||
#endif
|
||
|
||
// 平台检测
|
||
public static bool IsWCGame = false;
|
||
|
||
protected const int TextureCount = 3;
|
||
protected Texture2D[] _textures;
|
||
protected double _timeAccumulated;
|
||
protected double _frameInterval;
|
||
public double FrameInterval => _frameInterval;
|
||
protected bool _needInitTexture = true;
|
||
|
||
protected bool _needApplyTexture = false;
|
||
|
||
// 是否为多线程解码
|
||
protected bool _isMultithreadedDecode;
|
||
/// <summary>当前实例是否正在使用多线程解码(WebGL 等平台上请求多线程也会被降级为 false)。
|
||
/// </summary>
|
||
public bool IsMultithreadedDecodeActive => _isMultithreadedDecode;
|
||
|
||
/// <summary>
|
||
/// 为 true 时由 Timeline 的 <c>ProcessFrame</c> 驱动显示节拍,<see cref="Update"/> 不再按 <c>Time.deltaTime</c> 消费 <c>_hasFrameReady</c>,
|
||
/// 避免与 Playable 时间轴脱节;须配合 <see cref="SyncMultithreadedToTimelineFrame"/> 使用。
|
||
/// </summary>
|
||
protected bool _timelineMultithreadedExternalClock;
|
||
|
||
public bool TimelineMultithreadedExternalClockActive => _timelineMultithreadedExternalClock;
|
||
|
||
/// <summary>由 Timeline 在起播前设置;停止时 <see cref="InternalStopSync"/> 会清零。</summary>
|
||
public void SetTimelineMultithreadedExternalClock(bool active)
|
||
{
|
||
_timelineMultithreadedExternalClock = active;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 与 <see cref="SetTimelineMultithreadedExternalClock"/> 二选一(多线程时):为 true 时不走外接时钟,
|
||
/// 子线程按握手持续解码,<c>ProcessFrame</c> 仅通过 <see cref="TimelineCatchUpPresentToFrame"/> 消费 <c>_hasFrameReady</c> 追上 Timeline 帧号,接近 UI 待机多线程路径。
|
||
/// </summary>
|
||
protected bool _timelineMultithreadedCatchUpMode;
|
||
|
||
public bool TimelineMultithreadedCatchUpModeActive => _timelineMultithreadedCatchUpMode;
|
||
|
||
public void SetTimelineMultithreadedCatchUpMode(bool active)
|
||
{
|
||
_timelineMultithreadedCatchUpMode = active;
|
||
}
|
||
|
||
// 多线程相关
|
||
protected Thread _decodeThread;
|
||
// 帧状态:0=允许解码下一帧, 1=帧已准备好等待显示
|
||
protected long _hasFrameReady;
|
||
|
||
protected volatile bool _isPlaying;
|
||
public bool IsPlaying => _isPlaying;
|
||
|
||
/// <summary>
|
||
/// 解码器是否处于可用状态(isPlaying 且内部解码器实例存在)
|
||
/// </summary>
|
||
public bool IsDecoderValid => _isPlaying && _decoder != null;
|
||
|
||
protected volatile bool _isPause;
|
||
public bool IsPause => _isPause;
|
||
|
||
// 销毁状态标志(防止销毁期间的异步操作继续执行)
|
||
protected bool _isDestroying = false;
|
||
|
||
// 第一帧回调待执行标志(用于从子线程安全地在主线程执行回调)
|
||
protected volatile bool _pendingFirstFrameCallback = false;
|
||
|
||
/// <summary>解码线程在播完时置位,由 <see cref="Update"/> 在主线程触发 <see cref="OnPlayCompleted"/>。</summary>
|
||
protected volatile bool _pendingPlayCompleted;
|
||
|
||
// ==区间循环专用
|
||
|
||
// 每次播放到End, 跳转到的frame
|
||
protected long _loopBeginFrameIndex = -1;
|
||
public long LoopBeginFrameIndex => _loopBeginFrameIndex;
|
||
|
||
protected long _loopEndFrameIndex = -1;
|
||
public long LoopEndFrameIndex => _loopEndFrameIndex;
|
||
|
||
// 是否正在循环seek中(防止SeekToFrame被多次调用)
|
||
protected volatile bool _isLoopSeeking = false;
|
||
|
||
protected string _codecName = "";
|
||
|
||
// 总时长 微秒
|
||
protected long _duration;
|
||
public long Duration => _duration;
|
||
|
||
protected int _videoWidth;
|
||
public int VideoWidth => _videoWidth;
|
||
|
||
protected int _videoHeight;
|
||
public int VideoHeight => _videoHeight;
|
||
|
||
protected int _videoValidWidth;
|
||
public int VideoValidWidth => _videoValidWidth;
|
||
|
||
public long PlayFrameIndex => _playFrameIndex;
|
||
protected long _playFrameIndex;
|
||
|
||
// 总帧数
|
||
protected long _frameCount;
|
||
public long FrameCount => _frameCount;
|
||
|
||
/// <summary>由 Timeline 的 LeviathanVideoBehaviour 在 Prepare 时从片段写入;非 Timeline 播片使用默认值即可。</summary>
|
||
private int _timelineMultithreadedWaitMs = 64;
|
||
|
||
/// <summary>运行态 Timeline 多线程:为 true 时 <see cref="SyncMultithreadedToTimelineFrame"/> 内只写 CPU 纹理,结束后再 <see cref="FlushDeferredTimelineDisplay"/>,对齐 MatchGame 等非 Timeline 路径「单次 Apply」。</summary>
|
||
private bool _timelineMultithreadedDeferGpuApply;
|
||
|
||
/// <summary>由 Timeline 的 LeviathanVideoBehaviour 在运行态包在 Sync 外;勿长期置 true。</summary>
|
||
public void SetTimelineMultithreadedDeferGpuApply(bool defer)
|
||
{
|
||
_timelineMultithreadedDeferGpuApply = defer;
|
||
}
|
||
|
||
private bool MultithreadedTimelineApplyTexturesImmediately => !_timelineMultithreadedDeferGpuApply;
|
||
|
||
private int _timelinePerfSeq;
|
||
private int _lateApplyPerfSeq;
|
||
|
||
private void LogTimelineVideoPerf(string phase, long target, long curBefore, long curAfter, int consumes, long elapsedMs,
|
||
bool reached, string note = null)
|
||
{
|
||
if (!LeviathanLogConfig.EnableTimelineVideoPerfLog)
|
||
return;
|
||
var seq = ++_timelinePerfSeq;
|
||
var periodic = (seq % 50) == 0;
|
||
var slow = elapsedMs >= 8;
|
||
var interesting = note != null || !reached;
|
||
if (!interesting && !slow && !periodic)
|
||
return;
|
||
|
||
var msg = $"{phase} tgt={target} bef={curBefore} aft={curAfter} cons={consumes} {elapsedMs}ms ok={reached}";
|
||
if (note != null)
|
||
msg += " " + note;
|
||
msg += $" [{name}]";
|
||
LeviathanLogConfig.Log("TimelineVideoPerf", msg);
|
||
}
|
||
|
||
[SerializeField]
|
||
[Tooltip("播放速度倍率:1 为正常;越大越快(与 Time.deltaTime 相乘推进时间轴)")]
|
||
[Min(0.01f)]
|
||
protected float _playSpeed = 1f;
|
||
|
||
/// <summary>Timeline 多线程同步时 <see cref="WaitMultithreadedFrameReady"/> 的毫秒上限(钳制 8~500)。</summary>
|
||
public int TimelineMultithreadedWaitMs
|
||
{
|
||
get => _timelineMultithreadedWaitMs;
|
||
set => _timelineMultithreadedWaitMs = Mathf.Clamp(value, 8, 500);
|
||
}
|
||
|
||
/// <summary>Timeline CatchUp:单次 <see cref="TimelineCatchUpPresentToFrame"/> 内最多消费几帧就绪(与整帧预算取 min)。</summary>
|
||
int _timelineCatchUpMaxConsumePerCall = 8;
|
||
|
||
/// <summary>同一 Unity 帧内所有 CatchUp 调用合计的 DisplayFrame 上限;新帧开始时重置。</summary>
|
||
int _timelineCatchUpUnityFrameDisplayBudget = 8;
|
||
|
||
int _catchUpBudgetUnityFrame = -1;
|
||
int _catchUpRemainingDisplayBudget;
|
||
|
||
public int TimelineCatchUpMaxConsumePerCall
|
||
{
|
||
get => _timelineCatchUpMaxConsumePerCall;
|
||
set => _timelineCatchUpMaxConsumePerCall = Mathf.Clamp(value, 1, 32);
|
||
}
|
||
|
||
public int TimelineCatchUpUnityFrameDisplayBudget
|
||
{
|
||
get => _timelineCatchUpUnityFrameDisplayBudget;
|
||
set => _timelineCatchUpUnityFrameDisplayBudget = Mathf.Clamp(value, 1, 32);
|
||
}
|
||
|
||
/// <summary>播放速度倍率(读写均会钳制在合理范围)。</summary>
|
||
public float PlaySpeed
|
||
{
|
||
get => _playSpeed;
|
||
set => SetPlaySpeed(value);
|
||
}
|
||
|
||
public enum PlayAlphaType
|
||
{
|
||
None = 0,
|
||
RightSide = 1,
|
||
BottomSide = 2,
|
||
ChromaKey = 3,
|
||
}
|
||
|
||
public PlayAlphaType AlphaType
|
||
{
|
||
get => _alphaType;
|
||
set
|
||
{
|
||
if (_alphaType != value)
|
||
{
|
||
SetAlphaType(value);
|
||
}
|
||
}
|
||
}
|
||
|
||
[SerializeField]
|
||
protected bool disableAutoSize;
|
||
[SerializeField]
|
||
protected bool autoPlayOnStart;
|
||
|
||
public bool DisableAutoSize
|
||
{
|
||
get => disableAutoSize;
|
||
set => disableAutoSize = value;
|
||
}
|
||
|
||
[SerializeField]
|
||
protected bool _isLooping = true;
|
||
|
||
public bool IsLooping => _isLooping;
|
||
|
||
/// <summary>
|
||
/// 播放完成回调事件(非循环播放时触发)
|
||
/// </summary>
|
||
public System.Action OnPlayCompleted;
|
||
|
||
/// <summary>
|
||
/// 首帧渲染完毕回调事件
|
||
/// 无论同步或异步解码首帧模式都会触发
|
||
/// </summary>
|
||
public System.Action OnFirstFrameRendered;
|
||
|
||
[SerializeField]
|
||
protected PlayAlphaType _alphaType = PlayAlphaType.None;
|
||
|
||
// 上次应用的alphaType,用于检测是否需要更新材质
|
||
protected PlayAlphaType _lastAppliedAlphaType = PlayAlphaType.None;
|
||
|
||
protected virtual void Start()
|
||
{
|
||
if (autoPlayOnStart && bytesFile)
|
||
Play();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 与 <see cref="OnDisable"/> 成对:重新激活后继续解码(含 Timeline / 外部在隐藏时调用过 <see cref="Pause"/> 的情况)。
|
||
/// </summary>
|
||
protected virtual void OnEnable()
|
||
{
|
||
if (!_isPlaying || !_isPause)
|
||
return;
|
||
#if UNITY_EDITOR
|
||
if (!Application.isPlaying)
|
||
EditorResumeDecoderIgnoringPlayMode();
|
||
else
|
||
#endif
|
||
Resume();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 未激活或组件禁用时暂停,避免隐藏物体继续耗 CPU(尤其多线程解码);见 <see cref="OnEnable"/> 恢复。
|
||
/// </summary>
|
||
protected virtual void OnDisable()
|
||
{
|
||
if (!_isPlaying)
|
||
return;
|
||
#if UNITY_EDITOR
|
||
if (!Application.isPlaying)
|
||
{
|
||
EditorPauseDecoderIgnoringPlayMode();
|
||
return;
|
||
}
|
||
#endif
|
||
Pause();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 第一帧解码完成回调(由解码器触发,可能在子线程中调用)
|
||
/// 设置标志位,实际处理在主线程的 Update 中执行
|
||
/// </summary>
|
||
protected virtual void OnFirstFrameDecodedCallback()
|
||
{
|
||
_pendingFirstFrameCallback = true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 处理第一帧解码完成的实际逻辑(必须在主线程调用)
|
||
/// 子类需要重写此方法来处理渲染目标的显示
|
||
/// </summary>
|
||
protected virtual void HandleFirstFrameDecoded()
|
||
{
|
||
// 先更新纹理,确保显示的是新视频的第一帧
|
||
DisplayFrame();
|
||
|
||
// 子类负责启用渲染目标
|
||
OnEnableRenderTarget();
|
||
|
||
// 触发首帧渲染完毕回调
|
||
OnFirstFrameRendered?.Invoke();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 启用渲染目标(子类实现)
|
||
/// </summary>
|
||
protected abstract void OnEnableRenderTarget();
|
||
|
||
/// <summary>
|
||
/// 禁用渲染目标(子类实现)
|
||
/// </summary>
|
||
protected abstract void OnDisableRenderTarget();
|
||
|
||
protected virtual void OnDestroy()
|
||
{
|
||
Log("开始销毁,清理所有资源");
|
||
|
||
// 立即设置销毁标志,阻止任何新的操作
|
||
_isDestroying = true;
|
||
|
||
_isPlaying = false;
|
||
|
||
// 强制同步停止,避免异步操作
|
||
try
|
||
{
|
||
InternalStopSync();
|
||
}
|
||
catch (System.Exception e)
|
||
{
|
||
LogWarning($"同步停止时出现异常: {e.Message}");
|
||
}
|
||
|
||
// Release textures
|
||
if (_textures != null && _textures.Length > 0)
|
||
{
|
||
foreach (var tex in _textures)
|
||
{
|
||
if (tex)
|
||
DestroyOwnedUnityObject(tex);
|
||
}
|
||
}
|
||
_textures = null;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 播放视频
|
||
/// </summary>
|
||
/// <param name="fileData">视频文件</param>
|
||
/// <param name="playAlpha">播放透明度</param>
|
||
/// <param name="multithreadedDecode">是否多线程解码</param>
|
||
/// <param name="beginFrameIndex">开始播放的帧索引</param>
|
||
/// <param name="syncDecodeFirstFrame">是否同步解码第一帧, 区别在于是否当前帧就能立马看到视频, 会耗时10ms</param>
|
||
/// <param name="fastDecode">是否启用快速解码(跳过环路滤波器),默认开启以提升性能</param>
|
||
/// <returns>返回值: 0表示成功, 其他表示错误码</returns>
|
||
public int PlayVideo(TextAsset fileData, PlayAlphaType playAlpha, bool multithreadedDecode, int beginFrameIndex = 1, bool syncDecodeFirstFrame = true, bool fastDecode = true)
|
||
{
|
||
// 如果正在销毁,立即返回
|
||
if (_isDestroying)
|
||
{
|
||
LogWarning("PlayVideo: 组件正在销毁,取消播放");
|
||
return -1;
|
||
}
|
||
|
||
if (fileData == null)
|
||
{
|
||
return -1;
|
||
}
|
||
var ret = 0;
|
||
// 防御:_isPlaying 为 true 但解码器实例丢失(编辑器 Preview 恢复/脚本重编译等)
|
||
if (_isPlaying && _decoder == null)
|
||
{
|
||
LogWarning("PlayVideo: _isPlaying 为 true 但 _decoder 为 null,重置状态");
|
||
_isPlaying = false;
|
||
}
|
||
if (_isPlaying) return ret;
|
||
|
||
_playFrameIndex = 0;
|
||
_timeAccumulated = 0;
|
||
_isLoopSeeking = false;
|
||
|
||
_alphaType = playAlpha;
|
||
|
||
#if UNITY_WEBGL
|
||
// webGL不支持c#多线程
|
||
_isMultithreadedDecode = false;
|
||
#else
|
||
_isMultithreadedDecode = multithreadedDecode;
|
||
#endif
|
||
|
||
// 根据平台选择解码器
|
||
if (IsWCGame)
|
||
{
|
||
OnDisableRenderTarget();
|
||
// 创建新的decoder - 使用Worker版本
|
||
var workerDecoder = new LeviathanWorkerDecoder();
|
||
workerDecoder.IsLooping = _isLooping; // 设置循环播放状态
|
||
_decoder = workerDecoder;
|
||
Log($"[PlayWithData] 创建新的微信小游戏Worker解码器 (静态检测: {IsWCGame}, 循环播放: {_isLooping})");
|
||
}
|
||
else
|
||
{
|
||
// 只有同步解码第一帧时才立即启用,否则等待第一帧解码完成后再启用
|
||
if (syncDecodeFirstFrame)
|
||
{
|
||
OnEnableRenderTarget();
|
||
}
|
||
else
|
||
{
|
||
OnDisableRenderTarget();
|
||
}
|
||
_decoder = new LeviathanSoftwareDecoder();
|
||
Log($"[PlayWithData] 使用软件解码器 (静态检测: {IsWCGame})");
|
||
}
|
||
_needInitTexture = true;
|
||
|
||
// 订阅第一帧回调事件
|
||
_decoder.OnFirstFrameDecoded += OnFirstFrameDecodedCallback;
|
||
|
||
// 初始化解码器
|
||
bytesFile = fileData;
|
||
#if UNITY_EDITOR
|
||
_lastBytesFile = bytesFile;
|
||
#endif
|
||
#if UNITY_2022_1_OR_NEWER
|
||
ret = _decoder.Init(fileData.GetData<byte>(), beginFrameIndex, syncDecodeFirstFrame, fastDecode);
|
||
#else
|
||
// Unity 2020/2021: 创建持久的数据副本供解码器使用
|
||
if (_hasVideoDataBuffer)
|
||
{
|
||
_videoDataBuffer.Dispose();
|
||
}
|
||
_videoDataBuffer = new NativeArray<byte>(fileData.bytes, Allocator.Persistent);
|
||
_hasVideoDataBuffer = true;
|
||
ret = _decoder.Init(_videoDataBuffer, beginFrameIndex, syncDecodeFirstFrame, fastDecode);
|
||
#endif
|
||
|
||
_frameCount = _decoder.FrameCount;
|
||
_loopBeginFrameIndex = beginFrameIndex;
|
||
_loopEndFrameIndex = _frameCount;
|
||
_duration = _decoder.Duration;
|
||
_frameInterval = _decoder.FrameInterval;
|
||
_codecName = _decoder.CodecName;
|
||
|
||
if (ret != 0)
|
||
{
|
||
LogError($"Init 失败 ret: {ret}");
|
||
}
|
||
|
||
OnUpdateRenderTargetSize();
|
||
|
||
// 仅当初始化且同步首帧解码成功时才立即显示,避免无有效帧时 DisplayFrame 越界
|
||
if (syncDecodeFirstFrame && ret == 0)
|
||
{
|
||
DisplayFrame();
|
||
OnFirstFrameRendered?.Invoke();
|
||
}
|
||
|
||
if (_isMultithreadedDecode)
|
||
{
|
||
// 多线程模式
|
||
Interlocked.Exchange(ref _hasFrameReady, 1); // 第一帧已准备好
|
||
_isPlaying = true;
|
||
_isPause = false;
|
||
_decodeThread = new Thread(DecodeThreadFunction)
|
||
{
|
||
Priority = System.Threading.ThreadPriority.AboveNormal,
|
||
};
|
||
#if UNITY_EDITOR
|
||
_decodeThread.Name = $"{nameof(LeviathanVideoDecoderBase)}.{nameof(DecodeThreadFunction)}";
|
||
#endif
|
||
_decodeThread.Start();
|
||
}
|
||
else
|
||
{
|
||
// 单线程模式
|
||
_isPlaying = true;
|
||
_isPause = false;
|
||
}
|
||
return ret;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 更新渲染目标尺寸(子类实现)
|
||
/// </summary>
|
||
protected abstract void OnUpdateRenderTargetSize();
|
||
|
||
protected void InternalStop()
|
||
{
|
||
if (!_isPlaying) return;
|
||
|
||
// 立即设置状态,防止重复调用
|
||
_isPlaying = false;
|
||
|
||
// 其他平台的同步停止逻辑
|
||
InternalStopSync();
|
||
}
|
||
|
||
// 同步停止方法(非微信平台)
|
||
protected void InternalStopSync()
|
||
{
|
||
// 重置第一帧回调标志,避免旧视频的回调影响新视频
|
||
_pendingFirstFrameCallback = false;
|
||
_pendingPlayCompleted = false;
|
||
_needApplyTexture = false;
|
||
_isPause = false;
|
||
_timelineMultithreadedExternalClock = false;
|
||
_timelineMultithreadedCatchUpMode = false;
|
||
_timelineMultithreadedDeferGpuApply = false;
|
||
_catchUpBudgetUnityFrame = -1;
|
||
_catchUpRemainingDisplayBudget = 0;
|
||
|
||
if (_isMultithreadedDecode)
|
||
{
|
||
if (_decodeThread != null && _decodeThread.IsAlive)
|
||
{
|
||
_decodeThread.Join(1000); // 添加超时避免死锁
|
||
if (_decodeThread.IsAlive)
|
||
{
|
||
// 强制终止线程
|
||
_decodeThread.Abort();
|
||
}
|
||
}
|
||
_decodeThread = null;
|
||
}
|
||
|
||
// 其他平台正常销毁decoder
|
||
if (_decoder != null)
|
||
{
|
||
_decoder.OnFirstFrameDecoded -= OnFirstFrameDecodedCallback;
|
||
_decoder.InternalStop();
|
||
_decoder.Dispose();
|
||
_decoder = null;
|
||
}
|
||
|
||
#if !UNITY_2022_1_OR_NEWER
|
||
// Unity 2020/2021: 释放数据缓冲区
|
||
if (_hasVideoDataBuffer)
|
||
{
|
||
_videoDataBuffer.Dispose();
|
||
_hasVideoDataBuffer = false;
|
||
}
|
||
#endif
|
||
|
||
OnDisableRenderTarget();
|
||
}
|
||
|
||
// 主线程
|
||
protected virtual void Update()
|
||
{
|
||
// 如果正在销毁,立即返回
|
||
if (_isDestroying) return;
|
||
if (!isActiveAndEnabled)
|
||
return;
|
||
|
||
// 处理第一帧回调(从子线程安全地在主线程执行)
|
||
if (_pendingFirstFrameCallback)
|
||
{
|
||
_pendingFirstFrameCallback = false;
|
||
HandleFirstFrameDecoded();
|
||
}
|
||
|
||
if (_pendingPlayCompleted)
|
||
{
|
||
_pendingPlayCompleted = false;
|
||
OnPlayCompleted?.Invoke();
|
||
}
|
||
|
||
if (_isPause) return;
|
||
if (!_isPlaying) return;
|
||
|
||
// CatchUp 模式下让 Update 负责轻量级帧消费,不再 early return
|
||
// ProcessFrame 只处理大 gap 的 Seek,小 gap 由 Update 的 deltaTime 积累自然追赶
|
||
if (_isMultithreadedDecode && _timelineMultithreadedExternalClock)
|
||
return;
|
||
|
||
#if UNITY_WEBGL && !UNITY_EDITOR
|
||
_timeAccumulated += 1.0 / Application.targetFrameRate * _playSpeed;
|
||
#else
|
||
_timeAccumulated += Time.deltaTime * _playSpeed;
|
||
#endif
|
||
if (_isMultithreadedDecode)
|
||
{
|
||
// 多线程模式
|
||
// CatchUp 模式下循环消费多帧,匹配 Timeline 推进速度;Lockstep 模式保持单帧消费
|
||
int maxConsume = _timelineMultithreadedCatchUpMode ? 2 : 1;
|
||
|
||
for (int i = 0; i < maxConsume && _timeAccumulated >= _frameInterval; i++)
|
||
{
|
||
// 检查是否有新帧准备好
|
||
if (Interlocked.Read(ref _hasFrameReady) > 0)
|
||
{
|
||
// 复制帧数据到纹理(不立即Apply)
|
||
DisplayFrame(applyImmediately: false);
|
||
|
||
_timeAccumulated -= _frameInterval;
|
||
|
||
// DisplayFrame 完成后,通知解码线程可以解码下一帧
|
||
Interlocked.Exchange(ref _hasFrameReady, 0);
|
||
}
|
||
else
|
||
{
|
||
// 帧未就绪,停止消费(避免自旋等待)
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (_timeAccumulated > _frameInterval * 3)
|
||
{
|
||
_timeAccumulated = _frameInterval;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// 单线程模式
|
||
if (IsWCGame)
|
||
{
|
||
// Worker解码器:每次Update解码一帧
|
||
if (_decoder.DecodeNextFrame() == 0)
|
||
{
|
||
DisplayFrame(applyImmediately: false);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// 每次Update最多解码一帧,不追帧
|
||
if (_timeAccumulated >= _frameInterval)
|
||
{
|
||
if (_decoder.DecodeNextFrame() == 0)
|
||
{
|
||
_timeAccumulated -= _frameInterval;
|
||
|
||
long currentFrame = _decoder.DecodedFrameIndex;
|
||
if (currentFrame >= _loopEndFrameIndex)
|
||
{
|
||
if (_isLooping)
|
||
{
|
||
SeekToFrame(_loopBeginFrameIndex);
|
||
}
|
||
}
|
||
|
||
DisplayFrame(applyImmediately: false);
|
||
}
|
||
|
||
if (_timeAccumulated > _frameInterval * 3)
|
||
{
|
||
_timeAccumulated = _frameInterval;
|
||
}
|
||
}
|
||
}
|
||
if (_playFrameIndex >= _loopEndFrameIndex && !_isLooping)
|
||
{
|
||
Pause();
|
||
OnPlayCompleted?.Invoke();
|
||
}
|
||
}
|
||
}
|
||
|
||
protected virtual void LateUpdate()
|
||
{
|
||
if (_isDestroying || !isActiveAndEnabled)
|
||
return;
|
||
if (_needApplyTexture) {
|
||
_needApplyTexture = false;
|
||
if (_decoder != null && _textures != null)
|
||
{
|
||
var perfLate = LeviathanLogConfig.EnableTimelineVideoPerfLog && _timelineMultithreadedCatchUpMode;
|
||
var swLate = perfLate ? Stopwatch.StartNew() : null;
|
||
DisplayFrameApply();
|
||
if (swLate != null)
|
||
{
|
||
var ms = swLate.ElapsedMilliseconds;
|
||
if (ms >= 4 || (++_lateApplyPerfSeq % 90) == 0)
|
||
LeviathanLogConfig.Log("TimelineVideoPerf", $"LateApply GPU={ms}ms [{name}]");
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
// 解码线程(仅多线程模式)
|
||
protected void DecodeThreadFunction()
|
||
{
|
||
try
|
||
{
|
||
while (_isPlaying)
|
||
{
|
||
if (!_isPause)
|
||
{
|
||
// 等待主线程显示完当前帧(_hasFrameReady == 0 表示允许解码下一帧)
|
||
var spinHandshake = 0;
|
||
while (Interlocked.Read(ref _hasFrameReady) != 0 && _isPlaying)
|
||
{
|
||
if (spinHandshake++ < 8000)
|
||
Thread.SpinWait(16);
|
||
else
|
||
Thread.Sleep(0);
|
||
}
|
||
if (!_isPlaying) break;
|
||
|
||
// 解码下一帧
|
||
if (_decoder.DecodeNextFrame() == 0)
|
||
{
|
||
// 解码成功,通知主线程帧已准备好
|
||
Interlocked.Exchange(ref _hasFrameReady, 1);
|
||
}
|
||
|
||
if (_playFrameIndex >= _loopEndFrameIndex && !_isLoopSeeking)
|
||
{
|
||
if (_isLooping)
|
||
{
|
||
_isLoopSeeking = true;
|
||
SeekToFrame(_loopBeginFrameIndex);
|
||
// Seek后继续解码,_hasFrameReady 保持为 1,等待主线程显示
|
||
}
|
||
else
|
||
{
|
||
_isPause = true;
|
||
Interlocked.Exchange(ref _hasFrameReady, 1);
|
||
_pendingPlayCompleted = true;
|
||
}
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// 暂停时不宜 Sleep 过大:Timeline 每帧 Pause/Resume,5ms 会叠加明显卡顿;1ms 在长时间暂停时仍可接受
|
||
Thread.Sleep(1);
|
||
}
|
||
}
|
||
}
|
||
catch (ThreadAbortException)
|
||
{
|
||
Thread.ResetAbort();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 编辑器里(含 Timeline 预览、GUI 事件里求值)可能仍判定为“非运行态销毁”,
|
||
/// 仅用 Application.isPlaying 不可靠;编辑器内对运行时创建的纹理统一 DestroyImmediate。
|
||
/// </summary>
|
||
private static void DestroyOwnedUnityObject(UnityEngine.Object obj)
|
||
{
|
||
if (obj == null)
|
||
return;
|
||
#if UNITY_EDITOR
|
||
UnityEngine.Object.DestroyImmediate(obj);
|
||
#else
|
||
UnityEngine.Object.Destroy(obj);
|
||
#endif
|
||
}
|
||
|
||
// 初始化纹理
|
||
protected virtual void InitTextures()
|
||
{
|
||
if (_textures != null)
|
||
{
|
||
for (int i = 0; i < _textures.Length && i < TextureCount; i++)
|
||
{
|
||
if (_textures[i] != null)
|
||
DestroyOwnedUnityObject(_textures[i]);
|
||
}
|
||
}
|
||
|
||
var videoFrame = _decoder.VideoFrame;
|
||
// 软件解码器使用YUV格式
|
||
_textures = new Texture2D[TextureCount];
|
||
_textures[0] = new Texture2D(videoFrame->linesize[0], videoFrame->height, TextureFormat.Alpha8, false);
|
||
_textures[1] = new Texture2D(videoFrame->linesize[1], videoFrame->height / 2, TextureFormat.Alpha8, false);
|
||
_textures[2] = new Texture2D(videoFrame->linesize[2], videoFrame->height / 2, TextureFormat.Alpha8, false);
|
||
_textures[0].wrapMode = TextureWrapMode.Clamp;
|
||
_textures[1].wrapMode = TextureWrapMode.Clamp;
|
||
_textures[2].wrapMode = TextureWrapMode.Clamp;
|
||
|
||
_videoValidWidth = videoFrame->width;
|
||
_videoWidth = videoFrame->linesize[0];
|
||
_videoHeight = videoFrame->height;
|
||
|
||
OnInitMaterial();
|
||
OnUpdateRenderTargetSize();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 初始化材质(子类实现)
|
||
/// </summary>
|
||
protected abstract void OnInitMaterial();
|
||
|
||
/// <summary>
|
||
/// 更新纹理
|
||
/// </summary>
|
||
/// <param name="applyImmediately">是否立即Apply上传到GPU。false时只复制数据,可以稍后调用DisplayFrameApply</param>
|
||
protected virtual void DisplayFrame(bool applyImmediately = true)
|
||
{
|
||
if (_decoder == null)
|
||
return;
|
||
var videoFrame = _decoder.VideoFrame;
|
||
if (videoFrame == null)
|
||
return;
|
||
if (videoFrame->linesize[0] <= 0)
|
||
return;
|
||
|
||
bool texturesOk = _textures != null && _textures.Length >= TextureCount && _textures[0] != null;
|
||
// 检查纹理是否需要重新创建
|
||
if (!texturesOk || _textures[0].width != videoFrame->linesize[0] || _textures[0].height != videoFrame->height)
|
||
{
|
||
InitTextures();
|
||
_lastAppliedAlphaType = _alphaType;
|
||
_needInitTexture = false;
|
||
|
||
if (_decoder is LeviathanWorkerDecoder workerDecoder)
|
||
{
|
||
workerDecoder.SetTextureIds(_textures);
|
||
}
|
||
}
|
||
else if (_needInitTexture)
|
||
{
|
||
// 只在alphaType变化时才更新材质
|
||
if (_alphaType != _lastAppliedAlphaType)
|
||
{
|
||
OnInitMaterial();
|
||
_lastAppliedAlphaType = _alphaType;
|
||
}
|
||
// 如果是Worker版本,将纹理ID传递给JSLIB
|
||
if (_decoder is LeviathanWorkerDecoder workerDecoder)
|
||
{
|
||
workerDecoder.SetTextureIds(_textures);
|
||
}
|
||
_needInitTexture = false;
|
||
}
|
||
|
||
// 复制帧数据到纹理(需要保护_videoFrame)
|
||
_decoder.CopyFrameDataToTextures(_textures);
|
||
|
||
// 根据参数决定是否立即Apply
|
||
if (applyImmediately)
|
||
{
|
||
_decoder.ApplyTextures(_textures);
|
||
_needApplyTexture = false; // 立即Apply后,清除延迟标志
|
||
OnTexturesUploadedToGpu();
|
||
}
|
||
else
|
||
{
|
||
_needApplyTexture = true; // 延迟Apply,设置标志在LateUpdate中执行
|
||
}
|
||
|
||
// 更新播放帧索引
|
||
if (_isMultithreadedDecode)
|
||
{
|
||
_playFrameIndex = Interlocked.Read(ref _decoder.GetDecodedFrameIndexRef());
|
||
}
|
||
else
|
||
{
|
||
_playFrameIndex = _decoder.DecodedFrameIndex;
|
||
}
|
||
|
||
// 重置循环seek标志
|
||
_isLoopSeeking = false;
|
||
}
|
||
|
||
/// <summary>
|
||
/// <see cref="DisplayFrame"/> / <see cref="DisplayFrameApply"/> 在 <see cref="ILeviathanDecoder.ApplyTextures"/> 之后调用。
|
||
/// YUV UI 在编辑器 Timeline 等路径下,仅主纹理可能触发 Canvas 刷新,U/V 已 Apply 但画面色度仍卡首帧时可在此重绑并标脏。
|
||
/// </summary>
|
||
protected virtual void OnTexturesUploadedToGpu()
|
||
{
|
||
}
|
||
|
||
/// <summary>
|
||
/// 将纹理数据上传到GPU(可与下一帧解码并行)
|
||
/// 配合 DisplayFrame(false) 使用,实现分步显示优化
|
||
/// </summary>
|
||
protected virtual void DisplayFrameApply()
|
||
{
|
||
_decoder.ApplyTextures(_textures);
|
||
OnTexturesUploadedToGpu();
|
||
}
|
||
|
||
// 跳转到指定时间点
|
||
public void Seek(double ms)
|
||
{
|
||
_decoder.Seek(ms);
|
||
}
|
||
|
||
// 跳转到指定帧
|
||
public void SeekToFrame(long frame)
|
||
{
|
||
_decoder.SeekToFrame(frame);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Timeline 等外部时间源:Seek 到指定帧并立即解码显示一帧(不依赖 Update 推进)。
|
||
/// 仅支持非多线程解码;与 Timeline 同轨时请关闭多线程解码。
|
||
/// </summary>
|
||
/// <param name="applyImmediately">false 时仅写入纹理 CPU 侧,需再调 <see cref="FlushDeferredTimelineDisplay"/>(或等 <see cref="LateUpdate"/>)。</param>
|
||
public void ScrubToFrameAndDisplay(long frameIndex, bool applyImmediately = true)
|
||
{
|
||
if (_isDestroying || _decoder == null || !_isPlaying)
|
||
return;
|
||
if (_isMultithreadedDecode)
|
||
{
|
||
LogWarning("ScrubToFrameAndDisplay: 多线程解码下无法安全擦洗,请关闭多线程解码");
|
||
return;
|
||
}
|
||
|
||
if (frameIndex < 1)
|
||
frameIndex = 1;
|
||
else if (_frameCount > 0 && frameIndex > _frameCount)
|
||
frameIndex = _frameCount;
|
||
|
||
SeekToFrame(frameIndex);
|
||
if (_decoder.DecodeNextFrame() == 0)
|
||
{
|
||
// avformat_seek_file 只能定位到关键帧,如果解码出的帧早于目标帧则继续解码
|
||
int safety = 300;
|
||
while (_decoder.DecodedFrameIndex < frameIndex && safety-- > 0)
|
||
{
|
||
if (_decoder.DecodeNextFrame() != 0)
|
||
break;
|
||
}
|
||
DisplayFrame(applyImmediately);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Timeline 顺序播放:不 Seek,仅解码下一帧并显示。与 ScrubToFrameAndDisplay 配合可降低每帧 Seek 的开销。
|
||
/// </summary>
|
||
/// <param name="applyImmediately">false 时须调用 <see cref="FlushDeferredTimelineDisplay"/> 完成 GPU 上传。</param>
|
||
public bool TryAdvanceOneFrameForTimeline(bool applyImmediately = true)
|
||
{
|
||
if (_isDestroying || _decoder == null || !_isPlaying)
|
||
return false;
|
||
if (_isMultithreadedDecode)
|
||
return false;
|
||
if (_decoder.DecodeNextFrame() != 0)
|
||
return false;
|
||
DisplayFrame(applyImmediately);
|
||
return true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 将 Timeline 路径下延迟的纹理 Apply 立刻提交(与 <see cref="LateUpdate"/> 中逻辑一致)。
|
||
/// </summary>
|
||
public void FlushDeferredTimelineDisplay()
|
||
{
|
||
if (!_needApplyTexture || _decoder == null || _textures == null)
|
||
return;
|
||
_needApplyTexture = false;
|
||
DisplayFrameApply();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 多线程 + Timeline:把显示对齐到目标帧。顺序前进时尽量用子线程解码,并在成功后<strong>不</strong>立刻 Pause,
|
||
/// 以便在两拍 ProcessFrame 之间预解下一帧;同一 Timeline 帧重复求值时由 Behaviour 侧 Pause 刹停。
|
||
/// </summary>
|
||
public bool SyncMultithreadedToTimelineFrame(long targetFrame)
|
||
{
|
||
if (!_isMultithreadedDecode || !_isPlaying || _decoder == null || _isDestroying)
|
||
return false;
|
||
|
||
if (targetFrame < 1)
|
||
targetFrame = 1;
|
||
else if (_frameCount > 0 && targetFrame > _frameCount)
|
||
targetFrame = _frameCount;
|
||
|
||
var perfOn = LeviathanLogConfig.EnableTimelineVideoPerfLog;
|
||
var sw = perfOn ? Stopwatch.StartNew() : null;
|
||
var curBefore = GetTimelineSyncReferenceFrame();
|
||
string perfNote = null;
|
||
var result = false;
|
||
try
|
||
{
|
||
long cur = curBefore;
|
||
if (cur == targetFrame)
|
||
{
|
||
Pause();
|
||
result = true;
|
||
return result;
|
||
}
|
||
|
||
Pause();
|
||
DrainMultithreadedFrameHandshake();
|
||
|
||
long curAfterDrain = GetTimelineSyncReferenceFrame();
|
||
// 子线程已在上一拍预解好本帧,Drain 已显示并对齐
|
||
if (curAfterDrain == targetFrame)
|
||
{
|
||
Resume();
|
||
result = true;
|
||
return result;
|
||
}
|
||
|
||
if (targetFrame == curAfterDrain + 1)
|
||
{
|
||
Resume();
|
||
Thread.Sleep(0);
|
||
if (!WaitMultithreadedFrameReady(_timelineMultithreadedWaitMs))
|
||
{
|
||
Pause();
|
||
DrainMultithreadedFrameHandshake();
|
||
perfNote = "WaitTimeoutSeek";
|
||
result = MainThreadSeekDecodeAndDisplayForTimeline(targetFrame);
|
||
return result;
|
||
}
|
||
|
||
DisplayFrame(applyImmediately: MultithreadedTimelineApplyTexturesImmediately);
|
||
Interlocked.Exchange(ref _hasFrameReady, 0);
|
||
// 不 Pause:解码线程可立即开始预解 target+1,主线程下一拍常可零等待或仅 Drain
|
||
result = true;
|
||
return result;
|
||
}
|
||
|
||
perfNote = "Seek";
|
||
result = MainThreadSeekDecodeAndDisplayForTimeline(targetFrame);
|
||
return result;
|
||
}
|
||
finally
|
||
{
|
||
if (sw != null)
|
||
LogTimelineVideoPerf("Lockstep", targetFrame, curBefore, GetTimelineSyncReferenceFrame(), 0, sw.ElapsedMilliseconds,
|
||
result, perfNote);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 多线程 + Timeline「CatchUp」模式:入口<strong>不</strong> <see cref="Pause"/> 子线程,仅靠消费 <c>_hasFrameReady</c> 追上目标帧。
|
||
/// 与 Match 待机一致:<strong>帧未就绪时不阻塞主线程</strong>(仅极短自旋),避免 <see cref="WaitMultithreadedFrameReady"/> 把帧率锁死在解码速度上。
|
||
/// 若本拍未追上目标帧,返回 false,由 Behaviour 保持 <c>_lastScrubFrame</c> 待下拍再追。回跳或正向落后 ≥20 帧走主线程 Seek,避免多拍逐步追。
|
||
/// 同一 Unity 帧内共享 Display 预算,抑制 60Hz ProcessFrame 对 24fps 视频的多遍求值尖峰。
|
||
/// </summary>
|
||
/// <returns>当前显示是否已对齐 <paramref name="targetFrame"/>。</returns>
|
||
public bool TimelineCatchUpPresentToFrame(long targetFrame)
|
||
{
|
||
if (!_isMultithreadedDecode || !_timelineMultithreadedCatchUpMode || !_isPlaying || _decoder == null || _isDestroying)
|
||
return false;
|
||
|
||
if (targetFrame < 1)
|
||
targetFrame = 1;
|
||
else if (_frameCount > 0 && targetFrame > _frameCount)
|
||
targetFrame = _frameCount;
|
||
|
||
var unityFrame = Time.frameCount;
|
||
if (unityFrame != _catchUpBudgetUnityFrame)
|
||
{
|
||
_catchUpBudgetUnityFrame = unityFrame;
|
||
_catchUpRemainingDisplayBudget = _timelineCatchUpUnityFrameDisplayBudget;
|
||
}
|
||
|
||
var perfOn = LeviathanLogConfig.EnableTimelineVideoPerfLog;
|
||
var sw = perfOn ? Stopwatch.StartNew() : null;
|
||
var curBefore = GetTimelineSyncReferenceFrame();
|
||
var consumes = 0;
|
||
string perfNote = null;
|
||
var result = false;
|
||
try
|
||
{
|
||
long cur = curBefore;
|
||
if (cur == targetFrame)
|
||
{
|
||
// 不 Pause:解码线程继续预解 target+1,下拍 ProcessFrame 可直接消费(与 Lockstep 一致)
|
||
result = true;
|
||
return result;
|
||
}
|
||
|
||
long gap = targetFrame - cur;
|
||
// 顺序落后较大时逐步消费要跨多拍 ProcessFrame,易长时间 ok=False;中等缺口直接 Seek 往往更少主线程空转。
|
||
// 负向/回跳仍走 Seek;极大缺口原先即 Seek(与 gap>=阈值合并为一条分支)。
|
||
const int catchUpSeekIfForwardGapAtLeast = 20;
|
||
if (gap < 0 || gap >= catchUpSeekIfForwardGapAtLeast)
|
||
{
|
||
Pause();
|
||
perfNote = "Seek";
|
||
result = MainThreadSeekDecodeAndDisplayForTimeline(targetFrame);
|
||
return result;
|
||
}
|
||
|
||
Resume();
|
||
|
||
var maxPerCall = _timelineCatchUpMaxConsumePerCall;
|
||
if (gap <= 2)
|
||
maxPerCall = Mathf.Min(2, maxPerCall); // 允许消费 2 帧,减少追赶拍数
|
||
var stepCap = Mathf.Min(maxPerCall, _catchUpRemainingDisplayBudget);
|
||
var steps = 0;
|
||
while (GetTimelineSyncReferenceFrame() < targetFrame && steps < stepCap)
|
||
{
|
||
if (Interlocked.Read(ref _hasFrameReady) <= 0)
|
||
{
|
||
// 先短自旋,再短阻塞等待:让解码线程有时间产出下一帧
|
||
if (!TrySpinUntilMultithreadedFrameReady(CatchUpFrameReadyLightSpins, 4))
|
||
{
|
||
if (!WaitMultithreadedFrameReady(_catchUpConsumePerFrameWaitMs))
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (Interlocked.Read(ref _hasFrameReady) <= 0)
|
||
break;
|
||
|
||
DisplayFrame(applyImmediately: MultithreadedTimelineApplyTexturesImmediately);
|
||
Interlocked.Exchange(ref _hasFrameReady, 0);
|
||
steps++;
|
||
_catchUpRemainingDisplayBudget = Mathf.Max(0, _catchUpRemainingDisplayBudget - 1);
|
||
}
|
||
|
||
consumes = steps;
|
||
result = GetTimelineSyncReferenceFrame() == targetFrame;
|
||
return result;
|
||
}
|
||
finally
|
||
{
|
||
if (sw != null)
|
||
LogTimelineVideoPerf("CatchUp", targetFrame, curBefore, GetTimelineSyncReferenceFrame(), consumes, sw.ElapsedMilliseconds,
|
||
result, perfNote);
|
||
}
|
||
}
|
||
|
||
/// <summary>CatchUp consume 循环中等待单帧就绪的毫秒上限;过短追不上 Timeline 帧号,过长占主线程。</summary>
|
||
int _catchUpConsumePerFrameWaitMs = 20;
|
||
|
||
public int CatchUpConsumePerFrameWaitMs
|
||
{
|
||
get => _catchUpConsumePerFrameWaitMs;
|
||
set => _catchUpConsumePerFrameWaitMs = Mathf.Clamp(value, 1, 50);
|
||
}
|
||
|
||
/// <summary>CatchUp:仅自旋、不 Sleep;上限略收紧以降低主线程在「帧迟迟未就绪」时的尖峰占用。</summary>
|
||
private const int CatchUpFrameReadyLightSpins = 1024;
|
||
|
||
private bool TrySpinUntilMultithreadedFrameReady(int maxSpins, int spinWaitIterations)
|
||
{
|
||
for (int i = 0; i < maxSpins; i++)
|
||
{
|
||
if (Interlocked.Read(ref _hasFrameReady) > 0)
|
||
return true;
|
||
Thread.SpinWait(spinWaitIterations);
|
||
}
|
||
|
||
return Interlocked.Read(ref _hasFrameReady) > 0;
|
||
}
|
||
|
||
/// <summary>Timeline 多线程:保持暂停,避免子线程抢解;无新视频帧时不要唤醒解码线程。</summary>
|
||
public void PauseTimelineMultithreadedHold()
|
||
{
|
||
if (!_isMultithreadedDecode || !_isPlaying)
|
||
return;
|
||
Pause();
|
||
}
|
||
|
||
private long GetTimelineSyncReferenceFrame()
|
||
{
|
||
if (_decoder == null)
|
||
return 1;
|
||
if (_playFrameIndex >= 1)
|
||
return _playFrameIndex;
|
||
long d = _decoder.DecodedFrameIndex;
|
||
return d >= 1 ? d : 1;
|
||
}
|
||
|
||
private void DrainMultithreadedFrameHandshake()
|
||
{
|
||
if (Interlocked.Read(ref _hasFrameReady) <= 0)
|
||
return;
|
||
DisplayFrame(applyImmediately: MultithreadedTimelineApplyTexturesImmediately);
|
||
Interlocked.Exchange(ref _hasFrameReady, 0);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 等待解码线程将 <c>_hasFrameReady</c> 置位。按<strong>真实经过时间</strong>截断。
|
||
/// Timeline 在 <see cref="ProcessFrame"/> 中调用:主线程不宜长时间 SpinWait(会拖垮帧率),短自旋后以 <see cref="Thread.Sleep(int)"/> 让步。
|
||
/// </summary>
|
||
private bool WaitMultithreadedFrameReady(int timeoutMs)
|
||
{
|
||
if (Interlocked.Read(ref _hasFrameReady) > 0)
|
||
return true;
|
||
|
||
int budget = Mathf.Clamp(timeoutMs, 1, 500);
|
||
var sw = Stopwatch.StartNew();
|
||
|
||
// 极短自旋仅覆盖「解码已结束、标志即将可见」的微窗口,避免主线程空转数毫秒
|
||
const int maxSpins = 2048;
|
||
for (int s = 0; s < maxSpins && sw.ElapsedMilliseconds < budget; s++)
|
||
{
|
||
if (Interlocked.Read(ref _hasFrameReady) > 0)
|
||
return true;
|
||
Thread.SpinWait(8);
|
||
}
|
||
|
||
while (sw.ElapsedMilliseconds < budget)
|
||
{
|
||
if (Interlocked.Read(ref _hasFrameReady) > 0)
|
||
return true;
|
||
Thread.Sleep(0);
|
||
}
|
||
|
||
return Interlocked.Read(ref _hasFrameReady) > 0;
|
||
}
|
||
|
||
private bool MainThreadSeekDecodeAndDisplayForTimeline(long frameIndex)
|
||
{
|
||
SeekToFrame(frameIndex);
|
||
if (_decoder.DecodeNextFrame() != 0)
|
||
{
|
||
Pause();
|
||
return false;
|
||
}
|
||
|
||
int safety = 300;
|
||
while (_decoder.DecodedFrameIndex < frameIndex && safety-- > 0)
|
||
{
|
||
if (_decoder.DecodeNextFrame() != 0)
|
||
break;
|
||
}
|
||
|
||
DisplayFrame(applyImmediately: MultithreadedTimelineApplyTexturesImmediately);
|
||
Interlocked.Exchange(ref _hasFrameReady, 0);
|
||
Resume();
|
||
|
||
// 重置时间积累,防止 Seek 后 Update 路径重复消费帧
|
||
_timeAccumulated = 0;
|
||
return true;
|
||
}
|
||
|
||
public void SetPlaySpeed(float speed)
|
||
{
|
||
_playSpeed = speed;
|
||
}
|
||
|
||
public virtual void SetAlphaType(PlayAlphaType alphaTypeValue)
|
||
{
|
||
_alphaType = alphaTypeValue;
|
||
if (_textures == null || _textures.Length <= 0) return;
|
||
OnUpdateMaterial();
|
||
_lastAppliedAlphaType = _alphaType;
|
||
OnUpdateRenderTargetSize();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 更新材质(子类实现)
|
||
/// </summary>
|
||
protected abstract void OnUpdateMaterial();
|
||
|
||
public void SetLooping(bool isLooping)
|
||
{
|
||
_isLooping = isLooping;
|
||
|
||
// 如果是微信小游戏平台,设置解码器的循环播放状态
|
||
if (IsWCGame)
|
||
{
|
||
_decoder.IsLooping = isLooping;
|
||
}
|
||
|
||
if (_isLooping && _isPause)
|
||
{
|
||
Resume();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 设置循环播放区间, 帧数范围为 1 ~ FrameCount
|
||
/// </summary>
|
||
/// <param name="beginFrame">开始帧索引</param>
|
||
/// <param name="endFrame">结束帧索引</param>
|
||
public void SetLoopingFrame(long beginFrame, long endFrame)
|
||
{
|
||
_loopBeginFrameIndex = beginFrame;
|
||
_loopEndFrameIndex = endFrame;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 切换视频
|
||
/// </summary>
|
||
/// <param name="fileData">视频文件</param>
|
||
/// <param name="playAlpha">播放透明度</param>
|
||
/// <param name="multithreadedDecode">是否多线程解码</param>
|
||
/// <param name="beginFrameIndex">开始播放的帧索引</param>
|
||
/// <param name="syncDecodeFirstFrame">是否同步解码第一帧, 区别在于是否当前帧就能立马看到视频, 会耗时10ms</param>
|
||
/// <param name="fastDecode">是否启用快速解码(跳过环路滤波器),默认开启以提升性能</param>
|
||
/// <returns>返回值: true表示成功, false表示失败</returns>
|
||
public bool ChangeVideo(TextAsset fileData, PlayAlphaType playAlpha, bool multithreadedDecode, int beginFrameIndex = 1, bool syncDecodeFirstFrame = true, bool fastDecode = true)
|
||
{
|
||
// 如果不同步解码第一帧,先隐藏渲染目标避免显示旧帧
|
||
if (!syncDecodeFirstFrame)
|
||
{
|
||
OnDisableRenderTarget();
|
||
}
|
||
return ChangeVideoSync(fileData, playAlpha, multithreadedDecode, beginFrameIndex, syncDecodeFirstFrame, fastDecode);
|
||
}
|
||
|
||
protected bool ChangeVideoSync(TextAsset fileData, PlayAlphaType playAlpha, bool multithreadedDecode, int beginFrameIndex = 1, bool syncDecodeFirstFrame = true, bool fastDecode = true)
|
||
{
|
||
Log("开始同步切换视频");
|
||
|
||
try
|
||
{
|
||
// 同步停止当前视频
|
||
StopVideoSync();
|
||
|
||
// 同步播放新视频
|
||
int result = PlayVideoSync(fileData, playAlpha, multithreadedDecode, beginFrameIndex, syncDecodeFirstFrame, fastDecode);
|
||
|
||
if (result != 0)
|
||
{
|
||
LogError($"同步播放视频失败,错误码: {result}");
|
||
return false;
|
||
}
|
||
|
||
Log("同步切换视频完成");
|
||
return true;
|
||
}
|
||
catch (System.Exception e)
|
||
{
|
||
LogError($"同步切换视频异常: {e.Message}");
|
||
return false;
|
||
}
|
||
}
|
||
|
||
protected void StopVideoSync()
|
||
{
|
||
// 立即禁用渲染目标显示,避免显示旧帧
|
||
OnDisableRenderTarget();
|
||
|
||
if (_isPlaying)
|
||
{
|
||
if (_isPlaying)
|
||
{
|
||
InternalStop();
|
||
}
|
||
|
||
// 同步等待停止操作完成
|
||
int maxWaitCount = 1000; // 最多等待1000次循环
|
||
int waitCount = 0;
|
||
|
||
while ((_isPlaying) && waitCount < maxWaitCount)
|
||
{
|
||
// 在同步模式下,我们不能使用yield,所以使用Thread.Sleep进行短暂等待
|
||
Thread.Sleep(1);
|
||
waitCount++;
|
||
}
|
||
|
||
if (waitCount >= maxWaitCount)
|
||
{
|
||
LogWarning("同步停止视频超时,继续执行后续操作");
|
||
}
|
||
else
|
||
{
|
||
Log($"视频同步停止完成,等待了 {waitCount} 次循环");
|
||
}
|
||
}
|
||
}
|
||
|
||
protected int PlayVideoSync(TextAsset fileData, PlayAlphaType playAlpha, bool multithreadedDecode, int beginFrameIndex = 1, bool syncDecodeFirstFrame = true, bool fastDecode = true)
|
||
{
|
||
// 播放新视频
|
||
int result;
|
||
if (fileData != null)
|
||
{
|
||
// 使用文件数据播放
|
||
result = PlayVideo(fileData, playAlpha, multithreadedDecode, beginFrameIndex, syncDecodeFirstFrame, fastDecode);
|
||
Log($"同步切换到视频资源: {fileData.name}");
|
||
}
|
||
else
|
||
{
|
||
LogError("没有提供有效的视频源");
|
||
return -1;
|
||
}
|
||
|
||
if (result != 0)
|
||
{
|
||
LogError($"同步播放视频失败,错误码: {result}");
|
||
return result;
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
internal void Play()
|
||
{
|
||
if (!Application.isPlaying) return;
|
||
if (bytesFile == null) return;
|
||
PlayVideo(bytesFile, _alphaType, true);
|
||
}
|
||
|
||
public void Stop()
|
||
{
|
||
if (!Application.isPlaying) return;
|
||
InternalStop();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Timeline 专用:强制停止解码器,不受 Application.isPlaying 限制。
|
||
/// 用于编辑器预览结束时清理状态。
|
||
/// </summary>
|
||
public void ForceStopForTimeline()
|
||
{
|
||
InternalStop();
|
||
OnDisableRenderTarget();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 轻量解析 bytes 视频元信息(<c>syncDecodeFirstFrame: false</c>,不解码到纹理),运行态与编辑器均可用。
|
||
/// </summary>
|
||
public static bool TryProbeVideoMetaFromBytes(TextAsset file, out double durationSeconds, out long frameCount,
|
||
out double frameInterval)
|
||
{
|
||
durationSeconds = 0;
|
||
frameCount = 0;
|
||
frameInterval = 0;
|
||
if (file == null)
|
||
return false;
|
||
|
||
var dec = new LeviathanSoftwareDecoder();
|
||
#if !UNITY_2022_1_OR_NEWER
|
||
var buffer = new NativeArray<byte>(file.bytes, Allocator.Temp);
|
||
#endif
|
||
try
|
||
{
|
||
int ret;
|
||
#if UNITY_2022_1_OR_NEWER
|
||
ret = dec.Init(file.GetData<byte>(), 1, syncDecodeFirstFrame: false, fastDecode: true);
|
||
#else
|
||
ret = dec.Init(buffer, 1, syncDecodeFirstFrame: false, fastDecode: true);
|
||
#endif
|
||
if (ret != 0)
|
||
return false;
|
||
|
||
durationSeconds = dec.Duration / 1_000_000.0;
|
||
frameCount = dec.FrameCount;
|
||
frameInterval = dec.FrameInterval;
|
||
if (durationSeconds <= 0 && frameCount > 0 && frameInterval > 0)
|
||
durationSeconds = frameCount * frameInterval;
|
||
return durationSeconds > 0.0001 || frameCount > 0;
|
||
}
|
||
finally
|
||
{
|
||
dec.Dispose();
|
||
#if !UNITY_2022_1_OR_NEWER
|
||
if (buffer.IsCreated)
|
||
buffer.Dispose();
|
||
#endif
|
||
}
|
||
}
|
||
|
||
#if UNITY_EDITOR
|
||
/// <summary>
|
||
/// 仅编辑器:解析 bytes 中的时长与帧信息,用于 Timeline 新建 Clip 时对齐长度。
|
||
/// </summary>
|
||
public static bool TryEditorProbeVideoMetaFromBytes(TextAsset file, out double durationSeconds, out long frameCount,
|
||
out double frameInterval)
|
||
{
|
||
return TryProbeVideoMetaFromBytes(file, out durationSeconds, out frameCount, out frameInterval);
|
||
}
|
||
#endif
|
||
|
||
/// <summary>编辑器 Timeline 多线程预览时也需要停/启解码线程,否则 Pause 被跳过会导致与主线程抢解。</summary>
|
||
private bool CanApplyPlaybackPauseResume()
|
||
{
|
||
if (Application.isPlaying)
|
||
return true;
|
||
#if UNITY_EDITOR
|
||
return _isMultithreadedDecode && (_timelineMultithreadedExternalClock || _timelineMultithreadedCatchUpMode);
|
||
#else
|
||
return false;
|
||
#endif
|
||
}
|
||
|
||
public void Pause()
|
||
{
|
||
if (!CanApplyPlaybackPauseResume())
|
||
return;
|
||
_isPause = true;
|
||
|
||
// 调用解码器的暂停方法
|
||
_decoder?.Pause(true);
|
||
}
|
||
|
||
#if UNITY_EDITOR
|
||
/// <summary>
|
||
/// 编辑器下暂停底层解码器(不依赖 Play 模式),用于首帧预览等;避免外部反射访问 <c>_decoder</c>。
|
||
/// </summary>
|
||
public void EditorPauseDecoderIgnoringPlayMode()
|
||
{
|
||
_isPause = true;
|
||
_decoder?.Pause(true);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 编辑器下恢复底层解码器(不依赖 Play 模式),与 <see cref="EditorPauseDecoderIgnoringPlayMode"/> / <see cref="OnEnable"/> 配合。
|
||
/// </summary>
|
||
public void EditorResumeDecoderIgnoringPlayMode()
|
||
{
|
||
_isPause = false;
|
||
_decoder?.Pause(false);
|
||
}
|
||
#endif
|
||
|
||
/// <summary>
|
||
/// 恢复播放(运行态下同步恢复解码器)。
|
||
/// </summary>
|
||
public void Resume()
|
||
{
|
||
if (!CanApplyPlaybackPauseResume())
|
||
return;
|
||
_isPause = false;
|
||
|
||
// 调用解码器的恢复方法
|
||
_decoder?.Pause(false);
|
||
}
|
||
}
|
||
|