Add namespace

This commit is contained in:
Santiago Lo Coco 2024-11-11 23:19:45 +01:00
parent 34e83ed312
commit d8f5282892
12 changed files with 1066 additions and 1030 deletions

View File

@ -2,35 +2,38 @@ using System.Collections.Generic;
using TMPro;
using UnityEngine;
public class ConfigureNavBar : MonoBehaviour
namespace WebViewStream
{
[SerializeField]
private EndpointLoader endpointLoader;
private bool isVisible = false;
/// <summary>
/// Toggles the visibility of the address field in the nav bar.
/// </summary>
public void ToggleVisibilityMethod()
public class ConfigureNavBar : MonoBehaviour
{
List<GameObject> canvases = endpointLoader.GetInstantiatedItems();
isVisible = !isVisible;
foreach (GameObject canvas in canvases)
{
TMP_InputField inputField = canvas.GetComponentInChildren<TMP_InputField>(true);
if (inputField != null)
{
Debug.Log("Setting address field visibility to " + isVisible);
inputField.gameObject.SetActive(isVisible);
}
[SerializeField]
private EndpointLoader endpointLoader;
BoxCollider boxCollider = canvas.GetComponent<BoxCollider>();
if (boxCollider != null)
private bool isVisible = false;
/// <summary>
/// Toggles the visibility of the address field in the nav bar.
/// </summary>
public void ToggleVisibilityMethod()
{
List<GameObject> canvases = endpointLoader.GetInstantiatedItems();
isVisible = !isVisible;
foreach (GameObject canvas in canvases)
{
boxCollider.size = new Vector3(boxCollider.size.x, isVisible ? 400 : 370, boxCollider.size.z);
boxCollider.center = new Vector3(0, isVisible ? 0 : -16, 0);
TMP_InputField inputField = canvas.GetComponentInChildren<TMP_InputField>(true);
if (inputField != null)
{
Debug.Log("Setting address field visibility to " + isVisible);
inputField.gameObject.SetActive(isVisible);
}
BoxCollider boxCollider = canvas.GetComponent<BoxCollider>();
if (boxCollider != null)
{
boxCollider.size = new Vector3(boxCollider.size.x, isVisible ? 400 : 370, boxCollider.size.z);
boxCollider.center = new Vector3(0, isVisible ? 0 : -16, 0);
}
}
}
}
}
}

View File

@ -2,23 +2,26 @@ using Microsoft.MixedReality.Toolkit;
using Microsoft.MixedReality.Toolkit.SpatialAwareness;
using UnityEngine;
public class ConfigureObservers : MonoBehaviour
namespace WebViewStream
{
private IMixedRealitySpatialAwarenessSystem spatialAwarenessSystem;
private void Start()
public class ConfigureObservers : MonoBehaviour
{
spatialAwarenessSystem =
MixedRealityToolkit.Instance.GetService<IMixedRealitySpatialAwarenessSystem>();
private IMixedRealitySpatialAwarenessSystem spatialAwarenessSystem;
if (spatialAwarenessSystem != null)
private void Start()
{
spatialAwarenessSystem.SuspendObservers();
Debug.Log("Spatial observers suspended");
}
else
{
Debug.LogWarning("SAS is not available");
spatialAwarenessSystem =
MixedRealityToolkit.Instance.GetService<IMixedRealitySpatialAwarenessSystem>();
if (spatialAwarenessSystem != null)
{
spatialAwarenessSystem.SuspendObservers();
Debug.Log("Spatial observers suspended");
}
else
{
Debug.LogWarning("SAS is not available");
}
}
}
}
}

View File

@ -2,85 +2,88 @@ using System.Collections.Generic;
using Microsoft.MixedReality.Toolkit.Utilities.Solvers;
using UnityEngine;
public class ConfigureOrbital : MonoBehaviour
namespace WebViewStream
{
[SerializeField]
private EndpointLoader endpointLoader;
private bool orbitalEnabled = false;
/// <summary>
/// Toggles the orbital behavior (solver) of the canvases.
/// </summary>
public void ToggleOrbital()
public class ConfigureOrbital : MonoBehaviour
{
orbitalEnabled = !orbitalEnabled;
List<GameObject> canvases = endpointLoader.GetInstantiatedItems();
[SerializeField]
private EndpointLoader endpointLoader;
foreach (GameObject canvas in canvases)
private bool orbitalEnabled = false;
/// <summary>
/// Toggles the orbital behavior (solver) of the canvases.
/// </summary>
public void ToggleOrbital()
{
Orbital orbital = canvas.GetComponent<Orbital>();
SolverHandler solverHandler = canvas.GetComponent<SolverHandler>();
orbitalEnabled = !orbitalEnabled;
List<GameObject> canvases = endpointLoader.GetInstantiatedItems();
if (orbital != null && solverHandler != null)
foreach (GameObject canvas in canvases)
{
orbital.enabled = orbitalEnabled;
Orbital orbital = canvas.GetComponent<Orbital>();
SolverHandler solverHandler = canvas.GetComponent<SolverHandler>();
if (orbitalEnabled)
if (orbital != null && solverHandler != null)
{
Vector3 headPosition = Camera.main.transform.position;
Quaternion headRotation = Camera.main.transform.rotation;
Vector3 relativePosition =
Quaternion.Inverse(headRotation) * (orbital.transform.position - headPosition);
orbital.enabled = orbitalEnabled;
orbital.LocalOffset = relativePosition;
if (orbitalEnabled)
{
Vector3 headPosition = Camera.main.transform.position;
Quaternion headRotation = Camera.main.transform.rotation;
Vector3 relativePosition =
Quaternion.Inverse(headRotation) * (orbital.transform.position - headPosition);
solverHandler.UpdateSolvers = true;
}
else
{
solverHandler.UpdateSolvers = false;
orbital.LocalOffset = relativePosition;
solverHandler.UpdateSolvers = true;
}
else
{
solverHandler.UpdateSolvers = false;
}
}
}
}
}
/// <summary>
/// Rotates the canvases to face the user.
/// </summary>
public void RotateCanvasToFaceUser()
{
List<GameObject> canvases = endpointLoader.GetInstantiatedItems();
foreach (GameObject canvas in canvases)
/// <summary>
/// Rotates the canvases to face the user.
/// </summary>
public void RotateCanvasToFaceUser()
{
Vector3 directionToCamera = canvas.transform.position - Camera.main.transform.position;
canvas.transform.rotation = Quaternion.LookRotation(directionToCamera);
List<GameObject> canvases = endpointLoader.GetInstantiatedItems();
foreach (GameObject canvas in canvases)
{
Vector3 directionToCamera = canvas.transform.position - Camera.main.transform.position;
canvas.transform.rotation = Quaternion.LookRotation(directionToCamera);
}
}
/// <summary>
/// Centers the canvases in front of the user.
/// </summary>
public void CenterCanvasesToUser()
{
List<GameObject> canvases = endpointLoader.GetInstantiatedItems();
Vector3 localOffset = new Vector3(-0.4f, 0.1f, 1f);
foreach (GameObject canvas in canvases)
{
Transform cameraTransform = Camera.main.transform;
canvas.transform.position =
cameraTransform.position + cameraTransform.TransformDirection(localOffset);
canvas.transform.rotation = Quaternion.LookRotation(cameraTransform.forward, cameraTransform.up);
localOffset = new Vector3(
localOffset.x + endpointLoader.GetItemWidth(canvas),
localOffset.y,
localOffset.z
);
}
}
}
/// <summary>
/// Centers the canvases in front of the user.
/// </summary>
public void CenterCanvasesToUser()
{
List<GameObject> canvases = endpointLoader.GetInstantiatedItems();
Vector3 localOffset = new Vector3(-0.4f, 0.1f, 1f);
foreach (GameObject canvas in canvases)
{
Transform cameraTransform = Camera.main.transform;
canvas.transform.position =
cameraTransform.position + cameraTransform.TransformDirection(localOffset);
canvas.transform.rotation = Quaternion.LookRotation(cameraTransform.forward, cameraTransform.up);
localOffset = new Vector3(
localOffset.x + endpointLoader.GetItemWidth(canvas),
localOffset.y,
localOffset.z
);
}
}
}
}

