com.guru.unity.sdk.core/Runtime/GuruCore/Runtime/ExtensionKit/CDNLoader.cs

445 lines
12 KiB
C#

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Firebase.Crashlytics;
using Guru;
using UnityEngine;
using UnityEngine.Networking;
#if UNITY_EDITOR
using UnityEditor;
#endif
/// <summary>
/// CDN云控配置
/// </summary>
[Serializable]
public class CDNConfig
{
public const string KEY_CDN_CONFIG = "cdn_config";
public string[] replace;
public string main;
public string fallback;
public int retry;
public int timeout;
private static CDNConfig _config;
public static void Init(string defaultValue)
{
FirebaseUtil.AppendDefaultValue(KEY_CDN_CONFIG, defaultValue);
}
/// <summary>
/// 加载配置
/// </summary>
/// <returns></returns>
public static CDNConfig Load()
{
if (_config != null)
return _config;
try
{
_config = FirebaseUtil.GetRemoteConfig<CDNConfig>(KEY_CDN_CONFIG);
}
catch (Exception e)
{
Log.E(e);
Crashlytics.LogException(e);
}
return _config;
}
}
public class CDNLoader : MonoBehaviour
{
/// <summary>
/// 受保护的URL参数列表
/// </summary>
private static string[] protectedParamKeys = new string[]
{
"generation"
};
/// <summary>
/// 加载配置
/// </summary>
private CDNConfig _config;
public CDNConfig Config => _config;
/// <summary>
/// 使用备用地址
/// </summary>
private bool _useFallback;
private int _retryNum;
private UnityWebRequest _request;
private Action<int, string> onError;
private Action<DownloadHandler> onComplete;
private LinkedList<LoadTask> _loadList; // 加载列表
private bool _isOnLoading;
#region 初始化
/// <summary>
/// 创建Loader
/// </summary>
/// <returns></returns>
public static CDNLoader Create()
{
var go = new GameObject(nameof(CDNLoader));
var loader = go.AddComponent<CDNLoader>();
return loader;
}
private void Awake()
{
Init();
}
private void Init()
{
_config = CDNConfig.Load();
_isOnLoading = false;
_useFallback = false;
_loadList = new LinkedList<LoadTask>();
}
/// <summary>
/// 获取链接地址
/// </summary>
/// <param name="originUrl"></param>
/// <returns></returns>
public string GetUrl(string originUrl, bool useFallback = false)
{
var prefix = useFallback ? _config.fallback : _config.main;
if (_config.replace != null && _config.replace.Length > 0)
{
foreach (var rp in _config.replace)
{
if (originUrl.Contains(rp))
{
originUrl = originUrl.Replace(rp, "");
break;
}
}
}
// 带有参数的URL地址过滤和拼接
originUrl = FixUrlParameter(originUrl);
return $"{prefix}{originUrl}";
}
/// <summary>
/// 过滤整理URL参数
/// </summary>
/// <param name="originUrl"></param>
/// <returns></returns>
public static string FixUrlParameter(string originUrl)
{
originUrl = originUrl.Replace("%2F", "/");
// 带有参数的URL地址过滤和拼接
if (originUrl.Contains("?"))
{
var raw = originUrl.Split('?');
if (raw.Length > 1)
{
var args = "";
var temp = raw[1].Split('&');
if (temp.Length > 0)
{
for (int i = 0; i < temp.Length; i++)
{
if (temp[i].Contains("="))
{
var kvp = temp[i].Split('=');
if (protectedParamKeys.Contains(kvp[0]))
{
args += $"{temp[i]}";
if (i < temp.Length - 2) args += "&";
}
}
}
}
originUrl = raw[0];
if (args.Length > 0) originUrl += $"?{args}";
}
}
return originUrl;
}
#endregion
#region 队列管理
private int TaskCount
{
get
{
if (_loadList != null)
return _loadList.Count;
else
return -1;
}
}
/// <summary>
/// 添加任务
/// </summary>
/// <param name="task"></param>
private void AddTask(LoadTask task, bool addFirst = false)
{
if (_loadList == null)
_loadList = new LinkedList<LoadTask>();
if(addFirst)
_loadList.AddFirst(task);
else
_loadList.AddLast(task);
}
/// <summary>
/// 获取任务
/// </summary>
/// <returns></returns>
private LoadTask GetTask()
{
if (TaskCount > 0)
{
var node = _loadList.First;
_loadList.RemoveFirst();
return node.Value;
}
return null;
}
#endregion
#region 加载接口
/// <summary>
/// 加载文本
/// </summary>
/// <param name="url"></param>
/// <param name="onComplete"></param>
/// <param name="onFail"></param>
/// <param name="onProgress"></param>
public void LoadText(string url, Action<string> onComplete, Action<string> onFail, Action<float> onProgress = null)
{
AddTask(new LoadTask()
{
url = url,
onGetString = onComplete,
onFail = onFail,
onProgress = onProgress,
type = LoadType.Text
});
}
/// <summary>
/// 加载文本
/// </summary>
/// <param name="url"></param>
/// <param name="onComplete"></param>
/// <param name="onFail"></param>
/// <param name="onProgress"></param>
public void LoadFile(string url, Action<byte[]> onComplete, Action<string> onFail, Action<float> onProgress = null)
{
AddTask(new LoadTask()
{
url = url,
onGetBytes = onComplete,
onFail = onFail,
onProgress = onProgress,
type = LoadType.Bytes
});
}
/// <summary>
/// 加载文本
/// </summary>
/// <param name="url"></param>
/// <param name="onComplete"></param>
/// <param name="onFail"></param>
/// <param name="onProgress"></param>
public void LoadTexture2D(string url, Action<Texture2D> onComplete, Action<string> onFail, Action<float> onProgress = null)
{
AddTask(new LoadTask()
{
url = url,
onGetTexture = onComplete,
onFail = onFail,
onProgress = onProgress,
type = LoadType.Bytes
});
}
#endregion
#region 加载队列
/// <summary>
/// 请务必在主线程中实例化CDNLoader
/// </summary>
private void Update()
{
// 加载中则跳过
if (_isOnLoading) return;
// 无任务则跳过
if (TaskCount <= 0) return;
// 开始下载任务
StartCoroutine(nameof(CRLoadTask));
}
/// <summary>
/// 协程加载任务
/// </summary>
/// <returns></returns>
private IEnumerator CRLoadTask()
{
_isOnLoading = true;
var task = GetTask();
if (task != null)
{
task.fixedUrl = GetUrl(task.url, task.useFallback);
Debug.Log($"<color=#88ff00>--- 下载地址: { task.fixedUrl }</color>");
// Debug.Log($"--- _useFallback: {_useFallback}");
// Debug.Log($"--- _retryNum: {_retryNum}");
// 使用Using初始化加载器
using (UnityWebRequest www = UnityWebRequest.Get(task.fixedUrl))
{
if (task.type == LoadType.Texture2D)
www.downloadHandler = new DownloadHandlerTexture();
else
www.downloadHandler = new DownloadHandlerBuffer();
www.timeout = _config.timeout;
yield return www.SendWebRequest();
if (www.result == UnityWebRequest.Result.Success)
{
switch (task.type)
{
case LoadType.Text:
task.onGetString?.Invoke(www.downloadHandler.text);
break;
case LoadType.Bytes:
task.onGetBytes?.Invoke(www.downloadHandler.data);
break;
case LoadType.Texture2D:
var handler2D = www.downloadHandler as DownloadHandlerTexture;
task.onGetTexture?.Invoke(handler2D?.texture ? handler2D.texture : null);
break;
default:
//TODO 请扩展更多的加载接口
break;
}
}
else
{
// 打印(上报) 错误信息
Debug.LogError(
$"Load asset fail, code:{www.responseCode} error:{www.error} url:{task.fixedUrl} type:{task.type.ToString()}");
// 下载错误
task.retryNum++;
if (task.retryNum > 2)
{
if (task.useFallback)
{
task.onFail(www.error);
task.Dispose();
}
else
{
task.retryNum = 0;
task.useFallback = true;
AddTask(task);
}
}
else
{
// 增加次数后加到队尾重新下载
AddTask(task);
}
}
_isOnLoading = false;
}
}
}
#endregion
#region 单元测试
#if UNITY_EDITOR
[MenuItem("Test/Test CDNLoader...")]
public static void EditorTestGetURL()
{
string url = "https://cdn.dof.fungame.studio/PicAssets%2FAndroid%2F6_cx_0818_17?generation=1649423811796255&alt=media";
string url1 = "https://cdn.dof.fungame.studio/PicAssets%2FAndroid%2F6_cx_0818_17?alt=media&generation=1649423811796255";
string url2 = "https://cdn.dof.fungame.studio/PicAssets%2FAndroid%2F6_cx_0818_17?alt=media";
string newUrl = FixUrlParameter(url1);
Debug.Log($"---> newUrl: {newUrl}");
}
#endif
#endregion
}
public enum LoadType
{
Text,
Bytes,
Texture2D,
}
/// <summary>
/// 加载任务
/// </summary>
[Serializable]
internal class LoadTask
{
public string url;
public string fixedUrl;
public Action<string> onGetString;
public Action<byte[]> onGetBytes;
public Action<Texture2D> onGetTexture;
public Action<string> onFail;
public Action<float> onProgress;
public bool useFallback = false;
public int retryNum = 0;
public LoadType type;
public int piority = 100;
public void Dispose()
{
onGetString = null;
onGetBytes = null;
onGetTexture = null;
onFail = null;
onProgress = null;
}
}