Files
tysdk-intergration/sdk-intergration/LocalPackages/LeviathanVideo/Runtime/LeviathanVideoDecoderBase.cs
2026-03-24 11:43:22 +08:00

928 lines
28 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.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;
// 多线程相关
protected Thread _decodeThread;
// 帧状态0=允许解码下一帧, 1=帧已准备好等待显示
protected long _hasFrameReady;
protected volatile bool _isPlaying;
public bool IsPlaying => _isPlaying;
protected bool _isPause;
public bool IsPause => _isPause;
// 销毁状态标志(防止销毁期间的异步操作继续执行)
protected bool _isDestroying = false;
// 第一帧回调待执行标志(用于从子线程安全地在主线程执行回调)
protected volatile bool _pendingFirstFrameCallback = false;
// ==区间循环专用
// 每次播放到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;
protected float _playSpeed = 1.0f;
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>
/// 第一帧解码完成回调(由解码器触发,可能在子线程中调用)
/// 设置标志位,实际处理在主线程的 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) Destroy(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;
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($"DecodeNextFrame ret: {ret}");
}
OnUpdateRenderTargetSize();
// 只有同步解码第一帧时才立即显示,否则等待第一帧解码完成后在 HandleFirstFrameDecoded 中显示
if (syncDecodeFirstFrame)
{
DisplayFrame();
// 同步模式下立即触发首帧渲染完毕回调
OnFirstFrameRendered?.Invoke();
}
if (_isMultithreadedDecode)
{
// 多线程模式
Interlocked.Exchange(ref _hasFrameReady, 1); // 第一帧已准备好
_isPlaying = true;
_isPause = false;
_decodeThread = new Thread(DecodeThreadFunction);
#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;
_needApplyTexture = false;
_isPause = false;
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 (_pendingFirstFrameCallback)
{
_pendingFirstFrameCallback = false;
HandleFirstFrameDecoded();
}
if (_isPause) return;
if (!_isPlaying) return;
#if UNITY_WEBGL && !UNITY_EDITOR
_timeAccumulated += 1.0 / Application.targetFrameRate * _playSpeed;
#else
_timeAccumulated += Time.deltaTime * _playSpeed;
#endif
if (_isMultithreadedDecode)
{
// 多线程模式
if (_timeAccumulated >= _frameInterval)
{
// 检查是否有新帧准备好
if (Interlocked.Read(ref _hasFrameReady) > 0)
{
// 复制帧数据到纹理不立即Apply
DisplayFrame(applyImmediately: false);
_timeAccumulated -= _frameInterval;
// DisplayFrame 完成后,通知解码线程可以解码下一帧
Interlocked.Exchange(ref _hasFrameReady, 0);
}
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 (_needApplyTexture) {
_needApplyTexture = false;
if (_decoder != null && _textures != null)
{
DisplayFrameApply();
}
}
}
// 解码线程(仅多线程模式)
protected void DecodeThreadFunction()
{
try
{
while (_isPlaying)
{
if (!_isPause)
{
// 等待主线程显示完当前帧_hasFrameReady == 0 表示允许解码下一帧)
while (Interlocked.Read(ref _hasFrameReady) != 0 && _isPlaying)
{
Thread.Sleep(1);
}
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);
// 触发播放完成回调
OnPlayCompleted?.Invoke();
}
}
}
else
{
Thread.Sleep(5);
}
}
}
catch (ThreadAbortException)
{
Thread.ResetAbort();
}
}
// 初始化纹理
protected virtual void InitTextures()
{
if (_textures != null && _textures.Length > 0)
{
Destroy(_textures[0]);
Destroy(_textures[1]);
Destroy(_textures[2]);
}
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)
{
var videoFrame = _decoder.VideoFrame;
if (videoFrame->linesize[0] <= 0) return;
// 检查纹理是否需要重新创建
if (_textures == null || _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后清除延迟标志
}
else
{
_needApplyTexture = true; // 延迟Apply设置标志在LateUpdate中执行
}
// 更新播放帧索引
if (_isMultithreadedDecode)
{
_playFrameIndex = Interlocked.Read(ref _decoder.GetDecodedFrameIndexRef());
}
else
{
_playFrameIndex = _decoder.DecodedFrameIndex;
}
// 重置循环seek标志
_isLoopSeeking = false;
}
/// <summary>
/// 将纹理数据上传到GPU可与下一帧解码并行
/// 配合 DisplayFrame(false) 使用,实现分步显示优化
/// </summary>
protected virtual void DisplayFrameApply()
{
_decoder.ApplyTextures(_textures);
}
// 跳转到指定时间点
public void Seek(double ms)
{
_decoder.Seek(ms);
}
// 跳转到指定帧
public void SeekToFrame(long frame)
{
_decoder.SeekToFrame(frame);
}
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();
}
public void Pause()
{
if (!Application.isPlaying) return;
_isPause = true;
// 调用解码器的暂停方法
_decoder?.Pause(true);
}
public void Resume()
{
if (!Application.isPlaying) return;
_isPause = false;
// 调用解码器的恢复方法
_decoder?.Pause(false);
}
}