Files
ft/Client/LocalPackages/LeviathanVideo/Runtime/LeviathanVideoDecoderBase.cs
2026-06-29 21:18:33 +08:00

1592 lines
54 KiB
C#
Raw 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.
/*----------------------------------------------------------------
// 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/Resume5ms 会叠加明显卡顿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);
}
}