Files
plugin-library/Assets/99.imdk_unity/Runtime/Scripts/ImmersalSDK.cs

503 lines
16 KiB
C#
Raw Normal View History

2025-06-19 10:56:43 +08:00
/*===============================================================================
Copyright (C) 2024 Immersal - Part of Hexagon. All Rights Reserved.
This file is part of the Immersal SDK.
The Immersal SDK cannot be copied, distributed, or made available to
third-parties for commercial purposes without written permission of Immersal Ltd.
Contact sales@immersal.com for licensing requests.
===============================================================================*/
using UnityEngine;
using UnityEngine.Events;
using System;
using System.Net.Http;
using System.Runtime.InteropServices;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Immersal.XR;
using Immersal.REST;
using AOT;
using UnityEngine.Serialization;
using Object = UnityEngine.Object;
namespace Immersal
{
public class ImmersalSDK : MonoBehaviour
{
// SDK properties
public static string sdkVersion = "2.1.1";
private static readonly string[] ServerList = new[] {"https://api.immersal.com", "https://immersal.hexagon.com.cn"};
public enum APIServer { DefaultServer = 0, ChinaServer = 1, CustomServer = 2 };
public APIServer ImmersalServer = APIServer.DefaultServer;
[Tooltip("SDK developer token")]
public string developerToken;
#region Interfaced components
[Header("Components")]
/*
* ImmersalSDK modular framework works with any implementation of the main components:
* IImmersalSession, IPlatformSupport, ILocalizer, ISceneUpdater and ITrackingAnalyzer.
*
* Serialization and inspector support for these implementation references are
* handled with an Object reference Property with a custom Attribute as well as
* a custom PropertyDrawer. An additional public Property is used for casting
* the Object to the relevant interface type.
*/
[SerializeField][InterfaceAttribute(typeof(IImmersalSession))]
private Object m_Session;
public IImmersalSession Session => m_Session as IImmersalSession;
[SerializeField][InterfaceAttribute(typeof(IPlatformSupport))]
private Object m_Platform;
public IPlatformSupport PlatformSupport => m_Platform as IPlatformSupport;
[SerializeField][InterfaceAttribute(typeof(ILocalizer))]
private Object m_Localizer;
public ILocalizer Localizer => m_Localizer as ILocalizer;
[SerializeField][InterfaceAttribute(typeof(ISceneUpdater))]
private Object m_SceneUpdater;
public ISceneUpdater SceneUpdater => m_SceneUpdater as ISceneUpdater;
[SerializeField][InterfaceAttribute(typeof(ITrackingAnalyzer))]
private Object m_TrackingAnalyzer;
public ITrackingAnalyzer TrackingAnalyzer => m_TrackingAnalyzer as ITrackingAnalyzer;
#endregion
// Configuration properties
[Header("Configuration")]
[SerializeField]
[Tooltip("Initialize the SDK on Awake")]
private bool m_InitializeAutomatically = true;
[SerializeField]
[Tooltip("Application target frame rate")]
private int m_TargetFrameRate = 60;
[Tooltip("Downsample image to HD resolution")]
[SerializeField]
private bool m_Downsample = true;
[SerializeField]
private ImmersalLogger.LoggingLevel m_LoggingLevel = ImmersalLogger.LoggingLevel.ErrorsAndWarnings;
[Header("Editor")]
[Tooltip("Map data download location in relation to Assets")]
[SerializeField]
private string m_DownloadDirectory = "Map Data";
// Public
public ITrackingStatus TrackingStatus => TrackingAnalyzer.TrackingStatus;
public bool IsReady => m_IsReady;
public int LicenseLevel => m_LicenseLevel;
public bool HasValidated => m_HasValidated;
public static HttpClient client;
public int targetFrameRate
{
get { return m_TargetFrameRate; }
set
{
m_TargetFrameRate = value;
SetFrameRate();
}
}
public string localizationServer
{
get => ImmersalServer == APIServer.CustomServer ? m_CustomServerUrl : ServerList[(int)ImmersalServer];
set
{
ImmersalServer = APIServer.CustomServer;
m_CustomServerUrl = value;
}
}
public bool downsample
{
get { return m_Downsample; }
set
{
m_Downsample = value;
SetDownsample();
}
}
public string DownloadDirectory
{
get { return m_DownloadDirectory; }
}
private void SetDownsample()
{
if (downsample)
{
Core.SetInteger("LocalizationMaxPixels", 960*720);
}
else
{
Core.SetInteger("LocalizationMaxPixels", 0);
}
}
// Events
[Header("Events")]
public UnityEvent OnInitializationComplete;
public UnityEvent OnUserValidationComplete;
public UnityEvent OnReset;
[SerializeField]
private string m_CustomServerUrl = "";
private int m_LicenseLevel = -1;
private bool m_IsReady = false;
private bool m_HasValidated = false;
private bool m_PluginCallbackRegistered = false;
private bool m_PlatformIsConfigured = false;
private bool m_LocalizerIsConfigured = false;
private bool m_MapsAreRegisteredAndLoaded = false;
#region Singleton pattern
private static ImmersalSDK instance = null;
public static ImmersalSDK Instance
{
get
{
#if UNITY_EDITOR
if (instance == null && !Application.isPlaying)
{
instance = FindObjectOfType<ImmersalSDK>();
}
#endif
if (instance == null)
{
ImmersalLogger.LogError("No ImmersalSDK instance found. Ensure one exists in the scene.");
}
return instance;
}
}
#endregion
private void Awake()
{
if (instance == null)
{
instance = this;
}
if (instance != this)
{
ImmersalLogger.LogError("There must be only one ImmersalSDK object in a scene.");
UnityEngine.Object.DestroyImmediate(this);
return;
}
if (m_InitializeAutomatically)
_ = Initialize();
}
public async Task Initialize()
{
ImmersalLogger.Level = m_LoggingLevel;
ImmersalLogger.Log("Initializing ImmersalSDK", ImmersalLogger.LoggingLevel.Verbose);
if (PlatformSupport == null || Localizer == null || SceneUpdater == null || TrackingAnalyzer == null)
{
throw new Exception("ImmersalSDK has null references, please assign all Properties.");
}
// plugin log callback
if (!m_PluginCallbackRegistered)
{
LogCallback callback_delegate = new LogCallback(Log);
IntPtr intptr_delegate = Marshal.GetFunctionPointerForDelegate(callback_delegate);
Native.PP_RegisterLogCallback(intptr_delegate);
m_PluginCallbackRegistered = true;
}
// http client
HttpClientHandler handler = new HttpClientHandler();
handler.ClientCertificateOptions = ClientCertificateOption.Automatic;
client = new HttpClient(handler);
client.DefaultRequestHeaders.ExpectContinue = false;
client.Timeout = TimeSpan.FromDays(1);
// validate user
if (developerToken != null && developerToken.Length > 0)
{
PlayerPrefs.SetString("token", developerToken);
ValidateUser();
}
SetFrameRate();
#if !UNITY_EDITOR
SetDownsample();
#endif
MapManager.RefreshLocalizationMethods();
await ConfigureComponents();
if (m_IsReady)
{
ImmersalLogger.Log("Immersal SDK ready!", ImmersalLogger.LoggingLevel.Verbose);
OnInitializationComplete?.Invoke();
}
}
public async void ValidateUser()
{
if (Application.platform == RuntimePlatform.IPhonePlayer || Application.platform == RuntimePlatform.Android)
{
m_LicenseLevel = Core.ValidateUser(developerToken);
string licenseLevel = m_LicenseLevel >= 1 ? "Enterprise" : "Free";
ImmersalLogger.Log($"{licenseLevel} License");
}
else
{
JobStatusAsync j = new JobStatusAsync();
SDKStatusResult result = await j.RunJobAsync();
m_LicenseLevel = result.level;
string licenseLevel = m_LicenseLevel >= 1 ? "Enterprise" : "Free";
ImmersalLogger.Log($"{licenseLevel} License");
}
m_HasValidated = true;
OnUserValidationComplete?.Invoke();
}
private async Task ConfigureComponents()
{
if (!m_MapsAreRegisteredAndLoaded)
{
m_MapsAreRegisteredAndLoaded = await RegisterAndLoadMaps();
}
if (!m_PlatformIsConfigured)
{
m_PlatformIsConfigured = await ConfigurePlatform();
if (m_PlatformIsConfigured)
ImmersalLogger.Log("Platform configured.", ImmersalLogger.LoggingLevel.Verbose);
}
if (!m_LocalizerIsConfigured)
{
m_LocalizerIsConfigured = await ConfigureLocalizer();
if (m_LocalizerIsConfigured)
ImmersalLogger.Log("Localizer configured.", ImmersalLogger.LoggingLevel.Verbose);
}
m_IsReady = m_PlatformIsConfigured && m_LocalizerIsConfigured && m_MapsAreRegisteredAndLoaded;
}
private async Task<bool> RegisterAndLoadMaps()
{
XRMap[] maps = FindObjectsOfType<XRMap>();
ImmersalLogger.Log($"Found {maps.Length} maps in scene.");
List<Task> mapRegisterTasks = new List<Task>();
foreach (XRMap map in maps)
{
ISceneUpdateable sceneUpdateable = map.gameObject.transform.GetComponentInParent<ISceneUpdateable>(true);
if (sceneUpdateable != null)
{
ImmersalLogger.Log($"Starting RegisterAndLoad for map {map.mapId}");
// map registering might initiate downloads so we should not await here
Task t = MapManager.RegisterAndLoadMap(map, sceneUpdateable);
mapRegisterTasks.Add(t);
}
}
// wait for all register tasks to finish
await Task.WhenAll(mapRegisterTasks.ToArray());
List<XRMap> registeredMaps = MapManager.GetRegisteredMaps();
ImmersalLogger.Log($"Registered maps: {(registeredMaps.Count > 0 ? string.Join(",", registeredMaps) : "none")}", ImmersalLogger.LoggingLevel.Verbose);
return true;
}
private async Task<bool> ConfigurePlatform()
{
try
{
IPlatformConfigureResult result = await PlatformSupport.ConfigurePlatform();
return result.Success;
}
catch (Exception e)
{
AbortWithException(e, "PlatformSupport");
}
return false;
}
private async Task<bool> ConfigureLocalizer()
{
// Get all registered maps
List<XRMap> maps = MapManager.GetRegisteredMaps();
// Create a mapping of maps and the associated localization methods
Dictionary<ILocalizationMethod, XRMap[]> mapping = maps
.GroupBy(map => map.LocalizationMethod)
.ToDictionary(
group => group.Key,
group => group.ToArray());
DefaultLocalizerConfiguration config = new DefaultLocalizerConfiguration
{
ConfigurationsToAdd = mapping,
StopRunningTasks = true
};
try
{
ILocalizerConfigurationResult result = await Localizer.ConfigureLocalizer(config);
return result.Success;
}
catch (Exception e)
{
AbortWithException(e, "Localizer");
}
return false;
}
private void SetFrameRate()
{
Application.targetFrameRate = targetFrameRate;
}
// Convenience method
// Note: no data processing chain here
public async Task LocalizeOnce()
{
if (m_IsReady)
{
ILocalizationResults localizerResults;
IPlatformUpdateResult platformResult = await PlatformSupport.UpdatePlatform();
if (platformResult.Success)
{
localizerResults = await Localizer.Localize(platformResult.CameraData);
foreach (ILocalizationResult result in localizerResults.Results)
{
if (result.Success && MapManager.TryGetMapEntry(result.MapId, out MapEntry entry))
{
await SceneUpdater.UpdateScene(entry, platformResult.CameraData, result);
}
}
}
else
{
// Localization never ran due to failed platform update
// so we need invalidate results.
localizerResults = new LocalizationResults
{
Results = Array.Empty<ILocalizationResult>()
};
}
// Update tracking status
TrackingAnalyzer.Analyze(platformResult.Status, localizerResults);
}
}
private void AbortWithException(Exception e, string logPrefix = "")
{
m_IsReady = false;
ImmersalLogger.LogError($"{logPrefix}: {e.Message}");
Debug.LogException(e);
}
private void OnDestroy()
{
m_IsReady = false;
MapManager.RemoveAllMaps(false);
}
public void TriggerSoftReset()
{
SoftReset();
}
public async Task SoftReset()
{
await Session.ResetSession();
await ResetScenes();
TrackingAnalyzer.Reset();
}
public void TriggerResetScenes()
{
ResetScenes();
}
public async Task ResetScenes()
{
ImmersalLogger.Log("Resetting SceneUpdateables");
List<ISceneUpdateable> sceneUpdateables = MapManager.GetSceneUpdateablesInUse();
foreach (ISceneUpdateable sceneUpdateable in sceneUpdateables)
{
await sceneUpdateable.ResetScene();
}
}
[ContextMenu("RestartSdk")]
public async void RestartSdk()
{
ImmersalLogger.Log("Restarting ImmersalSDK", ImmersalLogger.LoggingLevel.Verbose);
m_IsReady = false;
// Stop and clean up components
await Session.StopSession();
// Clear maps
MapManager.RemoveAllMaps();
m_MapsAreRegisteredAndLoaded = false;
m_LocalizerIsConfigured = false;
await Localizer.StopAndCleanUp();
m_PlatformIsConfigured = false;
await PlatformSupport.StopAndCleanUp();
// Reset scenes
await ResetScenes();
// Invoke event
OnReset?.Invoke();
// Restart
await Initialize();
}
// Logging callback for plugin
[MonoPInvokeCallback(typeof(LogCallback))]
public static void Log(IntPtr ansiString)
{
string msg = Marshal.PtrToStringAnsi(ansiString);
ImmersalLogger.Log($"Plugin: {msg}", ImmersalLogger.LoggingLevel.Verbose);
}
}
public class ComponentTaskCriticalException : Exception
{
public ComponentTaskCriticalException(string message) : base(message) { }
}
}