View File

@ -1,34 +1,37 @@
using Microsoft.MixedReality.Toolkit.Input;
using UnityEngine;
public class ConfigurePointer : MonoBehaviour
namespace WebViewStream
{
private bool handRayPointerEnabled = true;
/// <summary>
/// Toggles the hand ray pointer on and off.
/// </summary>
public void ToggleHandRayPointer()
public class ConfigurePointer : MonoBehaviour
{
if (handRayPointerEnabled)
private bool handRayPointerEnabled = true;
/// <summary>
/// Toggles the hand ray pointer on and off.
/// </summary>
public void ToggleHandRayPointer()
{
DisableHandRayPointer();
handRayPointerEnabled = false;
if (handRayPointerEnabled)
{
DisableHandRayPointer();
handRayPointerEnabled = false;
}
else
{
EnableHandRayPointer();
handRayPointerEnabled = true;
}
}
else
private void EnableHandRayPointer()
{
EnableHandRayPointer();
handRayPointerEnabled = true;
PointerUtils.SetHandRayPointerBehavior(PointerBehavior.AlwaysOn);
}
private void DisableHandRayPointer()
{
PointerUtils.SetHandRayPointerBehavior(PointerBehavior.AlwaysOff);
}
}
private void EnableHandRayPointer()
{
PointerUtils.SetHandRayPointerBehavior(PointerBehavior.AlwaysOn);
}
private void DisableHandRayPointer()
{
PointerUtils.SetHandRayPointerBehavior(PointerBehavior.AlwaysOff);
}
}
}

View File

@ -2,35 +2,38 @@ using System;
using Microsoft.MixedReality.Toolkit.UI;
using UnityEngine;
public class DialogHandler : MonoBehaviour
namespace WebViewStream
{
[SerializeField]
private GameObject dialogPrefab;
/// <summary>
/// Opens a dialog with a title, question, and action.
/// </summary>
/// <param name="title"></param>
/// <param name="question"></param>
/// <param name="action"></param>
public void OpenDialog(string title, string question, Action action)
public class DialogHandler : MonoBehaviour
{
Dialog dialog = Dialog.Open(
dialogPrefab,
DialogButtonType.Yes | DialogButtonType.No,
title,
question,
true
);
if (dialog != null)
[SerializeField]
private GameObject dialogPrefab;
/// <summary>
/// Opens a dialog with a title, question, and action.
/// </summary>
/// <param name="title"></param>
/// <param name="question"></param>
/// <param name="action"></param>
public void OpenDialog(string title, string question, Action action)
{
dialog.OnClosed += (x) =>
Dialog dialog = Dialog.Open(
dialogPrefab,
DialogButtonType.Yes | DialogButtonType.No,
title,
question,
true
);
if (dialog != null)
{
if (x.Result == DialogButtonType.Yes)
dialog.OnClosed += (x) =>
{
action?.Invoke();
}
};
if (x.Result == DialogButtonType.Yes)
{
action?.Invoke();
}
};
}
}
}
}
}

View File

