Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#if UWB_WEBVIEW && !IMMUTABLE_CUSTOM_BROWSER && (UNITY_STANDALONE_WIN || UNITY_STANDALONE_OSX || UNITY_EDITOR_OSX || (UNITY_ANDROID && UNITY_EDITOR_WIN) || (UNITY_IPHONE && UNITY_EDITOR_WIN))
#if UWB_WEBVIEW && !IMMUTABLE_CUSTOM_BROWSER && (UNITY_STANDALONE_WIN || (UNITY_ANDROID && UNITY_EDITOR_WIN) || (UNITY_IPHONE && UNITY_EDITOR_WIN))

using System;
using System.IO;
Expand Down Expand Up @@ -32,6 +32,7 @@ public class UwbWebView : MonoBehaviour, IWebBrowserClient
#endif

private WebBrowserClient? webBrowserClient;
private GameBridgeServer? gameBridgeServer;

public async UniTask Init(int engineStartupTimeoutMs, bool redactTokensInLogs, Func<string, string> redactionHandler)
{
Expand Down Expand Up @@ -66,8 +67,9 @@ public async UniTask Init(int engineStartupTimeoutMs, bool redactTokensInLogs, F
var browserEngineMainDir = WebBrowserUtils.GetAdditionFilesDirectory();
browserClient.CachePath = new FileInfo(Path.Combine(browserEngineMainDir, "ImmutableSDK/UWBCache"));

// Game bridge path
browserClient.initialUrl = GameBridge.GetFilePath();
// Start local HTTP server to serve index.html
gameBridgeServer = new GameBridgeServer(GameBridge.GetFileSystemPath());
browserClient.initialUrl = gameBridgeServer.Start();

// Set up engine from standard UWB configuration asset
var engineConfigAsset = Resources.Load<EngineConfiguration>("Cef Engine Configuration");
Expand Down Expand Up @@ -145,6 +147,8 @@ public void Dispose()
if (webBrowserClient?.HasDisposed == true) return;

webBrowserClient?.Dispose();
gameBridgeServer?.Dispose();
gameBridgeServer = null;
}
#endif
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,15 @@ public static string GetFilePath()
filePath = filePath.Replace(" ", "%20");
return filePath;
}

