diff --git a/Packages/idv.jlchntoz.vvmw/Editor/Common/TrustedUrlUtils.cs b/Packages/idv.jlchntoz.vvmw/Editor/Common/TrustedUrlUtils.cs index 0b9a610..c707933 100644 --- a/Packages/idv.jlchntoz.vvmw/Editor/Common/TrustedUrlUtils.cs +++ b/Packages/idv.jlchntoz.vvmw/Editor/Common/TrustedUrlUtils.cs @@ -7,15 +7,44 @@ using UdonSharp; using UdonSharpEditor; -namespace JLChnToZ.VRC.VVMW { - public static class TrustedUrlUtils { - static readonly AsyncLazy> getTrustedUrlsTask = UniTask.Lazy(GetTrustedUrlsLazy); - static readonly Dictionary trustedDomains = new Dictionary(); - static readonly Dictionary messageCache = new Dictionary(); +namespace JLChnToZ.VRC.VVMW.Editors { + public enum TrustedUrlTypes { + UnityVideo, + AVProDesktop, + AVProAndroid, + ImageUrl, + StringUrl, + } + + public sealed class TrustedUrlUtils { + static readonly Dictionary instances = new Dictionary(); + static readonly AsyncLazy getTrustedUrlsTask = UniTask.Lazy(GetTrustedUrlsLazy); static GUIContent tempContent, warningContent; - static List trustedUrls; - - public static UniTask> TrustedUrls => getTrustedUrlsTask.Task; + readonly Dictionary trustedDomains = new Dictionary(); + readonly Dictionary messageCache = new Dictionary(); + readonly HashSet supportedProtocols; + List trustedUrls; + + static TrustedUrlUtils() { + var stringComparer = StringComparer.OrdinalIgnoreCase; + var supportedProtocolsCurl = new HashSet(new [] { + "http", "https", + }, stringComparer); + // https://www.renderheads.com/content/docs/AVProVideo/articles/supportedmedia.html + // https://learn.microsoft.com/en-us/windows/win32/medfound/supported-protocols + var supportedProtocolsMF = new HashSet(new [] { + "http", "https", "rtsp", "rtspt", "rtspu", "rtmp", "rtmps", + }, stringComparer); + // https://exoplayer.dev/supported-formats.html + var supportedProtocolsExo = new HashSet(new [] { + "http", "https", "rtsp", "rtmp", + }, stringComparer); + instances[TrustedUrlTypes.UnityVideo] = new TrustedUrlUtils(supportedProtocolsCurl); + instances[TrustedUrlTypes.AVProDesktop] = new TrustedUrlUtils(supportedProtocolsMF); + instances[TrustedUrlTypes.AVProAndroid] = new TrustedUrlUtils(supportedProtocolsExo); + instances[TrustedUrlTypes.ImageUrl] = new TrustedUrlUtils(supportedProtocolsCurl); + instances[TrustedUrlTypes.StringUrl] = new TrustedUrlUtils(supportedProtocolsCurl); + } static GUIContent GetContent(string label, string tooltip = null) { if (tempContent == null) tempContent = new GUIContent(); @@ -34,32 +63,35 @@ static GUIContent GetWarningContent(string tooltip) { return warningContent; } - static async UniTask> GetTrustedUrlsLazy() { - if (trustedUrls == null) { - var vrcsdkConfig = ConfigManager.RemoteConfig; - if (!vrcsdkConfig.IsInitialized()) { - Debug.Log("[VVMW] VRCSDK config is not initialized, initializing..."); - var initState = new UniTaskCompletionSource(); - vrcsdkConfig.Init( - () => initState.TrySetResult(), - () => initState.TrySetException(new Exception("Failed to initialize VRCSDK config.")) - ); - await initState.Task; - } - if (!vrcsdkConfig.HasKey("urlList")) { - Debug.LogWarning("[VVMW] Failed to fetch trusted url list."); - return null; - } - trustedUrls = vrcsdkConfig.GetList("urlList"); + static async UniTask GetTrustedUrlsLazy() { + var vrcsdkConfig = ConfigManager.RemoteConfig; + if (!vrcsdkConfig.IsInitialized()) { + Debug.Log("[VVMW] VRCSDK config is not initialized, initializing..."); + var initState = new UniTaskCompletionSource(); + vrcsdkConfig.Init( + () => initState.TrySetResult(), + () => initState.TrySetException(new Exception("Failed to initialize VRCSDK config.")) + ); + await initState.Task; } - return trustedUrls; + if (vrcsdkConfig.HasKey("urlList")) { + var trustedUrls = vrcsdkConfig.GetList("urlList"); + instances[TrustedUrlTypes.UnityVideo].trustedUrls = trustedUrls; + instances[TrustedUrlTypes.AVProDesktop].trustedUrls = trustedUrls; + instances[TrustedUrlTypes.AVProAndroid].trustedUrls = trustedUrls; + } + if (vrcsdkConfig.HasKey("imageHostUrlList")) + instances[TrustedUrlTypes.ImageUrl].trustedUrls = vrcsdkConfig.GetList("imageHostUrlList"); + if (vrcsdkConfig.HasKey("stringHostUrlList")) + instances[TrustedUrlTypes.StringUrl].trustedUrls = vrcsdkConfig.GetList("stringHostUrlList"); } - public static void CopyTrustedUrlsToStringArray(SerializedProperty stringArray) => - CopyTrustedUrlsToStringArrayAsync(stringArray, true).Forget(); + public static void CopyTrustedUrlsToStringArray(SerializedProperty stringArray, TrustedUrlTypes urlType) => + CopyTrustedUrlsToStringArrayAsync(stringArray, urlType, true).Forget(); - static async UniTask CopyTrustedUrlsToStringArrayAsync(SerializedProperty stringArray, bool applyChanges = true) { - var urlList = await TrustedUrls; + static async UniTask CopyTrustedUrlsToStringArrayAsync(SerializedProperty stringArray, TrustedUrlTypes urlType, bool applyChanges = true) { + await getTrustedUrlsTask.Task; + var urlList = instances[urlType].trustedUrls; stringArray.arraySize = urlList.Count; for (int i = 0; i < urlList.Count; i++) { var url = urlList[i]; @@ -75,28 +107,57 @@ static async UniTask CopyTrustedUrlsToStringArrayAsync(SerializedProperty string UdonSharpEditorUtility.CopyProxyToUdon(usharp); } - public static void DrawUrlField(SerializedProperty urlProperty, params GUILayoutOption[] options) { + public static void DrawUrlField(SerializedProperty urlProperty, TrustedUrlTypes urlType, params GUILayoutOption[] options) { var contentRect = EditorGUILayout.GetControlRect(true, EditorGUIUtility.singleLineHeight, options); - DrawUrlField(urlProperty, contentRect); + DrawUrlField(urlProperty, urlType, contentRect); } - public static void DrawUrlField(SerializedProperty urlProperty, Rect rect) { + public static void DrawUrlField(SerializedProperty urlProperty, TrustedUrlTypes urlTypes, Rect rect) { var content = GetContent(urlProperty.displayName, urlProperty.tooltip); - if (urlProperty.propertyType == SerializedPropertyType.Generic) + if (urlProperty.propertyType == SerializedPropertyType.Generic) // VRCUrl urlProperty = urlProperty.FindPropertyRelative("url"); var url = urlProperty.stringValue; using (new EditorGUI.PropertyScope(rect, content, urlProperty)) - urlProperty.stringValue = DrawUrlField(url, rect, content); + urlProperty.stringValue = DrawUrlField(url, urlTypes, rect, content); } - public static string DrawUrlField(string url, Rect rect, string propertyLabel = null, string propertyTooltip = null) => - DrawUrlField(url, rect, GetContent(propertyLabel, propertyTooltip)); + public static string DrawUrlField(string url, TrustedUrlTypes urlType, Rect rect, string propertyLabel = null, string propertyTooltip = null) => + DrawUrlField(url, urlType, rect, GetContent(propertyLabel, propertyTooltip)); - public static string DrawUrlField(string url, Rect rect, GUIContent content) { + public static string DrawUrlField(string url, TrustedUrlTypes urlType, Rect rect, GUIContent content) { + var instnace = instances[urlType]; + var invalidMessage = instnace.GetValidateMessage(url); + var rect2 = rect; + if (!string.IsNullOrEmpty(invalidMessage)) { + var warnContent = GetWarningContent(invalidMessage); + var labelStyle = EditorStyles.miniLabel; + var warnSize = labelStyle.CalcSize(warnContent); + var warnRect = new Rect(rect2.xMax - warnSize.x, rect2.y, warnSize.x, rect2.height); + rect2.width -= warnSize.x; + GUI.Label(warnRect, warnContent, labelStyle); + } + using (var changed = new EditorGUI.ChangeCheckScope()) { + var newUrl = EditorGUI.TextField(rect2, content, url); + if (changed.changed) { + instnace.messageCache.Remove(url); + url = newUrl; + } + } + return url; + } + + TrustedUrlUtils(HashSet supportedProtocols) { + this.supportedProtocols = supportedProtocols; + } + + string GetValidateMessage(string url) { if (!messageCache.TryGetValue(url, out var invalidMessage)) { invalidMessage = ""; if (Uri.TryCreate(url, UriKind.Absolute, out var uri)) { - if (trustedUrls == null) TrustedUrls.Forget(); // Force to fetch trusted urls. + if (!supportedProtocols.Contains(uri.Scheme)) + invalidMessage = $"{uri.Scheme} is not a supported protocol."; + else if (trustedUrls == null) + getTrustedUrlsTask.Task.Forget(); // Force to fetch trusted urls. else { // Check domains. var domainName = uri.Host; if (!trustedDomains.TryGetValue(domainName, out var trusted)) { @@ -119,23 +180,7 @@ public static string DrawUrlField(string url, Rect rect, GUIContent content) { invalidMessage = "This URL is invalid."; messageCache[url] = invalidMessage; } - var rect2 = rect; - if (!string.IsNullOrEmpty(invalidMessage)) { - var warnContent = GetWarningContent(invalidMessage); - var labelStyle = EditorStyles.miniLabel; - var warnSize = labelStyle.CalcSize(warnContent); - var warnRect = new Rect(rect2.xMax - warnSize.x, rect2.y, warnSize.x, rect2.height); - rect2.width -= warnSize.x; - GUI.Label(warnRect, warnContent, labelStyle); - } - using (var changed = new EditorGUI.ChangeCheckScope()) { - var newUrl = EditorGUI.TextField(rect2, content, url); - if (changed.changed) { - messageCache.Remove(url); - url = newUrl; - } - } - return url; + return invalidMessage; } } } \ No newline at end of file diff --git a/Packages/idv.jlchntoz.vvmw/Editor/VVMW/CoreEditor.cs b/Packages/idv.jlchntoz.vvmw/Editor/VVMW/CoreEditor.cs index aabbac3..34d9c5d 100644 --- a/Packages/idv.jlchntoz.vvmw/Editor/VVMW/CoreEditor.cs +++ b/Packages/idv.jlchntoz.vvmw/Editor/VVMW/CoreEditor.cs @@ -5,7 +5,6 @@ using UnityEngine.SceneManagement; using UnityEditor; using UdonSharpEditor; -using VRC.Core; using VRC.SDK3.Video.Components; using VRC.SDK3.Video.Components.AVPro; @@ -39,6 +38,7 @@ public class CoreEditor : VVMWEditorBase { SerializedProperty avProPropertyNamesProperty; ReorderableListUtils playerHandlersList, audioSourcesList, targetsList; string[] playerNames; + bool[] playerTypes; List screenTargetVisibilityState; bool showTrustUrlList; @@ -77,35 +77,38 @@ protected override void OnEnable() { screenTargetVisibilityState = new List(); for (int i = 0, count = screenTargetsProperty.arraySize; i < count; i++) screenTargetVisibilityState.Add(false); - TrustedUrlUtils.CopyTrustedUrlsToStringArray(trustedUrlDomainsProperty); + TrustedUrlUtils.CopyTrustedUrlsToStringArray(trustedUrlDomainsProperty, TrustedUrlTypes.AVProDesktop); } public override void OnInspectorGUI() { base.OnInspectorGUI(); if (UdonSharpGUI.DrawDefaultUdonSharpBehaviourHeader(target, false, false)) return; serializedObject.Update(); - TrustedUrlUtils.DrawUrlField(defaultUrlProperty); + int autoPlayPlayerType = autoPlayPlayerTypeProperty.intValue - 1; + bool isAvPro = playerTypes != null && autoPlayPlayerType >= 0 && autoPlayPlayerType < playerTypes.Length && playerTypes[autoPlayPlayerType]; + TrustedUrlUtils.DrawUrlField(defaultUrlProperty, isAvPro ? TrustedUrlTypes.AVProDesktop : TrustedUrlTypes.UnityVideo); if (!string.IsNullOrEmpty(defaultUrlProperty.FindPropertyRelative("url").stringValue)) { - TrustedUrlUtils.DrawUrlField(defaultQuestUrlProperty); + TrustedUrlUtils.DrawUrlField(defaultQuestUrlProperty, isAvPro ? TrustedUrlTypes.AVProAndroid : TrustedUrlTypes.UnityVideo); if (playerNames == null || playerNames.Length != playerHandlersProperty.arraySize) playerNames = new string[playerHandlersProperty.arraySize]; + if (playerTypes == null || playerTypes.Length != playerHandlersProperty.arraySize) + playerTypes = new bool[playerHandlersProperty.arraySize]; for (int i = 0; i < playerNames.Length; i++) { var playerHandler = playerHandlersProperty.GetArrayElementAtIndex(i).objectReferenceValue as VideoPlayerHandler; if (playerHandler == null) playerNames[i] = "null"; - else if (string.IsNullOrEmpty(playerHandler.playerName)) - playerNames[i] = playerHandler.name; - else - playerNames[i] = playerHandler.playerName; + else { + playerNames[i] = string.IsNullOrEmpty(playerHandler.playerName) ? playerHandler.name : playerHandler.playerName; + playerTypes[i] = playerHandler.isAvPro; + } } - int selectedIndex = autoPlayPlayerTypeProperty.intValue - 1; var rect = EditorGUILayout.GetControlRect(true, EditorGUIUtility.singleLineHeight); var content = GetTempContent(autoPlayPlayerTypeProperty); using (new EditorGUI.PropertyScope(rect, content, autoPlayPlayerTypeProperty)) using (var changed = new EditorGUI.ChangeCheckScope()) { rect = EditorGUI.PrefixLabel(rect, content); - selectedIndex = EditorGUI.Popup(rect, selectedIndex, playerNames); - if (changed.changed) autoPlayPlayerTypeProperty.intValue = selectedIndex + 1; + autoPlayPlayerType = EditorGUI.Popup(rect, autoPlayPlayerType, playerNames); + if (changed.changed) autoPlayPlayerTypeProperty.intValue = autoPlayPlayerType + 1; } } EditorGUILayout.PropertyField(loopProperty); @@ -143,7 +146,7 @@ public override void OnInspectorGUI() { "The list of trusted URL domains from VRChat. This list is for display proper error message when the video URL is not trusted." ), true); if (GUILayout.Button("Update from VRChat", GUILayout.ExpandWidth(false))) - TrustedUrlUtils.CopyTrustedUrlsToStringArray(trustedUrlDomainsProperty); + TrustedUrlUtils.CopyTrustedUrlsToStringArray(trustedUrlDomainsProperty, TrustedUrlTypes.AVProDesktop); } if (showTrustUrlList) using (new EditorGUILayout.VerticalScope(GUI.skin.box)) @@ -513,7 +516,7 @@ static void AppendElement(SerializedProperty property, int value) { static void UpdateTrustedUrlList() { var cores = new List(SceneManager.GetActiveScene().IterateAllComponents()); using (var so = new SerializedObject(cores.ToArray())) - TrustedUrlUtils.CopyTrustedUrlsToStringArray(so.FindProperty("trustedUrlDomains")); + TrustedUrlUtils.CopyTrustedUrlsToStringArray(so.FindProperty("trustedUrlDomains"), TrustedUrlTypes.AVProDesktop); } } } \ No newline at end of file diff --git a/Packages/idv.jlchntoz.vvmw/Editor/VVMW/PlayListEditorWindow.cs b/Packages/idv.jlchntoz.vvmw/Editor/VVMW/PlayListEditorWindow.cs index b67210e..7e043b3 100644 --- a/Packages/idv.jlchntoz.vvmw/Editor/VVMW/PlayListEditorWindow.cs +++ b/Packages/idv.jlchntoz.vvmw/Editor/VVMW/PlayListEditorWindow.cs @@ -19,6 +19,7 @@ public class PlayListEditorWindow : EditorWindow { FrontendHandler frontendHandler; Core loadedCore; string[] playerHandlerNames; + bool[] playerHandlerTypes; int firstUnityPlayerIndex = -1, firstAvProPlayerIndex = -1; [SerializeField] List playLists = new List(); ReorderableList playListView; @@ -211,13 +212,14 @@ void DrawPlayListEntry(Rect rect, int index, bool isActive, bool isFocused) { } } } + var isAvPro = playerHandlerTypes != null && entry.playerIndex >= 0 && entry.playerIndex < playerHandlerTypes.Length && playerHandlerTypes[entry.playerIndex]; var urlRect = rect; urlRect.yMin = titleRect.yMax + EditorGUIUtility.standardVerticalSpacing; urlRect.height = EditorGUIUtility.singleLineHeight; using (var changed = new EditorGUI.ChangeCheckScope()) { tempContent.text = "URL (PC)"; urlRect = EditorGUI.PrefixLabel(urlRect, tempContent); - var newUrl = TrustedUrlUtils.DrawUrlField(entry.url, urlRect, ""); + var newUrl = TrustedUrlUtils.DrawUrlField(entry.url, isAvPro ? TrustedUrlTypes.AVProDesktop : TrustedUrlTypes.UnityVideo, urlRect, ""); if (changed.changed) { entry.url = newUrl; selectedPlayList.entries[index] = entry; @@ -231,7 +233,7 @@ void DrawPlayListEntry(Rect rect, int index, bool isActive, bool isFocused) { tempContent.text = "URL (Quest)"; urlQuestRect = EditorGUI.PrefixLabel(urlQuestRect, tempContent); var newUrl = string.IsNullOrEmpty(entry.urlForQuest) ? entry.url : entry.urlForQuest; - newUrl = TrustedUrlUtils.DrawUrlField(newUrl, urlQuestRect, ""); + newUrl = TrustedUrlUtils.DrawUrlField(newUrl, isAvPro ? TrustedUrlTypes.AVProAndroid : TrustedUrlTypes.UnityVideo, urlQuestRect, ""); if (changed.changed) { entry.urlForQuest = newUrl == entry.url ? string.Empty : newUrl; selectedPlayList.entries[index] = entry; @@ -379,6 +381,8 @@ void UpdatePlayerHandlerInfos() { var handlersCount = playerHandlersProperty.arraySize; if (playerHandlerNames == null || playerHandlerNames.Length != handlersCount) playerHandlerNames = new string[handlersCount]; + if (playerHandlerTypes == null || playerHandlerTypes.Length != handlersCount) + playerHandlerTypes = new bool[handlersCount]; for (int i = 0; i < handlersCount; i++) { var handler = playerHandlersProperty.GetArrayElementAtIndex(i).objectReferenceValue as VideoPlayerHandler; if (handler == null) { @@ -387,8 +391,10 @@ void UpdatePlayerHandlerInfos() { } playerHandlerNames[i] = string.IsNullOrEmpty(handler.playerName) ? handler.name : handler.playerName; if (handler.isAvPro) { + playerHandlerTypes[i] = true; if (firstAvProPlayerIndex < 0) firstAvProPlayerIndex = i; } else { + playerHandlerTypes[i] = false; if (firstUnityPlayerIndex < 0) firstUnityPlayerIndex = i; } }