@ -5,341 +5,344 @@ using Microsoft.MixedReality.WebView;
using UnityEngine;
using UnityEngine.Networking;
public class EndpointLoader : MonoBehaviour
namespace WebViewStream
{
[SerializeField]
private GameObject dynamicItem;
[SerializeField]
private ServiceDiscovery serviceDiscovery;
[SerializeField]
private ServicesListPopulator servicesListPopulator;
[SerializeField]
private DialogHandler dialogHandler;
private string apiUrl;
private bool triedMulticast = false;
private bool defaultEndpointLoaded = false;
private List<GameObject> instantiatedItems = new List<GameObject>();
private HashSet<MdnsService> availableServices = new HashSet<MdnsService>();
private float loadTimeout = 10f;
private bool areItemsVisible = true;
private const string defaultApiUrl = "http://windows.local:5000/api/endpoints";
private const string defaultEndpoint1 = "http://windows.local:8100/mystream/";
private const string defaultEndpoint2 = "http://windows.local:8200/mystream/";
private void Start()
public class EndpointLoader : MonoBehaviour
{
apiUrl = defaultApiUrl;
StartCoroutine(TimeoutFallback());
StartCoroutine(LoadEndpoints());
}
[SerializeField]
private GameObject dynamicItem;
private IEnumerator TimeoutFallback()
{
float timer = 0f;
[SerializeField]
private ServiceDiscovery serviceDiscovery;
while (timer < loadTimeout && availableServices.Count == 0)
[SerializeField]
private ServicesListPopulator servicesListPopulator;
[SerializeField]
private DialogHandler dialogHandler;
private string apiUrl;
private bool triedMulticast = false;
private bool defaultEndpointLoaded = false;
private List<GameObject> instantiatedItems = new List<GameObject>();
private HashSet<MdnsService> availableServices = new HashSet<MdnsService>();
private bool areItemsVisible = true;
private const float loadTimeout = 20f;
private const string defaultApiUrl = "http://windows.local:5000/api/endpoints";
private const string defaultEndpoint1 = "http://windows.local:8100/mystream/";
private const string defaultEndpoint2 = "http://windows.local:8200/mystream/";
private void Start()
{
yield return new WaitForSeconds(1f);
timer += 1f;
apiUrl = defaultApiUrl;
StartCoroutine(TimeoutFallback());
StartCoroutine(LoadEndpoints());
}
if (availableServices.Count == 0)
private IEnumerator TimeoutFallback()
{
Debug.LogWarning("Timeout reached. Loading default endpoints...");
dialogHandler.OpenDialog(
"Timeout reached",
"No services were found within the time limit.\r\n"
+ "Would you like to load the default endpoints now?\r\n"
+ "If you click \"No\", we will continue waiting for mDNS services to appear.",
() =>
{
StartCoroutine(TryLoadingFromDefaultEndpoints());
}
);
float timer = 0f;
while (timer < loadTimeout && availableServices.Count == 0)
{
yield return new WaitForSeconds(1f);
timer += 1f;
}
if (availableServices.Count == 0)
{
Debug.LogWarning("Timeout reached. Loading default endpoints...");
dialogHandler.OpenDialog(
"Timeout reached",
"No services were found within the time limit.\r\n"
+ "Would you like to load the default endpoints now?\r\n"
+ "If you click \"No\", we will continue waiting for mDNS services to appear.",
() =>
{
StartCoroutine(TryLoadingFromDefaultEndpoints());
}
);
}
}
}
private Vector3 CalculateNextPosition()
{
Transform cameraTransform = Camera.main.transform;
Vector3 localOffset = new Vector3(-0.4f, 0.1f, 1f);
if (instantiatedItems.Count == 0)
private Vector3 CalculateNextPosition()
{
Transform cameraTransform = Camera.main.transform;
Vector3 localOffset = new Vector3(-0.4f, 0.1f, 1f);
if (instantiatedItems.Count == 0)
{
return cameraTransform.position + cameraTransform.TransformDirection(localOffset);
}
GameObject lastItem = instantiatedItems[instantiatedItems.Count - 1];
localOffset = new Vector3(localOffset.x + GetItemWidth(lastItem), localOffset.y, localOffset.z);
return cameraTransform.position + cameraTransform.TransformDirection(localOffset);
}
GameObject lastItem = instantiatedItems[instantiatedItems.Count - 1];
localOffset = new Vector3(localOffset.x + GetItemWidth(lastItem), localOffset.y, localOffset.z);
return cameraTransform.position + cameraTransform.TransformDirection(localOffset);
}
/// <summary>
/// Toggles the visibility of the items spawned by this script.
/// </summary>
public void ToggleItemsVisibility()
{
areItemsVisible = !areItemsVisible;
foreach (var item in instantiatedItems)
/// <summary>
/// Toggles the visibility of the items spawned by this script.
/// </summary>
public void ToggleItemsVisibility()
{
item.SetActive(areItemsVisible);
}
}
/// <summary>
/// Returns the width of the item in world space units.
/// </summary>
/// <param name="item"></param>
/// <returns></returns>
public float GetItemWidth(GameObject item)
{
RectTransform rectTransform = item.GetComponent<RectTransform>();
if (rectTransform != null)
{
return rectTransform.rect.width * rectTransform.lossyScale.x + 0.2f;
}
return 0.8f;
}
/// <summary>
/// Spawns a new item with a WebView component and loads the specified URL.
/// </summary>
/// <param name="url"></param>
public void SpawnItem(string url)
{
if (dynamicItem != null)
{
Vector3 nextPosition = CalculateNextPosition();
Transform cameraTransform = Camera.main.transform;
Quaternion rotation = Quaternion.LookRotation(cameraTransform.forward, cameraTransform.up);
GameObject newItem = Instantiate(
dynamicItem,
nextPosition,
rotation,
dynamicItem.transform.parent
);
newItem.SetActive(true);
instantiatedItems.Add(newItem);
var webView = newItem.GetComponentInChildren<WebView>();
if (webView != null)
areItemsVisible = !areItemsVisible;
foreach (var item in instantiatedItems)
{
webView.Load(url);
item.SetActive(areItemsVisible);
}
}
else
/// <summary>
/// Returns the width of the item in world space units.
/// </summary>
/// <param name="item"></param>
/// <returns></returns>
public float GetItemWidth(GameObject item)
{
Debug.LogError("Dynamic item is not assigned.");
}
}
/// <summary>
/// Returns a list of all items instantiated by this script.
/// </summary>
/// <returns></returns>
public List<GameObject> GetInstantiatedItems()
{
return instantiatedItems;
}
private IEnumerator TryLoadingFromDefaultEndpoints()
{
using (UnityWebRequest request = UnityWebRequest.Get(defaultEndpoint1))
{
yield return request.SendWebRequest();
ProcessEndpointResponse(request, defaultEndpoint1, ref defaultEndpointLoaded);
}
using (UnityWebRequest request = UnityWebRequest.Get(defaultEndpoint2))
{
yield return request.SendWebRequest();
ProcessEndpointResponse(request, defaultEndpoint2, ref defaultEndpointLoaded);
}
if (!defaultEndpointLoaded)
{
Debug.LogError("Failed to load default endpoints");
dialogHandler.OpenDialog(
"Failed to load the default endpoints",
"Do you want to try one more time?\r\n"
+ "If you click \"No\", we will continue waiting for mDNS services to appear.",
() =>
{
StartCoroutine(TryLoadingFromDefaultEndpoints());
}
);
}
}
private void ProcessEndpointResponse(UnityWebRequest request, string endpoint, ref bool loadedFlag)
{
if (
request.result == UnityWebRequest.Result.ConnectionError
|| request.result == UnityWebRequest.Result.ProtocolError
)
{
Debug.LogError($"Error loading from {endpoint}: {request.error}");
}
else
{
Debug.Log($"Loaded from {endpoint} successfully.");
SpawnItem(endpoint);
loadedFlag = true;
}
}
private IEnumerator LoadEndpoints()
{
if (!triedMulticast)
{
StartListeningForMulticast();
yield break;
}
if (defaultEndpointLoaded)
{
Debug.Log("Default endpoint already loaded");
yield break;
}
Debug.Log($"Loading endpoints from {apiUrl}");
var request = new UnityWebRequest(apiUrl, UnityWebRequest.kHttpVerbGET);
request.downloadHandler = new DownloadHandlerBuffer();
request.SetRequestHeader("Content-Type", "application/json");
yield return request.SendWebRequest();
if (
request.result == UnityWebRequest.Result.ConnectionError
|| request.result == UnityWebRequest.Result.ProtocolError
)
{
Debug.LogWarning($"Error loading endpoints: {request.error}");
if (triedMulticast)
RectTransform rectTransform = item.GetComponent<RectTransform>();
if (rectTransform != null)
{
Debug.LogError("Multicast also failed");
return rectTransform.rect.width * rectTransform.lossyScale.x + 0.2f;
}
return 0.8f;
}
/// <summary>
/// Spawns a new item with a WebView component and loads the specified URL.
/// </summary>
/// <param name="url"></param>
public void SpawnItem(string url)
{
if (dynamicItem != null)
{
Vector3 nextPosition = CalculateNextPosition();
Transform cameraTransform = Camera.main.transform;
Quaternion rotation = Quaternion.LookRotation(cameraTransform.forward, cameraTransform.up);
GameObject newItem = Instantiate(
dynamicItem,
nextPosition,
rotation,
dynamicItem.transform.parent
);
newItem.SetActive(true);
instantiatedItems.Add(newItem);
var webView = newItem.GetComponentInChildren<WebView>();
if (webView != null)
{
webView.Load(url);
}
}
else
{
Debug.LogError("Dynamic item is not assigned.");
}
}
/// <summary>
/// Returns a list of all items instantiated by this script.
/// </summary>
/// <returns></returns>
public List<GameObject> GetInstantiatedItems()
{
return instantiatedItems;
}
private IEnumerator TryLoadingFromDefaultEndpoints()
{
using (UnityWebRequest request = UnityWebRequest.Get(defaultEndpoint1))
{
yield return request.SendWebRequest();
ProcessEndpointResponse(request, defaultEndpoint1, ref defaultEndpointLoaded);
}
using (UnityWebRequest request = UnityWebRequest.Get(defaultEndpoint2))
{
yield return request.SendWebRequest();
ProcessEndpointResponse(request, defaultEndpoint2, ref defaultEndpointLoaded);
}
if (!defaultEndpointLoaded)
{
Debug.LogError("Failed to load default endpoints");
dialogHandler.OpenDialog(
"Failed to load the default endpoints",
"Do you want to try one more time?\r\n"
+ "If you click \"No\", we will continue waiting for mDNS services to appear.",
() =>
{
StartCoroutine(TryLoadingFromDefaultEndpoints());
}
);
}
}
private void ProcessEndpointResponse(UnityWebRequest request, string endpoint, ref bool loadedFlag)
{
if (
request.result == UnityWebRequest.Result.ConnectionError
|| request.result == UnityWebRequest.Result.ProtocolError
)
{
Debug.LogError($"Error loading from {endpoint}: {request.error}");
}
else
{
Debug.Log($"Loaded from {endpoint} successfully.");
SpawnItem(endpoint);
loadedFlag = true;
}
}
private IEnumerator LoadEndpoints()
{
if (!triedMulticast)
{
StartListeningForMulticast();
yield break;
}
Debug.LogWarning("Trying to load from default endpoints");
yield return StartCoroutine(TryLoadingFromDefaultEndpoints());
}
if (defaultEndpointLoaded)
{
Debug.Log("At least one default endpoint loaded successfully");
yield break;
}
var json = request.downloadHandler.text;
json = "{\"Items\":" + json + "}";
Debug.Log($"Received JSON: {json}");
Endpoint[] endpoints = JsonHelper.FromJson<Endpoint>(json);
if (endpoints.Length == 0)
{
Debug.LogError("Parsed endpoints are empty");
}
else
{
if (instantiatedItems.Count > 0)
if (defaultEndpointLoaded)
{
foreach (var item in instantiatedItems)
{
Destroy(item);
}
instantiatedItems.Clear();
Debug.Log("Default endpoint already loaded");
yield break;
}
foreach (var endpoint in endpoints)
Debug.Log($"Loading endpoints from {apiUrl}");
var request = new UnityWebRequest(apiUrl, UnityWebRequest.kHttpVerbGET);
request.downloadHandler = new DownloadHandlerBuffer();
request.SetRequestHeader("Content-Type", "application/json");
yield return request.SendWebRequest();
if (
request.result == UnityWebRequest.Result.ConnectionError
|| request.result == UnityWebRequest.Result.ProtocolError
)
{
if (endpoint.url == null || endpoint.url.Length == 0)
Debug.LogWarning($"Error loading endpoints: {request.error}");
if (triedMulticast)
{
Debug.LogWarning($"Endpoint URL is null for endpoint");
continue;
Debug.LogError("Multicast also failed");
yield break;
}
SpawnItem(endpoint.url);
Debug.LogWarning("Trying to load from default endpoints");
yield return StartCoroutine(TryLoadingFromDefaultEndpoints());
}
}
}
private void StartListeningForMulticast()
{
Debug.Log("Starting multicast discovery for endpoints");
triedMulticast = true;
serviceDiscovery.StartListening(
(service) =>
if (defaultEndpointLoaded)
{
bool wasAdded = availableServices.Add(service);
if (wasAdded)
Debug.Log("At least one default endpoint loaded successfully");
yield break;
}
var json = request.downloadHandler.text;
json = "{\"Items\":" + json + "}";
Debug.Log($"Received JSON: {json}");
Endpoint[] endpoints = JsonHelper.FromJson<Endpoint>(json);
if (endpoints.Length == 0)
{
Debug.LogError("Parsed endpoints are empty");
}
else
{
if (instantiatedItems.Count > 0)
{
AddServiceToTable(service);
foreach (var item in instantiatedItems)
{
Destroy(item);
}
instantiatedItems.Clear();
}
foreach (var endpoint in endpoints)
{
if (endpoint.url == null || endpoint.url.Length == 0)
{
Debug.LogWarning($"Endpoint URL is null for endpoint");
continue;
}
SpawnItem(endpoint.url);
}
}
);
}
}
private void AddServiceToTable(MdnsService service)
{
servicesListPopulator.AddItemFromService(
service,
() =>
{
apiUrl = $"http://{service.IpAddress}:{service.Port}{service.Path}";
StartCoroutine(LoadEndpoints());
}
);
}
/// <summary>
/// Clears the list of available services.
/// </summary>
public void ClearServices()
{
availableServices.Clear();
servicesListPopulator.RemoveAllItems();
}
/// <summary>
/// Reloads the list of available services.
/// </summary>
public void ReloadEndpoints()
{
triedMulticast = false;
StartCoroutine(LoadEndpoints());
}
[Serializable]
public class Endpoint
{
public int id;
public string url;
}
public static class JsonHelper
{
public static T[] FromJson<T>(string json)
private void StartListeningForMulticast()
{
Wrapper<T> wrapper = JsonUtility.FromJson<Wrapper<T>>(json);
return wrapper.Items;
Debug.Log("Starting multicast discovery for endpoints");
triedMulticast = true;
serviceDiscovery.StartListening(
(service) =>
{
bool wasAdded = availableServices.Add(service);
if (wasAdded)
{
AddServiceToTable(service);
}
}
);
}
private void AddServiceToTable(MdnsService service)
{
servicesListPopulator.AddItemFromService(
service,
() =>
{
apiUrl = $"http://{service.IpAddress}:{service.Port}{service.Path}";
StartCoroutine(LoadEndpoints());
}
);
}
/// <summary>
/// Clears the list of available services.
/// </summary>
public void ClearServices()
{
availableServices.Clear();
servicesListPopulator.RemoveAllItems();
}
/// <summary>
/// Reloads the list of available services.
/// </summary>
public void ReloadEndpoints()
{
triedMulticast = false;
StartCoroutine(LoadEndpoints());
}
[Serializable]
private class Wrapper<T>
public class Endpoint
{
public T[] Items;
public int id;
public string url;
}
public static class JsonHelper
{
public static T[] FromJson<T>(string json)
{
Wrapper<T> wrapper = JsonUtility.FromJson<Wrapper<T>>(json);
return wrapper.Items;
}
[Serializable]
private class Wrapper<T>
{
public T[] Items;
}
}
}
}
}