/// <summary>
/// Gets the file system path to index.html (without file:// scheme or URL encoding).
/// </summary>
public static string GetFileSystemPath()
{
return GetFilePath()
.Replace(SCHEME_FILE, "")
.Replace("%20", " ");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
#if UNITY_STANDALONE_WIN || (UNITY_ANDROID && UNITY_EDITOR_WIN) || (UNITY_IPHONE && UNITY_EDITOR_WIN)

using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using UnityEngine;

namespace Immutable.Browser.Core
{
/// <summary>
/// Local HTTP server for index.html to provide a proper origin instead of null from file:// URLs.
/// </summary>
public class GameBridgeServer : IDisposable
{
private const string TAG = "[Game Bridge Server]";

// Fixed port to maintain consistent origin for localStorage/IndexedDB persistence
private const int PORT = 51990;
private static readonly string URL = "http://localhost:" + PORT + "/";

private HttpListener? _listener;
private Thread? _listenerThread;
private byte[]? _indexHtmlContent;
private readonly CancellationTokenSource _cts = new CancellationTokenSource();
private bool _disposed;

/// <summary>
/// Creates a new GameBridgeServer instance.
/// </summary>
/// <param name="indexHtmlPath">The file system path to the index.html file.</param>
public GameBridgeServer(string indexHtmlPath)
{
if (string.IsNullOrEmpty(indexHtmlPath))
throw new ArgumentNullException(nameof(indexHtmlPath));

if (!File.Exists(indexHtmlPath))
throw new FileNotFoundException($"{TAG} index.html not found: {indexHtmlPath}");

_indexHtmlContent = File.ReadAllBytes(indexHtmlPath);
Debug.Log($"{TAG} Loaded index.html ({_indexHtmlContent.Length} bytes)");
}

/// <summary>
/// Starts the game bridge server.
/// </summary>
/// <returns>The URL to the index.html file.</returns>
public string Start()
{
if (_disposed)
throw new ObjectDisposedException(nameof(GameBridgeServer));

if (_listener?.IsListening == true)
return URL;

EnsurePortAvailable();

_listener = new HttpListener();
_listener.Prefixes.Add(URL);
_listener.Start();

Debug.Log($"{TAG} Started on {URL}");

_listenerThread = new Thread(ListenerLoop)
{
Name = "GameBridgeServer",
IsBackground = true
};
_listenerThread.Start();

return URL;
}

private void ListenerLoop()
{
while (!_cts.Token.IsCancellationRequested && _listener?.IsListening == true)
{
try
{
var context = _listener.GetContext();
HandleRequest(context);
}
catch (HttpListenerException) when (_cts.Token.IsCancellationRequested)
{
break;
}
catch (ObjectDisposedException)
{
break;
}
catch (Exception ex)
{
if (!_cts.Token.IsCancellationRequested)
Debug.LogError($"{TAG} Error: {ex.Message}");
}
}
}

private void HandleRequest(HttpListenerContext context)
{
var response = context.Response;
try
{
response.StatusCode = 200;
response.ContentType = "text/html; charset=utf-8";
response.ContentLength64 = _indexHtmlContent!.Length;
response.OutputStream.Write(_indexHtmlContent, 0, _indexHtmlContent.Length);
}
catch (Exception ex)
{
Debug.LogError($"{TAG} Error handling request: {ex.Message}");
}
finally
{
try { response.Close(); } catch { }
}
}

private void EnsurePortAvailable()
{
if (!IsPortAvailable(PORT))
{
throw new InvalidOperationException(
$"{TAG} Port {PORT} is already in use. " +
"Please close any application using this port to ensure localStorage/IndexedDB data persists correctly.");
}
}

private bool IsPortAvailable(int port)
{
try
{
var listener = new TcpListener(IPAddress.Loopback, port);
listener.Start();
listener.Stop();
return true;
}
catch
{
return false;
}
}

public void Dispose()
{
if (_disposed) return;
_disposed = true;

_cts.Cancel();
try
{
_listener?.Stop();
_listener?.Close();
}
catch { }

_listenerThread?.Join(TimeSpan.FromSeconds(1));
_cts.Dispose();
_indexHtmlContent = null;

Debug.Log($"{TAG} Stopped");
}
}
}

#endif

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
#if UNITY_STANDALONE_WIN || (UNITY_ANDROID && UNITY_EDITOR_WIN) || (UNITY_IPHONE && UNITY_EDITOR_WIN)

using System.IO;
using System.Net.Sockets;
using UnityEngine;
using Immutable.Browser.Core;
using Immutable.Passport;
using Immutable.Passport.Core.Logging;
using Cysharp.Threading.Tasks;

Expand All @@ -13,6 +14,7 @@ public class WindowsWebBrowserClientAdapter : IWebBrowserClient
public event OnUnityPostMessageDelegate OnUnityPostMessage;

private readonly IWindowsWebBrowserClient webBrowserClient;
private GameBridgeServer? gameBridgeServer;

public WindowsWebBrowserClientAdapter(IWindowsWebBrowserClient windowsWebBrowserClient)
{
Expand All @@ -33,8 +35,11 @@ public async UniTask Init()
// Initialise the web browser client asynchronously
await webBrowserClient.Init();

// Start local HTTP server to serve index.html
gameBridgeServer = new GameBridgeServer(GameBridge.GetFileSystemPath());

// Load the game bridge file into the web browser client
webBrowserClient.LoadUrl(GameBridge.GetFilePath());
webBrowserClient.LoadUrl(gameBridgeServer.Start());

// Get the JavaScript API call for posting messages from the web page to the Unity application
string postMessageApiCall = webBrowserClient.GetPostMessageApiCall();
Expand All @@ -59,6 +64,8 @@ public void LaunchAuthURL(string url, string? redirectUri)
public void Dispose()
{
webBrowserClient.Dispose();
gameBridgeServer?.Dispose();
gameBridgeServer = null;
}
}
}
Expand Down
Loading