From 643529e874e06b018e09b12eea6f468d97da5c65 Mon Sep 17 00:00:00 2001 From: KoalaTrapLord Date: Wed, 18 Mar 2026 15:15:36 -0400 Subject: [PATCH] Add WebGLBridge for WebGL build support, CORS, file input fallback (closes #57) --- .../Assets/Scripts/Services/WebGLBridge.cs | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 unity-client/Assets/Scripts/Services/WebGLBridge.cs diff --git a/unity-client/Assets/Scripts/Services/WebGLBridge.cs b/unity-client/Assets/Scripts/Services/WebGLBridge.cs new file mode 100644 index 0000000..ef17f28 --- /dev/null +++ b/unity-client/Assets/Scripts/Services/WebGLBridge.cs @@ -0,0 +1,106 @@ +using System; +using System.Runtime.InteropServices; +using UnityEngine; + +namespace CommanderAILab.Services +{ + /// + /// WebGL-specific bridge: reads server URL from URL query params, + /// provides JS interop for file input (camera fallback on WebGL), + /// handles browser security restrictions (HTTPS in prod), + /// and loading screen progress. + /// + public class WebGLBridge : MonoBehaviour + { + public static WebGLBridge Instance { get; private set; } + +#if UNITY_WEBGL && !UNITY_EDITOR + [DllImport("__Internal")] private static extern string GetURLParam(string key); + [DllImport("__Internal")] private static extern void OpenFileDialog(string accept, string callbackObject, string callbackMethod); + [DllImport("__Internal")] private static extern void SetLoadingProgress(float progress); +#else + private static string GetURLParam(string key) => ""; + private static void OpenFileDialog(string accept, string obj, string method) { } + private static void SetLoadingProgress(float progress) { } +#endif + + private Action _fileCallback; + + private void Awake() + { + if (Instance != null) { Destroy(gameObject); return; } + Instance = this; + DontDestroyOnLoad(gameObject); + } + + /// + /// Read server URL from ?server= query param. + /// Falls back to http://localhost:8080 if not set. + /// + public string GetServerUrl() + { + string url = ""; +#if UNITY_WEBGL && !UNITY_EDITOR + try { url = GetURLParam("server"); } catch { } +#endif + if (string.IsNullOrEmpty(url)) url = "http://localhost:8080"; + + // Browser security: enforce HTTPS in production + if (IsProductionBuild() && url.StartsWith("http://")) + { + Debug.LogWarning("[WebGLBridge] Production build should use HTTPS. Upgrading URL."); + url = url.Replace("http://", "https://"); + } + return url; + } + + /// + /// Open a file picker dialog (WebGL camera fallback). + /// On WebGL, WebCamTexture is not available so we use + /// an HTML file input element instead. + /// + public void PickImageFile(Action callback) + { + _fileCallback = callback; +#if UNITY_WEBGL && !UNITY_EDITOR + OpenFileDialog("image/*", gameObject.name, "OnFileSelected"); +#else + Debug.LogWarning("[WebGLBridge] File picker is only available in WebGL builds."); +#endif + } + + /// Called from JavaScript when file is selected. + public void OnFileSelected(string base64Data) + { + if (string.IsNullOrEmpty(base64Data)) return; + try + { + byte[] data = Convert.FromBase64String(base64Data); + _fileCallback?.Invoke(data); + } + catch (Exception e) + { + Debug.LogError($"[WebGLBridge] File parse error: {e.Message}"); + } + } + + /// Update the loading screen progress bar (0–1). + public void UpdateLoadingProgress(float progress) + { +#if UNITY_WEBGL && !UNITY_EDITOR + SetLoadingProgress(Mathf.Clamp01(progress)); +#endif + } + + private bool IsProductionBuild() + { +#if UNITY_WEBGL && !UNITY_EDITOR + // Check if running on localhost + string host = Application.absoluteURL; + return !host.Contains("localhost") && !host.Contains("127.0.0.1"); +#else + return false; +#endif + } + } +}