View File

@ -1,46 +1,49 @@
public class MdnsService
namespace WebViewStream
{
public string IpAddress { get; }
public int Port { get; }
public string Path { get; }
public string Host { get; }
/// <summary>
/// Represents a service discovered via mDNS.
/// </summary>
/// <param name="ipAddress"></param>
/// <param name="port"></param>
/// <param name="path"></param>
/// <param name="host"></param>
public MdnsService(string ipAddress, int port, string path, string host)
public class MdnsService
{
IpAddress = ipAddress;
Port = port;
Path = path;
Host = host;
}
public string IpAddress { get; }
public int Port { get; }
public string Path { get; }
public string Host { get; }
public override string ToString()
{
return $"IpAddress: {IpAddress}, Port: {Port}, Path: {Path}, Host: {Host}";
}
/// <summary>
/// Represents a service discovered via mDNS.
/// </summary>
/// <param name="ipAddress"></param>
/// <param name="port"></param>
/// <param name="path"></param>
/// <param name="host"></param>
public MdnsService(string ipAddress, int port, string path, string host)
{
IpAddress = ipAddress;
Port = port;
Path = path;
Host = host;
}
public override bool Equals(object obj)
{
return obj is MdnsService service
&& IpAddress == service.IpAddress
&& Host == service.Host
&& Port == service.Port
&& Path == service.Path;
}
public override string ToString()
{
return $"IpAddress: {IpAddress}, Port: {Port}, Path: {Path}, Host: {Host}";
}
public override int GetHashCode()
{
int hash = 17;
hash = hash * 31 + (IpAddress?.GetHashCode() ?? 0);
hash = hash * 31 + Port.GetHashCode();
hash = hash * 31 + (Path?.GetHashCode() ?? 0);
hash = hash * 31 + (Host?.GetHashCode() ?? 0);
return hash;
public override bool Equals(object obj)
{
return obj is MdnsService service
&& IpAddress == service.IpAddress
&& Host == service.Host
&& Port == service.Port
&& Path == service.Path;
}
public override int GetHashCode()
{
int hash = 17;
hash = hash * 31 + (IpAddress?.GetHashCode() ?? 0);
hash = hash * 31 + Port.GetHashCode();
hash = hash * 31 + (Path?.GetHashCode() ?? 0);
hash = hash * 31 + (Host?.GetHashCode() ?? 0);
return hash;
}
}
}
}

View File

@ -1,23 +1,26 @@
using Microsoft.MixedReality.Toolkit.UI;
using UnityEngine;
public class ScrollablePagination : MonoBehaviour
namespace WebViewStream
{
[SerializeField]
private ScrollingObjectCollection scrollView;
/// <summary>
/// Scrolls the collection by a specified amount.
/// </summary>
/// <param name="amount"></param>
public void ScrollByTier(int amount)
public class ScrollablePagination : MonoBehaviour
{
if (scrollView == null)
{
Debug.LogError("ScrollingObjectCollection is not set.");
return;
}
[SerializeField]
private ScrollingObjectCollection scrollView;
scrollView.MoveByTiers(amount);
/// <summary>
/// Scrolls the collection by a specified amount.
/// </summary>
/// <param name="amount"></param>
public void ScrollByTier(int amount)
{
if (scrollView == null)
{
Debug.LogError("ScrollingObjectCollection is not set.");
return;
}
scrollView.MoveByTiers(amount);
}
}
}
}

View File

