/*---------------------------------------------------------------- // 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 /// /// 视频解码器抽象基类 /// 包含解码逻辑、播放控制、多线程解码等公共功能 /// 子类需要实现具体的渲染逻辑 /// 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); } /// /// 静态构造函数 - 在类首次使用时执行平台检测 /// 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; /// /// 获取当前视频帧指针(子类使用) /// protected LeviathanVideo.Abstractions.AVFrame* VideoFrame => _decoder != null ? _decoder.VideoFrame : null; #if !UNITY_2022_1_OR_NEWER // Unity 2020/2021 兼容:保存数据副本以供解码器使用 protected NativeArray _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; /// 当前实例是否正在使用多线程解码(WebGL 等平台上请求多线程也会被降级为 false)。 /// public bool IsMultithreadedDecodeActive => _isMultithreadedDecode; /// /// 为 true 时由 Timeline 的 ProcessFrame 驱动显示节拍, 不再按 Time.deltaTime 消费 _hasFrameReady, /// 避免与 Playable 时间轴脱节;须配合 使用。 /// protected bool _timelineMultithreadedExternalClock; public bool TimelineMultithreadedExternalClockActive => _timelineMultithreadedExternalClock; /// 由 Timeline 在起播前设置;停止时 会清零。 public void SetTimelineMultithreadedExternalClock(bool active) { _timelineMultithreadedExternalClock = active; } /// /// 与 二选一(多线程时):为 true 时不走外接时钟, /// 子线程按握手持续解码,ProcessFrame 仅通过 消费 _hasFrameReady 追上 Timeline 帧号,接近 UI 待机多线程路径。 /// 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; /// /// 解码器是否处于可用状态(isPlaying 且内部解码器实例存在) /// public bool IsDecoderValid => _isPlaying && _decoder != null; protected volatile bool _isPause; public bool IsPause => _isPause; // 销毁状态标志(防止销毁期间的异步操作继续执行) protected bool _isDestroying = false; // 第一帧回调待执行标志(用于从子线程安全地在主线程执行回调) protected volatile bool _pendingFirstFrameCallback = false; /// 解码线程在播完时置位,由 在主线程触发 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; /// 由 Timeline 的 LeviathanVideoBehaviour 在 Prepare 时从片段写入;非 Timeline 播片使用默认值即可。 private int _timelineMultithreadedWaitMs = 64; /// 运行态 Timeline 多线程:为 true 时 内只写 CPU 纹理,结束后再 ,对齐 MatchGame 等非 Timeline 路径「单次 Apply」。 private bool _timelineMultithreadedDeferGpuApply; /// 由 Timeline 的 LeviathanVideoBehaviour 在运行态包在 Sync 外;勿长期置 true。 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; /// Timeline 多线程同步时 的毫秒上限(钳制 8~500)。 public int TimelineMultithreadedWaitMs { get => _timelineMultithreadedWaitMs; set => _timelineMultithreadedWaitMs = Mathf.Clamp(value, 8, 500); } /// Timeline CatchUp:单次 内最多消费几帧就绪(与整帧预算取 min)。 int _timelineCatchUpMaxConsumePerCall = 8; /// 同一 Unity 帧内所有 CatchUp 调用合计的 DisplayFrame 上限;新帧开始时重置。 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); } /// 播放速度倍率(读写均会钳制在合理范围)。 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; /// /// 播放完成回调事件(非循环播放时触发) /// public System.Action OnPlayCompleted; /// /// 首帧渲染完毕回调事件 /// 无论同步或异步解码首帧模式都会触发 /// public System.Action OnFirstFrameRendered; [SerializeField] protected PlayAlphaType _alphaType = PlayAlphaType.None; // 上次应用的alphaType,用于检测是否需要更新材质 protected PlayAlphaType _lastAppliedAlphaType = PlayAlphaType.None; protected virtual void Start() { if (autoPlayOnStart && bytesFile) Play(); } /// /// 与 成对:重新激活后继续解码(含 Timeline / 外部在隐藏时调用过 的情况)。 /// protected virtual void OnEnable() { if (!_isPlaying || !_isPause) return; #if UNITY_EDITOR if (!Application.isPlaying) EditorResumeDecoderIgnoringPlayMode(); else #endif Resume(); } /// /// 未激活或组件禁用时暂停,避免隐藏物体继续耗 CPU(尤其多线程解码);见 恢复。 /// protected virtual void OnDisable() { if (!_isPlaying) return; #if UNITY_EDITOR if (!Application.isPlaying) { EditorPauseDecoderIgnoringPlayMode(); return; } #endif Pause(); } /// /// 第一帧解码完成回调(由解码器触发,可能在子线程中调用) /// 设置标志位,实际处理在主线程的 Update 中执行 /// protected virtual void OnFirstFrameDecodedCallback() { _pendingFirstFrameCallback = true; } /// /// 处理第一帧解码完成的实际逻辑(必须在主线程调用) /// 子类需要重写此方法来处理渲染目标的显示 /// protected virtual void HandleFirstFrameDecoded() { // 先更新纹理,确保显示的是新视频的第一帧 DisplayFrame(); // 子类负责启用渲染目标 OnEnableRenderTarget(); // 触发首帧渲染完毕回调 OnFirstFrameRendered?.Invoke(); } /// /// 启用渲染目标(子类实现) /// protected abstract void OnEnableRenderTarget(); /// /// 禁用渲染目标(子类实现) /// 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; } /// /// 播放视频 /// /// 视频文件 /// 播放透明度 /// 是否多线程解码 /// 开始播放的帧索引 /// 是否同步解码第一帧, 区别在于是否当前帧就能立马看到视频, 会耗时10ms /// 是否启用快速解码(跳过环路滤波器),默认开启以提升性能 /// 返回值: 0表示成功, 其他表示错误码 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(), beginFrameIndex, syncDecodeFirstFrame, fastDecode); #else // Unity 2020/2021: 创建持久的数据副本供解码器使用 if (_hasVideoDataBuffer) { _videoDataBuffer.Dispose(); } _videoDataBuffer = new NativeArray(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; } /// /// 更新渲染目标尺寸(子类实现) /// 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(); } } /// /// 编辑器里(含 Timeline 预览、GUI 事件里求值)可能仍判定为“非运行态销毁”, /// 仅用 Application.isPlaying 不可靠;编辑器内对运行时创建的纹理统一 DestroyImmediate。 /// 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(); } /// /// 初始化材质(子类实现) /// protected abstract void OnInitMaterial(); /// /// 更新纹理 /// /// 是否立即Apply上传到GPU。false时只复制数据,可以稍后调用DisplayFrameApply 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; } /// /// / 之后调用。 /// YUV UI 在编辑器 Timeline 等路径下,仅主纹理可能触发 Canvas 刷新,U/V 已 Apply 但画面色度仍卡首帧时可在此重绑并标脏。 /// protected virtual void OnTexturesUploadedToGpu() { } /// /// 将纹理数据上传到GPU(可与下一帧解码并行) /// 配合 DisplayFrame(false) 使用,实现分步显示优化 /// protected virtual void DisplayFrameApply() { _decoder.ApplyTextures(_textures); OnTexturesUploadedToGpu(); } // 跳转到指定时间点 public void Seek(double ms) { _decoder.Seek(ms); } // 跳转到指定帧 public void SeekToFrame(long frame) { _decoder.SeekToFrame(frame); } /// /// Timeline 等外部时间源:Seek 到指定帧并立即解码显示一帧(不依赖 Update 推进)。 /// 仅支持非多线程解码;与 Timeline 同轨时请关闭多线程解码。 /// /// false 时仅写入纹理 CPU 侧,需再调 (或等 )。 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); } } /// /// Timeline 顺序播放:不 Seek,仅解码下一帧并显示。与 ScrubToFrameAndDisplay 配合可降低每帧 Seek 的开销。 /// /// false 时须调用 完成 GPU 上传。 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; } /// /// 将 Timeline 路径下延迟的纹理 Apply 立刻提交(与 中逻辑一致)。 /// public void FlushDeferredTimelineDisplay() { if (!_needApplyTexture || _decoder == null || _textures == null) return; _needApplyTexture = false; DisplayFrameApply(); } /// /// 多线程 + Timeline:把显示对齐到目标帧。顺序前进时尽量用子线程解码,并在成功后立刻 Pause, /// 以便在两拍 ProcessFrame 之间预解下一帧;同一 Timeline 帧重复求值时由 Behaviour 侧 Pause 刹停。 /// 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); } } /// /// 多线程 + Timeline「CatchUp」模式:入口 子线程,仅靠消费 _hasFrameReady 追上目标帧。 /// 与 Match 待机一致:帧未就绪时不阻塞主线程(仅极短自旋),避免 把帧率锁死在解码速度上。 /// 若本拍未追上目标帧,返回 false,由 Behaviour 保持 _lastScrubFrame 待下拍再追。回跳或正向落后 ≥20 帧走主线程 Seek,避免多拍逐步追。 /// 同一 Unity 帧内共享 Display 预算,抑制 60Hz ProcessFrame 对 24fps 视频的多遍求值尖峰。 /// /// 当前显示是否已对齐 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); } } /// CatchUp consume 循环中等待单帧就绪的毫秒上限;过短追不上 Timeline 帧号,过长占主线程。 int _catchUpConsumePerFrameWaitMs = 20; public int CatchUpConsumePerFrameWaitMs { get => _catchUpConsumePerFrameWaitMs; set => _catchUpConsumePerFrameWaitMs = Mathf.Clamp(value, 1, 50); } /// CatchUp:仅自旋、不 Sleep;上限略收紧以降低主线程在「帧迟迟未就绪」时的尖峰占用。 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; } /// Timeline 多线程:保持暂停,避免子线程抢解;无新视频帧时不要唤醒解码线程。 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); } /// /// 等待解码线程将 _hasFrameReady 置位。按真实经过时间截断。 /// Timeline 在 中调用:主线程不宜长时间 SpinWait(会拖垮帧率),短自旋后以 让步。 /// 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(); } /// /// 更新材质(子类实现) /// protected abstract void OnUpdateMaterial(); public void SetLooping(bool isLooping) { _isLooping = isLooping; // 如果是微信小游戏平台,设置解码器的循环播放状态 if (IsWCGame) { _decoder.IsLooping = isLooping; } if (_isLooping && _isPause) { Resume(); } } /// /// 设置循环播放区间, 帧数范围为 1 ~ FrameCount /// /// 开始帧索引 /// 结束帧索引 public void SetLoopingFrame(long beginFrame, long endFrame) { _loopBeginFrameIndex = beginFrame; _loopEndFrameIndex = endFrame; } /// /// 切换视频 /// /// 视频文件 /// 播放透明度 /// 是否多线程解码 /// 开始播放的帧索引 /// 是否同步解码第一帧, 区别在于是否当前帧就能立马看到视频, 会耗时10ms /// 是否启用快速解码(跳过环路滤波器),默认开启以提升性能 /// 返回值: true表示成功, false表示失败 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(); } /// /// Timeline 专用:强制停止解码器,不受 Application.isPlaying 限制。 /// 用于编辑器预览结束时清理状态。 /// public void ForceStopForTimeline() { InternalStop(); OnDisableRenderTarget(); } /// /// 轻量解析 bytes 视频元信息(syncDecodeFirstFrame: false,不解码到纹理),运行态与编辑器均可用。 /// 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(file.bytes, Allocator.Temp); #endif try { int ret; #if UNITY_2022_1_OR_NEWER ret = dec.Init(file.GetData(), 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 /// /// 仅编辑器:解析 bytes 中的时长与帧信息,用于 Timeline 新建 Clip 时对齐长度。 /// 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 /// 编辑器 Timeline 多线程预览时也需要停/启解码线程,否则 Pause 被跳过会导致与主线程抢解。 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 /// /// 编辑器下暂停底层解码器(不依赖 Play 模式),用于首帧预览等;避免外部反射访问 _decoder。 /// public void EditorPauseDecoderIgnoringPlayMode() { _isPause = true; _decoder?.Pause(true); } /// /// 编辑器下恢复底层解码器(不依赖 Play 模式),与 / 配合。 /// public void EditorResumeDecoderIgnoringPlayMode() { _isPause = false; _decoder?.Pause(false); } #endif /// /// 恢复播放(运行态下同步恢复解码器)。 /// public void Resume() { if (!CanApplyPlaybackPauseResume()) return; _isPause = false; // 调用解码器的恢复方法 _decoder?.Pause(false); } }