yyl
17 小时以前 8d77144cfb756529507dc6927dcdcfd9f4a30750
视频播放支持
7个文件已添加
3个文件已修改
780 ■■■■■ 已修改文件
Main/ResModule/ResManager.cs 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Hero/UIHeroController.cs 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Video.meta 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Video/UIVideoPlayer.cs 505 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Video/UIVideoPlayer.cs.meta 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Video/VideoManager.cs 117 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Video/VideoManager.cs.meta 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Video/VideoWin.cs 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Video/VideoWin.cs.meta 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/Utility/Extension.cs 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/ResModule/ResManager.cs
@@ -6,6 +6,8 @@
using UnityEngine.Video;
using Spine.Unity;
using UnityEngine.UI;
using Cysharp.Threading.Tasks;
@@ -251,6 +253,22 @@
        LoadAssetAsyncInternal<T>(directory, name, callBack, needExt);
    }
    public async UniTask<T> LoadAssetAsync<T>(string directory, string name, bool needExt = true) where T : UnityEngine.Object
    {
        var tcs = new UniTaskCompletionSource<T>();
        LoadAssetAsync<T>(directory, name, (isLoaded, asset) => {
            if (isLoaded)
            {
                tcs.TrySetResult(asset as T);
            }
            else
            {
                tcs.TrySetException(new Exception($"Failed to load asset: {directory}/{name}"));
            }
        }, needExt);
        return await tcs.Task;
    }
    private void LoadSpriteAsync<T>(string atlasName, string spriteName, Action<bool, UnityEngine.Object> callBack) where T : UnityEngine.Object
    {
#if !UNITY_EDITOR
Main/System/Hero/UIHeroController.cs
@@ -1,5 +1,6 @@
using System;
using Spine;
using Spine.Unity;
using UnityEngine;
using UnityEngine.UI;
@@ -10,7 +11,7 @@
    private int skinID;
    protected SkeletonGraphic skeletonGraphic;
    protected Spine.AnimationState spineAnimationState;
    public Spine.AnimationState spineAnimationState;
    private GameObject instanceGO;
    public Action onComplete;
@@ -26,7 +27,7 @@
                if (isLh)
                {
                    var skinConfigTmp = HeroSkinConfig.Get(skinID);
                    if (skinConfigTmp != null && skinConfigTmp.Tachie.Contains("SkeletonData"))
                    if (skinConfigTmp != null && skinConfigTmp.Tachie.IsSpine())
                    {
                        skeletonGraphic.enabled = true;
                    }
@@ -60,7 +61,7 @@
            //立绘特殊处理,没有spine动画的改用图片
            var lhImg = this.AddMissingComponent<RawImage>();
            if (!skinConfig.Tachie.Contains("SkeletonData"))
            if (!skinConfig.Tachie.IsSpine())
            {
                //图片替换
                lhImg.SetTexture2DPNG(skinConfig.Tachie);
@@ -150,7 +151,10 @@
        spineAnimationState.Complete += OnAnimationComplete;
    }
    public bool HasAnimation(string motionName)
    {
        return skeletonGraphic != null && skeletonGraphic.Skeleton != null && skeletonGraphic.Skeleton.ContainsMotion(motionName);
    }
    protected void OnDestroy()
@@ -171,15 +175,15 @@
    /// <param name="motionName">动作名</param>
    /// <param name="loop">循环</param>
    /// <param name="replay">如果相同动作是否再次重播,比如跑步重播就会跳帧不顺滑</param>
    public virtual void PlayAnimation(string motionName, bool loop = false, bool replay = true)
    public virtual TrackEntry PlayAnimation(string motionName, bool loop = false, bool replay = true)
    {
        if (spineAnimationState == null) return;
        if (spineAnimationState == null) return null;
        if (GetCurrentAnimationName() == motionName && !replay)
            return;
            return null;
        // 直接使用 ToString() 而不是调用 GetAnimationName
        spineAnimationState.SetAnimation(0, motionName.ToString(), loop);
        return spineAnimationState.SetAnimation(0, motionName.ToString(), loop);
    }
    // 播放第一个动画(作为默认动画)
Main/System/Video.meta
New file
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 9f8b60fd5cb8de143a61b1a3ea0fc728
folderAsset: yes
DefaultImporter:
  externalObjects: {}
  userData:
  assetBundleName:
  assetBundleVariant:
Main/System/Video/UIVideoPlayer.cs
New file
@@ -0,0 +1,505 @@
using System;
using UnityEngine;
using UnityEngine.Video;
using UnityEngine.UI;
/// <summary>
/// UI视频播放器组件 - 支持WebM格式透明视频播放
/// </summary>
public class UIVideoPlayer : MonoBehaviour
{
    [Header("组件引用")]
    public VideoPlayer videoPlayer;
    public RawImage videoImage;
    [Header("调试信息")]
    public const string directory = "Video";
    public string videoName;
    public bool isLoop = false;
    [Header("资源管理")]
    [Tooltip("播放完成后是否自动卸载资源(仅当非循环播放时有效)")]
    public bool autoUnloadOnFinish = true;
    [Header("回调事件")]
    public Action onComplete;
    public Action onPrepared;
    private RenderTexture _renderTexture;
    private VideoClip _loadedClip;
    private bool _isPrepared;
    private bool _isLoading;
    private Action<bool> _onPrepareCallback;
    #region Public API
    /// <summary>
    /// 加载并播放视频(通过资源系统加载 VideoClip)
    /// </summary>
    /// <param name="_videoName">视频文件名(不含扩展名)</param>
    /// <param name="_loop">是否循环播放</param>
    /// <param name="_onComplete">播放完成回调(仅在非循环播放时调用)</param>
    public async void LoadAndPlayVideo(string _videoName, bool _loop = false, Action _onComplete = null)
    {
        if (!ValidateComponents()) return;
        if (_isLoading)
        {
            Debug.LogWarning($"UIVideoPlayer: Already loading video. Please wait.");
            return;
        }
        videoName = _videoName;
        isLoop = _loop;
        onComplete = _onComplete;
        _isPrepared = false;
        _isLoading = true;
        // 清理旧资源
        ReleaseRenderTexture();
        // 隐藏或设置空状态颜色
        SetEmptyState();
        // 通过 ResManager 加载 VideoClip 资源(支持 AB 打包)
        _loadedClip = await ResManager.Instance.LoadAssetAsync<VideoClip>(directory, videoName, false);
        // 检查是否在加载过程中被取消
        if (!_isLoading)
        {
            _loadedClip = null;
            return;
        }
        _isLoading = false;
        if (_loadedClip == null)
        {
            Debug.LogError($"UIVideoPlayer: Failed to load VideoClip: {directory}/{videoName}");
            _isPrepared = false;
            return;
        }
        // 创建并绑定 RenderTexture(按视频实际尺寸)
        _renderTexture = new RenderTexture((int)_loadedClip.width, (int)_loadedClip.height, 0);
        videoPlayer.targetTexture = _renderTexture;
        videoImage.texture = _renderTexture;
        videoImage.SetNativeSize();
        // 显示视频
        SetActiveState();
        videoPlayer.source = VideoSource.VideoClip;
        videoPlayer.clip = _loadedClip;
        videoPlayer.isLooping = isLoop;
        videoPlayer.prepareCompleted += OnVideoPrepared;
        // 非循环播放时注册完成事件
        if (!isLoop)
        {
            videoPlayer.loopPointReached += OnVideoFinished;
        }
        videoPlayer.Prepare();
    }
    /// <summary>
    /// 加载并播放指定视频
    /// </summary>
    public void LoadAndPlay(string videoName, bool loop = false, Action onCompleteCallback = null)
    {
        LoadAndPlayVideo(videoName, loop, onCompleteCallback);
    }
    [ContextMenu("播放")]
    public void Play()
    {
        if (!ValidateComponents()) return;
        if (_isPrepared && !videoPlayer.isPlaying)
        {
            videoPlayer.Play();
        }
    }
    [ContextMenu("暂停")]
    public void Pause()
    {
        if (!ValidateComponents()) return;
        if (videoPlayer.isPlaying)
        {
            videoPlayer.Pause();
        }
    }
    [ContextMenu("恢复")]
    public void Resume()
    {
        if (!ValidateComponents()) return;
        if (_isPrepared && !videoPlayer.isPlaying)
        {
            videoPlayer.Play();
        }
    }
    /// <summary>
    /// 预加载视频(不自动播放)
    /// </summary>
    /// <param name="_videoName">视频文件名(不含扩展名)</param>
    /// <param name="onPreparedOK">预加载完成回调,参数为是否成功</param>
    public async void Prepare(string _videoName, Action<bool> onPreparedOK)
    {
        if (!ValidateComponents())
        {
            onPreparedOK?.Invoke(false);
            return;
        }
        if (_isLoading)
        {
            Debug.LogWarning($"UIVideoPlayer: Already loading video. Please wait.");
            onPreparedOK?.Invoke(false);
            return;
        }
        videoName = _videoName;
        _onPrepareCallback = onPreparedOK;
        _isPrepared = false;
        _isLoading = true;
        // 清理旧资源
        ReleaseRenderTexture();
        SetEmptyState();
        // 通过 ResManager 加载 VideoClip 资源(支持 AB 打包)
        _loadedClip = await ResManager.Instance.LoadAssetAsync<VideoClip>(directory, videoName, false);
        // 检查是否在加载过程中被取消
        if (!_isLoading)
        {
            _loadedClip = null;
            return;
        }
        _isLoading = false;
        if (_loadedClip == null)
        {
            Debug.LogError($"UIVideoPlayer: Failed to load VideoClip: {directory}/{videoName}");
            _isPrepared = false;
            _onPrepareCallback?.Invoke(false);
            _onPrepareCallback = null;
            return;
        }
        // 创建并绑定 RenderTexture(按视频实际尺寸)
        _renderTexture = new RenderTexture((int)_loadedClip.width, (int)_loadedClip.height, 0);
        videoPlayer.targetTexture = _renderTexture;
        videoImage.texture = _renderTexture;
        videoImage.SetNativeSize();
        SetActiveState();
        videoPlayer.source = VideoSource.VideoClip;
        videoPlayer.clip = _loadedClip;
        videoPlayer.prepareCompleted += OnVideoPreparedForPreload;
        videoPlayer.Prepare();
    }
    /// <summary>
    /// 通过 URL 加载并播放视频
    /// </summary>
    /// <param name="url">视频 URL 地址</param>
    /// <param name="_loop">是否循环播放</param>
    /// <param name="_onComplete">播放完成回调(仅在非循环播放时调用)</param>
    public void LoadAndPlayFromURL(string url, bool _loop = false, Action _onComplete = null)
    {
        if (!ValidateComponents()) return;
        if (_isLoading)
        {
            Debug.LogWarning($"UIVideoPlayer: Already loading video. Please wait.");
            return;
        }
        videoName = url;
        isLoop = _loop;
        onComplete = _onComplete;
        _isPrepared = false;
        _isLoading = true;
        // 清理旧资源
        ReleaseRenderTexture();
        SetEmptyState();
        // 根据 URL 视频创建默认尺寸的 RenderTexture(准备完成后会调整)
        _renderTexture = new RenderTexture(1920, 1080, 0);
        videoPlayer.targetTexture = _renderTexture;
        videoImage.texture = _renderTexture;
        SetActiveState();
        videoPlayer.source = VideoSource.Url;
        videoPlayer.url = url;
        videoPlayer.isLooping = isLoop;
        videoPlayer.prepareCompleted += OnVideoPreparedForURL;
        // 非循环播放时注册完成事件
        if (!isLoop)
        {
            videoPlayer.loopPointReached += OnVideoFinished;
        }
        videoPlayer.Prepare();
    }
    [ContextMenu("停止")]
    public void Stop()
    {
        if (!ValidateComponents()) return;
        videoPlayer.Stop();
    }
    [ContextMenu("重新播放")]
    public void Restart()
    {
        if (!ValidateComponents()) return;
        if (_isPrepared)
        {
            videoPlayer.Stop();
            videoPlayer.Play();
        }
    }
    /// <summary>
    /// 卸载视频资源
    /// </summary>
    public void Unload()
    {
        if (videoPlayer == null) return;
        // 取消所有事件订阅
        videoPlayer.prepareCompleted -= OnVideoPrepared;
        videoPlayer.prepareCompleted -= OnVideoPreparedForPreload;
        videoPlayer.prepareCompleted -= OnVideoPreparedForURL;
        videoPlayer.loopPointReached -= OnVideoFinished;
        videoPlayer.Stop();
        videoPlayer.clip = null;
        videoPlayer.url = null;
        videoPlayer.targetTexture = null;
        if (videoImage != null)
        {
            videoImage.texture = null;
        }
        _loadedClip = null;
        ReleaseRenderTexture();
        // 恢复空状态
        SetEmptyState();
        _isPrepared = false;
        _isLoading = false;
        onComplete = null;
        onPrepared = null;
    }
    #endregion
    #region Properties
    /// <summary>
    /// 视频是否已准备好
    /// </summary>
    public bool IsPrepared => _isPrepared;
    /// <summary>
    /// 视频是否正在播放
    /// </summary>
    public bool IsPlaying => videoPlayer != null && videoPlayer.isPlaying;
    /// <summary>
    /// 视频是否正在加载
    /// </summary>
    public bool IsLoading => _isLoading;
    /// <summary>
    /// 当前播放时间(秒)
    /// </summary>
    public double CurrentTime => videoPlayer != null ? videoPlayer.time : 0;
    /// <summary>
    /// 视频总时长(秒)
    /// </summary>
    public double Duration => _loadedClip != null ? _loadedClip.length : 0;
    #endregion
    #region Private Methods
    [ContextMenu("重新加载")]
    private void Reload()
    {
        LoadAndPlayVideo(videoName, isLoop);
    }
    private void OnVideoPrepared(VideoPlayer vp)
    {
        vp.prepareCompleted -= OnVideoPrepared;
        _isPrepared = true;
        _isLoading = false;
        onPrepared?.Invoke();
        vp.Play();
    }
    private void OnVideoPreparedForPreload(VideoPlayer vp)
    {
        vp.prepareCompleted -= OnVideoPreparedForPreload;
        _isPrepared = true;
        _isLoading = false;
        _onPrepareCallback?.Invoke(true);
        _onPrepareCallback = null;
        // 预加载完成后不自动播放
    }
    private void OnVideoPreparedForURL(VideoPlayer vp)
    {
        vp.prepareCompleted -= OnVideoPreparedForURL;
        _isPrepared = true;
        _isLoading = false;
        // 调整 RenderTexture 尺寸为实际视频尺寸
        if (_renderTexture != null && vp.texture != null)
        {
            var newRT = new RenderTexture((int)vp.width, (int)vp.height, 0);
            ReleaseRenderTexture();
            _renderTexture = newRT;
            vp.targetTexture = _renderTexture;
            videoImage.texture = _renderTexture;
            videoImage.SetNativeSize();
        }
        onPrepared?.Invoke();
        vp.Play();
    }
    private void OnVideoFinished(VideoPlayer vp)
    {
        onComplete?.Invoke();
        // 根据设置自动清理资源
        if (autoUnloadOnFinish)
        {
            Unload();
        }
    }
    private void ReleaseRenderTexture()
    {
        if (_renderTexture != null)
        {
            _renderTexture.Release();
            Destroy(_renderTexture);
            _renderTexture = null;
        }
    }
    /// <summary>
    /// 设置空状态(未加载视频时)
    /// </summary>
    private void SetEmptyState()
    {
        if (videoImage != null)
        {
            videoImage.enabled = false;
        }
    }
    /// <summary>
    /// 设置激活状态(视频已加载)
    /// </summary>
    private void SetActiveState()
    {
        if (videoImage != null)
        {
            videoImage.enabled = true;
        }
    }
    /// <summary>
    /// 验证必需组件
    /// </summary>
    private bool ValidateComponents()
    {
        if (videoPlayer == null)
        {
            Debug.LogError("UIVideoPlayer: VideoPlayer component is not assigned.");
            return false;
        }
        if (videoImage == null)
        {
            Debug.LogError("UIVideoPlayer: RawImage component is not assigned.");
            return false;
        }
        return true;
    }
    #endregion
    #region Unity Lifecycle
    private void Awake()
    {
        // 初始化时设置为空状态
        SetEmptyState();
    }
    private void OnDestroy()
    {
        Unload();
    }
    /// <summary>
    /// 取消预加载
    /// </summary>
    public void CancelPrepare()
    {
        if (videoPlayer == null) return;
        // 取消所有准备相关的事件订阅
        videoPlayer.prepareCompleted -= OnVideoPrepared;
        videoPlayer.prepareCompleted -= OnVideoPreparedForPreload;
        videoPlayer.prepareCompleted -= OnVideoPreparedForURL;
        // 停止 VideoPlayer(如果正在加载)
        if (_isLoading)
        {
            videoPlayer.Stop();
        }
        // 清理资源
        videoPlayer.clip = null;
        videoPlayer.url = null;
        videoPlayer.targetTexture = null;
        if (videoImage != null)
        {
            videoImage.texture = null;
        }
        _loadedClip = null;
        ReleaseRenderTexture();
        SetEmptyState();
        // 重置状态
        _isPrepared = false;
        _isLoading = false;
        // 触发失败回调
        _onPrepareCallback?.Invoke(false);
        _onPrepareCallback = null;
    }
    #endregion
}
Main/System/Video/UIVideoPlayer.cs.meta
New file
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 780579551ee7fe442bc047278c4b96d6
MonoImporter:
  externalObjects: {}
  serializedVersion: 2
  defaultReferences: []
  executionOrder: 0
  icon: {instanceID: 0}
  userData:
  assetBundleName:
  assetBundleVariant:
Main/System/Video/VideoManager.cs
New file
@@ -0,0 +1,117 @@
using System;
using System.Collections.Generic;
using UnityEngine;
public class VideoManager : Singleton<VideoManager>
{
    private GameObjectPoolManager.GameObjectPool uiVideoPool;
    private Dictionary<string, UIVideoPlayer> prepareDict = new Dictionary<string, UIVideoPlayer>();
    private Dictionary<string, Action<bool, UIVideoPlayer>> prepareCallbacks = new Dictionary<string, Action<bool, UIVideoPlayer>>();
    public void Init()
    {
        GameObject uiVideoPrefab = UILoader.LoadPrefab("UIVideoPlayer");
        uiVideoPool = GameObjectPoolManager.Instance.GetPool(uiVideoPrefab);
    }
    public void Release()
    {
        if (uiVideoPool != null)
        {
            uiVideoPool.Clear();
            uiVideoPool = null;
        }
    }
    private UIVideoPlayer GetUIVideoPlayer()
    {
        GameObject videoObj = uiVideoPool.Request();
        if (videoObj != null)
        {
            return videoObj.GetComponent<UIVideoPlayer>();
        }
        return null;
    }
    public UIVideoPlayer PrepareVideo(string videoName, Action<bool, UIVideoPlayer> onPrepared)
    {
        //  如果已经在准备中,直接返回 并给出提示 已经在加载准备中
        if (prepareDict.TryGetValue(videoName, out UIVideoPlayer existingPlayer))
        {
            Debug.LogError("Video " + videoName + " is already being prepared.");
            return existingPlayer;
        }
        UIVideoPlayer uiVideoPlayer = GetUIVideoPlayer();
        if (uiVideoPlayer != null)
        {
            string _videoName = videoName; // 避免闭包问题
            prepareDict.Add(_videoName, uiVideoPlayer);
            prepareCallbacks.Add(_videoName, onPrepared);
            uiVideoPlayer.Prepare(videoName, (success) =>
            {
                OnPrepareOK(videoName, success, uiVideoPlayer);
            });
        }
        else
        {
            Debug.LogError("Failed to get UIVideoPlayer from pool.");
            onPrepared?.Invoke(false, null);
        }
        return uiVideoPlayer;
    }
    public void CancelPrepare(UIVideoPlayer uIVideoPlayer)
    {
        if (uIVideoPlayer != null)
            uIVideoPlayer.CancelPrepare();
        {
            uiVideoPool.Release(uIVideoPlayer.gameObject);
            // 从 prepareDict 移除
            string keyToRemove = null;
            foreach (var kv in prepareDict)
            {
                if (kv.Value == uIVideoPlayer)
                {
                    keyToRemove = kv.Key;
                    break;
                }
            }
            if (keyToRemove != null)
            {
                prepareDict.Remove(keyToRemove);
                prepareCallbacks.Remove(keyToRemove);
            }
        }
    }
    private void OnPrepareOK(string videoName, bool success, UIVideoPlayer uiVideoPlayer)
    {
        // 安全移除和回调
        if (prepareDict.ContainsKey(videoName))
        {
            prepareDict.Remove(videoName);
        }
        Action<bool, UIVideoPlayer> callback = null;
        if (prepareCallbacks.TryGetValue(videoName, out callback))
        {
            callback?.Invoke(success, uiVideoPlayer);
            prepareCallbacks.Remove(videoName);
        }
    }
    public void ReleaseVideoPlayer(UIVideoPlayer uIVideoPlayer)
    {
        if (uIVideoPlayer != null)
        {
            CancelPrepare(uIVideoPlayer);
            uiVideoPool.Release(uIVideoPlayer.gameObject);
        }
    }
}
Main/System/Video/VideoManager.cs.meta
New file
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 14d20be0871f2e24d96b79440816d7cb
MonoImporter:
  externalObjects: {}
  serializedVersion: 2
  defaultReferences: []
  executionOrder: 0
  icon: {instanceID: 0}
  userData:
  assetBundleName:
  assetBundleVariant:
Main/System/Video/VideoWin.cs
New file
@@ -0,0 +1,20 @@
using System;
using UnityEngine;
using UnityEngine.Video;
using UnityEngine.UI;
public class VideoWin : UIBase
{
    public UIVideoPlayer videoPlayer;
    protected override void OnOpen()
    {
        // 视频准备完成后会自动播放
    }
    protected override void OnPreClose()
    {
        base.OnPreClose();
        videoPlayer.Unload();
    }
}
Main/System/Video/VideoWin.cs.meta
New file
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: cf2a5c5ccb4f22f4c9408bd3b3efed69
MonoImporter:
  externalObjects: {}
  serializedVersion: 2
  defaultReferences: []
  executionOrder: 0
  icon: {instanceID: 0}
  userData:
  assetBundleName:
  assetBundleVariant:
Main/Utility/Extension.cs
@@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
using Spine;
using Spine.Unity;
public static class Extension
{
@@ -74,4 +76,61 @@
            }
        }
    }
    public static bool IsSpine(this string _resName)
    {
        if (string.IsNullOrEmpty(_resName))
        {
            return false;
        }
        return _resName.Contains("SkeletonData");
    }
    public static bool IsVideo(this string _resName)
    {
        if (string.IsNullOrEmpty(_resName))
        {
            return false;
        }
        return _resName.EndsWith(".mp4");
    }
    public static bool ContainsMotion(this Spine.Skeleton skeleton, string motionName)
    {
        if (skeleton == null || string.IsNullOrEmpty(motionName))
        {
            return false;
        }
        for (int i = 0; i < skeleton.Data.Animations.Count; i++)
        {
            if (skeleton.Data.Animations.Items[i].Name.ToLower() == motionName.ToLower())
            {
                return true;
            }
        }
        return false;
    }
    public static Spine.Animation GetSpineAnimation(this Spine.Skeleton skeleton, string motionName)
    {
        if (skeleton == null || string.IsNullOrEmpty(motionName))
        {
            return null;
        }
        for (int i = 0; i < skeleton.Data.Animations.Count; i++)
        {
            if (skeleton.Data.Animations.Items[i].Name.ToLower() == motionName.ToLower())
            {
                return skeleton.Data.Animations.Items[i];
            }
        }
        return null;
    }
}