@ -6,367 +6,370 @@ using System.Net.Sockets;
using System.Text;
using UnityEngine;
public class ServiceDiscovery : MonoBehaviour
namespace WebViewStream
{
private UdpClient udpClient;
private Action<MdnsService> action;
private string receivedIp;
private string receivedPort;
private string receivedPath;
private string receivedHost;
private IPAddress defaultIP;
private const string multicastAddress = "224.0.0.251";
private const int multicastPort = 5353;
private Queue<MdnsService> serviceQueue = new Queue<MdnsService>();
private IPAddress GetDefaultInterfaceIP()
public class ServiceDiscovery : MonoBehaviour
{
foreach (NetworkInterface ni in NetworkInterface.GetAllNetworkInterfaces())
private UdpClient udpClient;
private Action<MdnsService> action;
private string receivedIp;
private string receivedPort;
private string receivedPath;
private string receivedHost;
private IPAddress defaultIP;
private const string multicastAddress = "224.0.0.251";
private const int multicastPort = 5353;
private Queue<MdnsService> serviceQueue = new Queue<MdnsService>();
private IPAddress GetDefaultInterfaceIP()
{
if (ni.OperationalStatus == OperationalStatus.Up)
foreach (NetworkInterface ni in NetworkInterface.GetAllNetworkInterfaces())
{
var ipProps = ni.GetIPProperties();
if (ipProps.GatewayAddresses.Count > 0)
if (ni.OperationalStatus == OperationalStatus.Up)
{
foreach (UnicastIPAddressInformation ip in ipProps.UnicastAddresses)
var ipProps = ni.GetIPProperties();
if (ipProps.GatewayAddresses.Count > 0)
{
if (ip.Address.AddressFamily == AddressFamily.InterNetwork)
foreach (UnicastIPAddressInformation ip in ipProps.UnicastAddresses)
{
return ip.Address;
if (ip.Address.AddressFamily == AddressFamily.InterNetwork)
{
return ip.Address;
}
}
}
}
}
return null;
}
return null;
}
private List<IPAddress> GetRoutableLocalIPs()
{
List<IPAddress> localIPs = new List<IPAddress>();
foreach (IPAddress local in Dns.GetHostEntry(Dns.GetHostName()).AddressList)
private List<IPAddress> GetRoutableLocalIPs()
{
if (local.AddressFamily == AddressFamily.InterNetwork)
List<IPAddress> localIPs = new List<IPAddress>();
foreach (IPAddress local in Dns.GetHostEntry(Dns.GetHostName()).AddressList)
{
byte[] bytes = local.GetAddressBytes();
if (bytes[0] == 169 && bytes[1] == 254)
if (local.AddressFamily == AddressFamily.InterNetwork)
{
Debug.Log($"Skipping non-routable address: {local}");
continue;
byte[] bytes = local.GetAddressBytes();
if (bytes[0] == 169 && bytes[1] == 254)
{
Debug.Log($"Skipping non-routable address: {local}");
continue;
}
localIPs.Add(local);
}
}
return localIPs;
}
/// <summary>
/// Starts listening for mDNS service announcements.
/// </summary>
/// <param name="action"></param>
public void StartListening(Action<MdnsService> action)
{
try
{
defaultIP = GetDefaultInterfaceIP();
if (defaultIP == null)
{
Debug.LogError("No default interface found. Cannot start multicast listener.");
return;
}
localIPs.Add(local);
udpClient = new UdpClient();
udpClient.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
udpClient.Client.Bind(new IPEndPoint(defaultIP, multicastPort));
udpClient.JoinMulticastGroup(IPAddress.Parse(multicastAddress), defaultIP);
this.action = action;
Debug.Log("Listening for service announcements...");
SendMdnsQuery("_http._tcp.local");
udpClient.BeginReceive(OnReceive, null);
}
}
return localIPs;
}
/// <summary>
/// Starts listening for mDNS service announcements.
/// </summary>
/// <param name="action"></param>
public void StartListening(Action<MdnsService> action)
{
try
{
defaultIP = GetDefaultInterfaceIP();
if (defaultIP == null)
catch (Exception ex)
{
Debug.LogError("No default interface found. Cannot start multicast listener.");
return;
Debug.LogError($"Error starting UDP listener: {ex.Message}");
}
udpClient = new UdpClient();
udpClient.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
udpClient.Client.Bind(new IPEndPoint(defaultIP, multicastPort));
udpClient.JoinMulticastGroup(IPAddress.Parse(multicastAddress), defaultIP);
this.action = action;
Debug.Log("Listening for service announcements...");
SendMdnsQuery("_http._tcp.local");
udpClient.BeginReceive(OnReceive, null);
}
catch (Exception ex)
{
Debug.LogError($"Error starting UDP listener: {ex.Message}");
}
}
private void SendMdnsQuery(string serviceName)
{
byte[] query = CreateMdnsQuery(serviceName);
Debug.Log($"Sending mDNS query for {serviceName}");
udpClient.Send(query, query.Length, new IPEndPoint(IPAddress.Parse(multicastAddress), multicastPort));
}
private byte[] CreateMdnsQuery(string serviceName)
{
ushort transactionId = 0;
ushort flags = 0x0100;
ushort questions = 1;
byte[] header = new byte[12];
Array.Copy(
BitConverter.GetBytes((ushort)IPAddress.HostToNetworkOrder((short)transactionId)),
0,
header,
0,
2
);
Array.Copy(
BitConverter.GetBytes((ushort)IPAddress.HostToNetworkOrder((short)flags)),
0,
header,
2,
2
);
Array.Copy(
BitConverter.GetBytes((ushort)IPAddress.HostToNetworkOrder((short)questions)),
0,
header,
4,
2
);
byte[] name = EncodeName(serviceName);
byte[] query = new byte[header.Length + name.Length + 4];
Array.Copy(header, query, header.Length);
Array.Copy(name, 0, query, header.Length, name.Length);
query[query.Length - 4] = 0x00;
query[query.Length - 3] = 0x0C;
query[query.Length - 2] = 0x00;
query[query.Length - 1] = 0x01;
return query;
}
private byte[] EncodeName(string name)
{
string[] parts = name.Split('.');
byte[] result = new byte[name.Length + 2];
int offset = 0;
foreach (string part in parts)
{
result[offset++] = (byte)part.Length;
Array.Copy(Encoding.UTF8.GetBytes(part), 0, result, offset, part.Length);
offset += part.Length;
}
result[offset] = 0;
return result;
}
private void OnReceive(IAsyncResult result)
{
if (udpClient == null)
private void SendMdnsQuery(string serviceName)
{
return;
byte[] query = CreateMdnsQuery(serviceName);
Debug.Log($"Sending mDNS query for {serviceName}");
udpClient.Send(query, query.Length, new IPEndPoint(IPAddress.Parse(multicastAddress), multicastPort));
}
try
private byte[] CreateMdnsQuery(string serviceName)
{
IPEndPoint remoteEndPoint = new IPEndPoint(IPAddress.Any, multicastPort);
byte[] receivedBytes = udpClient.EndReceive(result, ref remoteEndPoint);
ushort flags = BitConverter.ToUInt16(new byte[] { receivedBytes[3], receivedBytes[2] }, 0);
if (flags == 0x0100)
{
udpClient?.BeginReceive(OnReceive, null);
return;
}
ParseMdnsResponse(receivedBytes);
udpClient?.BeginReceive(OnReceive, null);
}
catch (Exception ex)
{
Debug.LogError($"Error receiving UDP message: {ex.Message}");
}
}
private void AddMdnsService()
{
if (receivedIp != null && receivedPort != null && receivedHost != null && receivedPath != null)
{
MdnsService currentService = new MdnsService(
receivedIp,
int.Parse(receivedPort),
receivedPath,
receivedHost
ushort transactionId = 0;
ushort flags = 0x0100;
ushort questions = 1;
byte[] header = new byte[12];
Array.Copy(
BitConverter.GetBytes((ushort)IPAddress.HostToNetworkOrder((short)transactionId)),
0,
header,
0,
2
);
Array.Copy(
BitConverter.GetBytes((ushort)IPAddress.HostToNetworkOrder((short)flags)),
0,
header,
2,
2
);
Array.Copy(
BitConverter.GetBytes((ushort)IPAddress.HostToNetworkOrder((short)questions)),
0,
header,
4,
2
);
serviceQueue.Enqueue(currentService);
Debug.Log($"Added service: {currentService}");
receivedIp = null;
receivedPort = null;
receivedPath = null;
receivedHost = null;
}
}
private void ParseMdnsResponse(byte[] data)
{
int offset = 12;
ushort questions = (ushort)IPAddress.NetworkToHostOrder(BitConverter.ToInt16(data, 4));
ushort answerRRs = (ushort)IPAddress.NetworkToHostOrder(BitConverter.ToInt16(data, 6));
ushort additionalRRs = (ushort)IPAddress.NetworkToHostOrder(BitConverter.ToInt16(data, 10));
byte[] name = EncodeName(serviceName);
byte[] query = new byte[header.Length + name.Length + 4];
Array.Copy(header, query, header.Length);
Array.Copy(name, 0, query, header.Length, name.Length);
for (int i = 0; i < questions; i++)
{
offset = SkipName(data, offset);
offset += 4;
query[query.Length - 4] = 0x00;
query[query.Length - 3] = 0x0C;
query[query.Length - 2] = 0x00;
query[query.Length - 1] = 0x01;
return query;
}
for (int i = 0; i < answerRRs; i++)
private byte[] EncodeName(string name)
{
offset = ParseRecord(data, offset);
}
string[] parts = name.Split('.');
byte[] result = new byte[name.Length + 2];
int offset = 0;
for (int i = 0; i < additionalRRs; i++)
{
offset = ParseRecord(data, offset);
AddMdnsService();
}
}
private int ParseRecord(byte[] data, int offset)
{
string name;
(name, offset) = ReadName(data, offset);
ushort recordType = (ushort)IPAddress.NetworkToHostOrder(BitConverter.ToInt16(data, offset));
ushort recordClass = (ushort)IPAddress.NetworkToHostOrder(BitConverter.ToInt16(data, offset + 2));
uint ttl = (uint)IPAddress.NetworkToHostOrder(BitConverter.ToInt32(data, offset + 4));
ushort dataLength = (ushort)IPAddress.NetworkToHostOrder(BitConverter.ToInt16(data, offset + 8));
offset += 10;
if (ttl == 0)
{
Debug.LogWarning($"Zero TTL for {name}");
return offset + dataLength;
}
if (recordType == 1) // A Record
{
IPAddress ipAddress = new IPAddress(new ArraySegment<byte>(data, offset, dataLength).ToArray());
receivedIp = ipAddress.ToString();
receivedHost = name;
}
else if (recordType == 12) // PTR Record
{
string target;
(target, _) = ReadName(data, offset);
}
else if (recordType == 33) // SRV Record
{
ushort priority = (ushort)IPAddress.NetworkToHostOrder(BitConverter.ToInt16(data, offset));
ushort weight = (ushort)IPAddress.NetworkToHostOrder(BitConverter.ToInt16(data, offset + 2));
ushort port = (ushort)IPAddress.NetworkToHostOrder(BitConverter.ToInt16(data, offset + 4));
string target;
(target, _) = ReadName(data, offset + 6);
receivedPort = port.ToString();
}
else if (recordType == 16) // TXT Record
{
string txtData = Encoding.UTF8.GetString(data, offset, dataLength);
if (txtData.Contains("path"))
foreach (string part in parts)
{
receivedPath = txtData.Split('=')[1];
result[offset++] = (byte)part.Length;
Array.Copy(Encoding.UTF8.GetBytes(part), 0, result, offset, part.Length);
offset += part.Length;
}
result[offset] = 0;
return result;
}
private void OnReceive(IAsyncResult result)
{
if (udpClient == null)
{
return;
}
try
{
IPEndPoint remoteEndPoint = new IPEndPoint(IPAddress.Any, multicastPort);
byte[] receivedBytes = udpClient.EndReceive(result, ref remoteEndPoint);
ushort flags = BitConverter.ToUInt16(new byte[] { receivedBytes[3], receivedBytes[2] }, 0);
if (flags == 0x0100)
{
udpClient?.BeginReceive(OnReceive, null);
return;
}
ParseMdnsResponse(receivedBytes);
udpClient?.BeginReceive(OnReceive, null);
}
catch (Exception ex)
{
Debug.LogError($"Error receiving UDP message: {ex.Message}");
}
}
else if (recordType == 47) // NSEC Record
{
// Debug.Log($"NSEC Record: {name}");
}
else
{
Debug.Log($"Unknown Record Type {recordType} for {name}");
}
return offset + dataLength;
}
private (string, int) ReadName(byte[] data, int offset)
{
StringBuilder name = new StringBuilder();
int originalOffset = offset;
bool jumped = false;
while (data[offset] != 0)
private void AddMdnsService()
{
if ((data[offset] & 0xC0) == 0xC0)
if (receivedIp != null && receivedPort != null && receivedHost != null && receivedPath != null)
{
if (!jumped)
MdnsService currentService = new MdnsService(
receivedIp,
int.Parse(receivedPort),
receivedPath,
receivedHost
);
serviceQueue.Enqueue(currentService);
Debug.Log($"Added service: {currentService}");
receivedIp = null;
receivedPort = null;
receivedPath = null;
receivedHost = null;
}
}
private void ParseMdnsResponse(byte[] data)
{
int offset = 12;
ushort questions = (ushort)IPAddress.NetworkToHostOrder(BitConverter.ToInt16(data, 4));
ushort answerRRs = (ushort)IPAddress.NetworkToHostOrder(BitConverter.ToInt16(data, 6));
ushort additionalRRs = (ushort)IPAddress.NetworkToHostOrder(BitConverter.ToInt16(data, 10));
for (int i = 0; i < questions; i++)
{
offset = SkipName(data, offset);
offset += 4;
}
for (int i = 0; i < answerRRs; i++)
{
offset = ParseRecord(data, offset);
}
for (int i = 0; i < additionalRRs; i++)
{
offset = ParseRecord(data, offset);
AddMdnsService();
}
}
private int ParseRecord(byte[] data, int offset)
{
string name;
(name, offset) = ReadName(data, offset);
ushort recordType = (ushort)IPAddress.NetworkToHostOrder(BitConverter.ToInt16(data, offset));
ushort recordClass = (ushort)IPAddress.NetworkToHostOrder(BitConverter.ToInt16(data, offset + 2));
uint ttl = (uint)IPAddress.NetworkToHostOrder(BitConverter.ToInt32(data, offset + 4));
ushort dataLength = (ushort)IPAddress.NetworkToHostOrder(BitConverter.ToInt16(data, offset + 8));
offset += 10;
if (ttl == 0)
{
Debug.LogWarning($"Zero TTL for {name}");
return offset + dataLength;
}
if (recordType == 1) // A Record
{
IPAddress ipAddress = new IPAddress(new ArraySegment<byte>(data, offset, dataLength).ToArray());
receivedIp = ipAddress.ToString();
receivedHost = name;
}
else if (recordType == 12) // PTR Record
{
string target;
(target, _) = ReadName(data, offset);
}
else if (recordType == 33) // SRV Record
{
ushort priority = (ushort)IPAddress.NetworkToHostOrder(BitConverter.ToInt16(data, offset));
ushort weight = (ushort)IPAddress.NetworkToHostOrder(BitConverter.ToInt16(data, offset + 2));
ushort port = (ushort)IPAddress.NetworkToHostOrder(BitConverter.ToInt16(data, offset + 4));
string target;
(target, _) = ReadName(data, offset + 6);
receivedPort = port.ToString();
}
else if (recordType == 16) // TXT Record
{
string txtData = Encoding.UTF8.GetString(data, offset, dataLength);
if (txtData.Contains("path"))
{
originalOffset = offset + 2;
receivedPath = txtData.Split('=')[1];
}
offset = ((data[offset] & 0x3F) << 8) | data[offset + 1];
jumped = true;
}
else if (recordType == 47) // NSEC Record
{
// Debug.Log($"NSEC Record: {name}");
}
else
{
int length = data[offset++];
name.Append(Encoding.UTF8.GetString(data, offset, length) + ".");
offset += length;
Debug.Log($"Unknown Record Type {recordType} for {name}");
}
return offset + dataLength;
}
return (name.ToString().TrimEnd('.'), jumped ? originalOffset : offset + 1);
}
private int SkipName(byte[] data, int offset)
{
while (data[offset] != 0)
private (string, int) ReadName(byte[] data, int offset)
{
if ((data[offset] & 0xC0) == 0xC0)
{
return offset + 2;
}
offset += data[offset] + 1;
}
return offset + 1;
}
StringBuilder name = new StringBuilder();
int originalOffset = offset;
bool jumped = false;
private void Update()
{
if (serviceQueue.Count > 0)
{
Debug.Log($"Queued services: {serviceQueue.Count}");
while (serviceQueue.Count > 0)
while (data[offset] != 0)
{
MdnsService service = serviceQueue.Dequeue();
if (service == null)
if ((data[offset] & 0xC0) == 0xC0)
{
continue;
if (!jumped)
{
originalOffset = offset + 2;
}
offset = (data[offset] & 0x3F) << 8 | data[offset + 1];
jumped = true;
}
else
{
int length = data[offset++];
name.Append(Encoding.UTF8.GetString(data, offset, length) + ".");
offset += length;
}
}
return (name.ToString().TrimEnd('.'), jumped ? originalOffset : offset + 1);
}
private int SkipName(byte[] data, int offset)
{
while (data[offset] != 0)
{
if ((data[offset] & 0xC0) == 0xC0)
{
return offset + 2;
}
offset += data[offset] + 1;
}
return offset + 1;
}
private void Update()
{
if (serviceQueue.Count > 0)
{
Debug.Log($"Queued services: {serviceQueue.Count}");
while (serviceQueue.Count > 0)
{
MdnsService service = serviceQueue.Dequeue();
if (service == null)
{
continue;
}
Debug.Log($"Invoking action with: {service}");
action?.Invoke(service);
}
Debug.Log($"Invoking action with: {service}");
action?.Invoke(service);
}
}
}
private void OnDestroy()
{
StopListening();
}
private void OnDestroy()
{
StopListening();
}
private void StopListening()
{
udpClient?.DropMulticastGroup(IPAddress.Parse(multicastAddress));
udpClient?.Close();
udpClient = null;
private void StopListening()
{
udpClient?.DropMulticastGroup(IPAddress.Parse(multicastAddress));
udpClient?.Close();
udpClient = null;
}
}
}
}

View File

@ -4,100 +4,103 @@ using Microsoft.MixedReality.Toolkit.Utilities;
using TMPro;
using UnityEngine;
public class ServicesListPopulator : MonoBehaviour
namespace WebViewStream
{
[SerializeField]
private ScrollingObjectCollection scrollView;
[SerializeField]
private GameObject dynamicItem;
[SerializeField]
private GridObjectCollection gridObjectCollection;
private bool isVisible = true;
private const string apiUrlPrefix = "API URL: ";
private const string hostPrefix = "Host: ";
/// <summary>
/// Adds an item to the table from a service.
/// </summary>
/// <param name="service"></param>
/// <param name="action"></param>
public void AddItemFromService(MdnsService service, Action action)
public class ServicesListPopulator : MonoBehaviour
{
GameObject itemInstance = Instantiate(dynamicItem, gridObjectCollection.transform);
itemInstance.SetActive(true);
[SerializeField]
private ScrollingObjectCollection scrollView;
Debug.Log($"Adding service to table: {service}");
TextMeshPro[] textMeshes = itemInstance.GetComponentsInChildren<TextMeshPro>();
if (textMeshes.Length < 2)
[SerializeField]
private GameObject dynamicItem;
[SerializeField]
private GridObjectCollection gridObjectCollection;
private bool isVisible = true;
private const string apiUrlPrefix = "API URL: ";
private const string hostPrefix = "Host: ";
/// <summary>
/// Adds an item to the table from a service.
/// </summary>
/// <param name="service"></param>
/// <param name="action"></param>
public void AddItemFromService(MdnsService service, Action action)
{
Debug.LogError("Not enough text meshes found in dynamic item");
return;
}
textMeshes[0].text = $"{apiUrlPrefix}http://{service.IpAddress}:{service.Port}{service.Path}";
textMeshes[1].text = $"{hostPrefix}{service.Host}";
itemInstance
.GetComponentInChildren<Interactable>()
.OnClick.AddListener(() =>
GameObject itemInstance = Instantiate(dynamicItem, gridObjectCollection.transform);
itemInstance.SetActive(true);
Debug.Log($"Adding service to table: {service}");
TextMeshPro[] textMeshes = itemInstance.GetComponentsInChildren<TextMeshPro>();
if (textMeshes.Length < 2)
{
Debug.Log($"Clicked on service: {service.Host}");
action.Invoke();
ToggleVisibility();
});
gridObjectCollection.UpdateCollection();
scrollView.UpdateContent();
}
/// <summary>
/// Removes all items from the table.
/// </summary>
public void RemoveAllItems()
{
foreach (Transform child in gridObjectCollection.transform)
{
Destroy(child.gameObject);
}
Debug.Log("Removed all services from table");
gridObjectCollection.UpdateCollection();
scrollView.UpdateContent();
}
/// <summary>
/// Removes an item from the table by service.
/// </summary>
/// <param name="service"></param>
public void RemoveItemByService(MdnsService service)
{
string apiUrl = $"{apiUrlPrefix}http://{service.IpAddress}:{service.Port}{service.Path}";
string hostname = $"{hostPrefix}{service.Host}";
foreach (Transform child in gridObjectCollection.transform)
{
TextMeshPro[] textMeshes = child.GetComponentsInChildren<TextMeshPro>();
if (textMeshes.Length >= 2 && textMeshes[0].text == apiUrl && textMeshes[1].text == hostname)
{
Debug.Log($"Removing service from table: {service}");
Destroy(child.gameObject);
break;
Debug.LogError("Not enough text meshes found in dynamic item");
return;
}
textMeshes[0].text = $"{apiUrlPrefix}http://{service.IpAddress}:{service.Port}{service.Path}";
textMeshes[1].text = $"{hostPrefix}{service.Host}";
itemInstance
.GetComponentInChildren<Interactable>()
.OnClick.AddListener(() =>
{
Debug.Log($"Clicked on service: {service.Host}");
action.Invoke();
ToggleVisibility();
});
gridObjectCollection.UpdateCollection();
scrollView.UpdateContent();
}
gridObjectCollection.UpdateCollection();
scrollView.UpdateContent();
}
/// <summary>
/// Removes all items from the table.
/// </summary>
public void RemoveAllItems()
{
foreach (Transform child in gridObjectCollection.transform)
{
Destroy(child.gameObject);
}
/// <summary>
/// Toggles the visibility of the table.
/// </summary>
public void ToggleVisibility()
{
isVisible = !isVisible;
Debug.Log("Removed all services from table");
gridObjectCollection.UpdateCollection();
scrollView.UpdateContent();
}
gameObject.SetActive(isVisible);
/// <summary>
/// Removes an item from the table by service.
/// </summary>
/// <param name="service"></param>
public void RemoveItemByService(MdnsService service)
{
string apiUrl = $"{apiUrlPrefix}http://{service.IpAddress}:{service.Port}{service.Path}";
string hostname = $"{hostPrefix}{service.Host}";
foreach (Transform child in gridObjectCollection.transform)
{
TextMeshPro[] textMeshes = child.GetComponentsInChildren<TextMeshPro>();
if (textMeshes.Length >= 2 && textMeshes[0].text == apiUrl && textMeshes[1].text == hostname)
{
Debug.Log($"Removing service from table: {service}");
Destroy(child.gameObject);
break;
}
}
gridObjectCollection.UpdateCollection();
scrollView.UpdateContent();
}
/// <summary>
/// Toggles the visibility of the table.
/// </summary>
public void ToggleVisibility()
{
isVisible = !isVisible;
gameObject.SetActive(isVisible);
}
}
}
}

View File

@ -7,13 +7,15 @@ using UnityEngine.Windows.WebCam;
using Windows.Storage;
#endif
public class VideoCaptureHandler : MonoBehaviour
namespace WebViewStream
{
[SerializeField]
private GameObject videoCaptureButton = null;
public class VideoCaptureHandler : MonoBehaviour
{
[SerializeField]
private GameObject videoCaptureButton = null;
private Interactable videoCaptureButtonInteractable = null;
private VideoCapture videoCapture = null;
private Interactable videoCaptureButtonInteractable = null;
private VideoCapture videoCapture = null;
#if WINDOWS_UWP && !UNITY_EDITOR
private const string freeSpace = "System.FreeSpace";
@ -49,122 +51,123 @@ public class VideoCaptureHandler : MonoBehaviour
}
#endif
/// <summary>
/// Starts recording a video.
/// </summary>
public void StartRecordingVideo()
{
/// <summary>
/// Starts recording a video.
/// </summary>
public void StartRecordingVideo()
{
#if WINDOWS_UWP && !UNITY_EDITOR
VideoCapture.CreateAsync(true, OnVideoCaptureCreated);
#else
VideoCapture.CreateAsync(false, OnVideoCaptureCreated);
VideoCapture.CreateAsync(false, OnVideoCaptureCreated);
#endif
if (videoCaptureButtonInteractable == null)
{
videoCaptureButtonInteractable = videoCaptureButton.GetComponent<Interactable>();
}
videoCaptureButtonInteractable.IsToggled = true;
}
/// <summary>
/// Stops recording a video.
/// </summary>
public void StopRecordingVideo()
{
videoCapture.StopRecordingAsync(OnStoppedRecordingVideo);
videoCaptureButtonInteractable.IsToggled = false;
}
/// <summary>
/// Toggles the recording of a video.
/// </summary>
public void ToggleRecordingVideo()
{
if (videoCapture == null)
{
StartRecordingVideo();
}
else if (videoCapture.IsRecording)
{
StopRecordingVideo();
}
}
private void OnVideoCaptureCreated(VideoCapture videoCapture)
{
if (videoCapture != null)
{
this.videoCapture = videoCapture;
Resolution cameraResolution = new Resolution();
foreach (Resolution resolution in VideoCapture.SupportedResolutions)
if (videoCaptureButtonInteractable == null)
{
if (resolution.width * resolution.height > cameraResolution.width * cameraResolution.height)
{
cameraResolution = resolution;
}
videoCaptureButtonInteractable = videoCaptureButton.GetComponent<Interactable>();
}
videoCaptureButtonInteractable.IsToggled = true;
}
float cameraFramerate = 0.0f;
foreach (float framerate in VideoCapture.GetSupportedFrameRatesForResolution(cameraResolution))
/// <summary>
/// Stops recording a video.
/// </summary>
public void StopRecordingVideo()
{
videoCapture.StopRecordingAsync(OnStoppedRecordingVideo);
videoCaptureButtonInteractable.IsToggled = false;
}
/// <summary>
/// Toggles the recording of a video.
/// </summary>
public void ToggleRecordingVideo()
{
if (videoCapture == null)
{
if (framerate > cameraFramerate)
{
cameraFramerate = framerate;
}
StartRecordingVideo();
}
else if (videoCapture.IsRecording)
{
StopRecordingVideo();
}
CameraParameters cameraParameters = new CameraParameters();
cameraParameters.hologramOpacity = 0.75f;
cameraParameters.frameRate = cameraFramerate;
cameraParameters.cameraResolutionWidth = cameraResolution.width;
cameraParameters.cameraResolutionHeight = cameraResolution.height;
cameraParameters.pixelFormat = CapturePixelFormat.BGRA32;
this.videoCapture.StartVideoModeAsync(
cameraParameters,
VideoCapture.AudioState.ApplicationAndMicAudio,
OnStartedVideoCaptureMode
);
}
else
private void OnVideoCaptureCreated(VideoCapture videoCapture)
{
Debug.LogError("Failed to create VideoCapture instance");
}
}
if (videoCapture != null)
{
this.videoCapture = videoCapture;
private void OnStartedVideoCaptureMode(VideoCapture.VideoCaptureResult result)
{
if (result.success)
Resolution cameraResolution = new Resolution();
foreach (Resolution resolution in VideoCapture.SupportedResolutions)
{
if (resolution.width * resolution.height > cameraResolution.width * cameraResolution.height)
{
cameraResolution = resolution;
}
}
float cameraFramerate = 0.0f;
foreach (float framerate in VideoCapture.GetSupportedFrameRatesForResolution(cameraResolution))
{
if (framerate > cameraFramerate)
{
cameraFramerate = framerate;
}
}
CameraParameters cameraParameters = new CameraParameters();
cameraParameters.hologramOpacity = 0.75f;
cameraParameters.frameRate = cameraFramerate;
cameraParameters.cameraResolutionWidth = cameraResolution.width;
cameraParameters.cameraResolutionHeight = cameraResolution.height;
cameraParameters.pixelFormat = CapturePixelFormat.BGRA32;
this.videoCapture.StartVideoModeAsync(
cameraParameters,
VideoCapture.AudioState.ApplicationAndMicAudio,
OnStartedVideoCaptureMode
);
}
else
{
Debug.LogError("Failed to create VideoCapture instance");
}
}
private void OnStartedVideoCaptureMode(VideoCapture.VideoCaptureResult result)
{
string filename = string.Format(
"WebView_{0}.mp4",
DateTime.UtcNow.ToString("yyyy-MM-ddTHHmmssZ")
);
string filepath = Path.Combine(Application.persistentDataPath, filename);
Debug.Log("Saving video to: " + filepath);
if (result.success)
{
string filename = string.Format(
"WebView_{0}.mp4",
DateTime.UtcNow.ToString("yyyy-MM-ddTHHmmssZ")
);
string filepath = Path.Combine(Application.persistentDataPath, filename);
Debug.Log("Saving video to: " + filepath);
videoCapture.StartRecordingAsync(filepath, OnStartedRecordingVideo);
videoCapture.StartRecordingAsync(filepath, OnStartedRecordingVideo);
}
}
}
private void OnStartedRecordingVideo(VideoCapture.VideoCaptureResult result)
{
Debug.Log("Started recording video");
private void OnStartedRecordingVideo(VideoCapture.VideoCaptureResult result)
{
Debug.Log("Started recording video");
#if WINDOWS_UWP && !UNITY_EDITOR
StartCoroutine(CheckAvailableStorageSpace());
#endif
}
}
private void OnStoppedRecordingVideo(VideoCapture.VideoCaptureResult result)
{
Debug.Log("Stopped recording video");
videoCapture.StopVideoModeAsync(OnStoppedVideoCaptureMode);
}
private void OnStoppedRecordingVideo(VideoCapture.VideoCaptureResult result)
{
Debug.Log("Stopped recording video");
videoCapture.StopVideoModeAsync(OnStoppedVideoCaptureMode);
}
private void OnStoppedVideoCaptureMode(VideoCapture.VideoCaptureResult result)
{
videoCapture.Dispose();
videoCapture = null;
private void OnStoppedVideoCaptureMode(VideoCapture.VideoCaptureResult result)
{
videoCapture.Dispose();
videoCapture = null;
}
}
}
}

View File

@ -3,43 +3,46 @@ using Microsoft.MixedReality.WebView;
using TMPro;
using UnityEngine;
public class WebViewBrowser : MonoBehaviour
namespace WebViewStream
{
[SerializeField]
private TMP_InputField URLField;
private void Start()
public class WebViewBrowser : MonoBehaviour
{
var webViewComponent = gameObject.GetComponent<WebView>();
webViewComponent.GetWebViewWhenReady(
(IWebView webView) =>
{
URLField.onSubmit.AddListener((text) => LoadUrl(webView));
[SerializeField]
private TMP_InputField URLField;
webView.Navigated += OnNavigated;
if (webView.Page != null)
private void Start()
{
var webViewComponent = gameObject.GetComponent<WebView>();
webViewComponent.GetWebViewWhenReady(
(IWebView webView) =>
{
URLField.text = webView.Page.AbsoluteUri;
URLField.onSubmit.AddListener((text) => LoadUrl(webView));
webView.Navigated += OnNavigated;
if (webView.Page != null)
{
URLField.text = webView.Page.AbsoluteUri;
}
}
);
}
private void OnNavigated(string path)
{
URLField.text = path;
}
private void LoadUrl(IWebView webView)
{
if (Uri.TryCreate(URLField.text, UriKind.Absolute, out Uri uriResult))
{
webView.Load(uriResult);
}
else
{
Debug.LogWarning("Invalid URL entered.");
}
);
}
private void OnNavigated(string path)
{
URLField.text = path;
}
private void LoadUrl(IWebView webView)
{
if (Uri.TryCreate(URLField.text, UriKind.Absolute, out Uri uriResult))
{
webView.Load(uriResult);
}
else
{
Debug.LogWarning("Invalid URL entered.");
}
}
}
}