This repository has been archived on 2026-01-20. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
정영민 f4cf556cde update
2025-02-20 10:30:18 +09:00

1579 lines
60 KiB
C#

/*
* Copyright (C) 2023 because-why-not.com Limited
*
* Please refer to the license.txt for license information
*/
using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using UnityEngine;
namespace Byn.Awrtc.Unity
{
/// <summary>
/// UnityCallFactory allows to create new ICall objects and will dispose them
/// automatically when unity shuts down. It will also handle most
/// global tasks that aren't related to specific call objects such as
/// * Initialization (see EnsureInit)
/// * Listing available video devices
/// * logging
///
/// This class acts as a unity specific wrapper that will forward calls
/// to platform specific implementations depending on which platform
/// is selected in Unity Build Settings menu.
/// </summary>
public class UnityCallFactory : MonoBehaviour
{
/// <summary>
/// If set to true it will automatically set Application.runInBackground = true on startup.
/// Set to false if you want to turn off this behaviour.
/// Note the asset won't run well with runInBackground = false if the application is in the background.
/// </summary>
public static readonly bool FORCE_RUN_IN_BACKGROUND = true;
/// <summary>
/// Set to true to allow directly accessing UnityCallFactory.Instance
/// without calling one of the init methods first. This is available only to
/// give users time to switch to the new init methods.
///
/// If set to false an exception will be triggered if UnityCallFactory.Instance
/// is accessed before initialization. This helps to ensure that user
/// code will work on all platforms including those that
/// do not allow synchronous initialization.
///
/// In future versions this value will be false by default!
/// </summary>
public static readonly bool ALLOW_SYNCHRONOUS_INIT = false;
/// <summary>
/// Will switch the Android OS into communication mode. This will
/// cause let Android know we use voice calls. It should show
/// the Call volume if the audio buttons are used and might
/// trigger a general change in how the audio is piped within the OS.
/// The exact behaviour is specific to each device.
///
/// Ideally, set this to false and call it yourself before you
/// start an audio call and turn it off after the call finishes.
/// Some external libraries might behave differently if the
/// communication mode is active.
///
///
/// For more info see AndroidHelper.SetCommunicationMode();
///
/// </summary>
public static readonly bool ANDROID_SET_COMMUNICATION_MODE = true;
/// <summary>
/// Old versions of Unity / Mono are missing root certificates needed to check if wss or https servers are
/// trustworthy.
/// true = Invalid certificates are ignored. Communication is still encrypted but man in the middle attacks are possible
/// (this might be needed for the old .NET 3.5 backend)
/// false = use the default system to check certificates (a lot safer)
///
/// You can also overwrite the check by setting Byn.Awrtc.Native.WebsocketSharpFactory.CertCallbackOverride
/// (might not work on all platforms / all .NET backends)
///
/// This flag is ignored in WebGL as the browser takes care of this and we don't have any control over it.
/// </summary>
public static readonly bool ALLOW_UNCHECKED_REMOTE_CERTIFICATE = false;
#if NET_2_0
//Unity 2017 and .NET 3.5 compiler doesn't even know TLS1.2 or TLS1.1 and will give you a compiler error if used
//we use a hack to try allow newer TLS versions if the underlying platform do support them
//This isn't really safe and best to upgrade to .NET 4.x asap!
public static readonly SslProtocols TLS_VERSIONS
= (SslProtocols)(Byn.Awrtc.Native.SslProtocolsOverride.Tls12
| Native.SslProtocolsOverride.Tls11
| Native.SslProtocolsOverride.Tls);
#else
//Unity 2018 with .NET 4.x
//servers can block old versions for security reasons but could also fail to understand newer versions
//Use the newest TLS version whenever possible
public static readonly System.Security.Authentication.SslProtocols TLS_VERSIONS = System.Security.Authentication.SslProtocols.Tls12;
// | System.Security.Authentication.SslProtocols.Tls11;
#endif
/// <summary>
/// Keeps the device from sleeping / turning of the screen
/// while calls are active.
///
/// </summary>
public static readonly bool KEEP_AWAKE = true;
/// <summary>
/// Prefix added to all log message to make filtering easier
/// </summary>
public static readonly string GLOBAL_LOG_PREFIX = "awrtc:";
public static readonly bool LOG_TIME = true;
/// <summary>
/// Sets the default log level used on startup.
/// </summary>
public static readonly LogLevel DEFAULT_LOG_LEVEL = LogLevel.Warning;
/// <summary>
/// Turns on the internal low level log. This will
/// heavily impact performance and might cause crashes.
/// Log appears in platform specific log output e.g.
/// xcode on ios
/// logcat on android
/// visual studio debug output window (connect to unity.exe via "Debug->Attach to process"
/// Won't appear in the unity log!
/// </summary>
public static readonly bool NATIVE_VERBOSE_LOG = false;
/// <summary>
/// For android log messages are split to avoid logcat cutting off the end
/// (keep a few characters below the actual size limit to allow for a prefix added later)
/// </summary>
private static readonly int ANDROID_LOG_LEN = 1000;
/// <summary>
/// If this is set to true the native log will be forwarded to the C# side log
/// and output to the Unity log. Only use for testing! This can cause Unity to crash
/// or stall due to threading issues between Unity C# and the native layer.
///
/// Set NATIVE_VERBOSE_LOG to true as well
/// </summary>
public static readonly bool NATIVE_VERBOSE_LOG_TO_UNITY = false;
/// <summary>
/// True will prioritize the h264 encoder over VP8. Meaning iOS to iOS calls will use
/// less CPU due to hardware acceleration. Other platforms might not support h264 and
/// still fall back to VP8.
///
/// Set this to false if you experience problems with video encoding / decoding e.g.
/// if the video doesn't show up at all on remote machine
///
/// iOS hardware h264 encoder might fail to encode certain resolutions and can only
/// send a limited number of streams (4 last tested with iOS 13 on iPad mini 4)
/// </summary>
public static readonly bool IOS_PREFER_H264_HARDWARE_ACC = true;
#if UNITY_ANDROID && !UNITY_EDITOR
/// <summary>
/// Keeps track of the original Android audio mode. Will be restored
/// after the last call finished
///
/// </summary>
private int mAndroidPreviousAudioMode;
#endif
/// <summary>
/// Keeps track of the original Unity's Screen.sleepTimeout value. After
/// the last call is destroyed it will be reset to this value.
/// </summary>
private int mSleepTimeoutBackup;
/// <summary>
/// Will be set to true after the first call is created. False
/// after the last is destroyed.
/// While true some special flags will be set to optimize for
/// active calls.
/// (native platforms only)
/// </summary>
private bool mHasActiveCall = false;
/// <summary>
/// Used to keep track of the init process
/// </summary>
public enum InitState
{
/// <summary>
/// Singleton not yet initialized and init process not yet started
/// </summary>
Uninitialized = 0,
/// <summary>
/// Singleton might be created but the init process is not yet completed. Some
/// features might not work.
/// Depending on the platform the user might see a pop up asking for access
/// to media devices or network.
/// </summary>
Initializing = 1,
/// <summary>
/// Init process is complete.
/// </summary>
Initialized = 2,
/// <summary>
/// Init process has failed. See log for detailed information.
/// </summary>
Failed = 3,
/// <summary>
/// Indicates that the factory is in the process of shutting down and
/// cleaning up the memory.
///
/// </summary>
Disposing = 4,
/// <summary>
/// Destroyed
/// </summary>
Disposed = 5
}
private static InitState sInitState = InitState.Uninitialized;
/// <summary>
/// To keep track user scripts callbacks
/// </summary>
private class InitCallbacks
{
public readonly Action onSuccess;
public readonly Action<string> onFailure;
public InitCallbacks(Action success, Action<string> failure)
{
onSuccess = success;
onFailure = failure;
}
}
/// <summary>
/// Buffers all user script callbacks that try to ensure the init process is completed
/// via EnsureInit. Will be emptied after init completed or failed
/// </summary>
private static List<InitCallbacks> sInitCallbacks = new List<InitCallbacks>();
/// <summary>
/// Error message set if the init process failed to inform user scripts that try to access it later.
/// </summary>
private static string sInitFailedMessage = "";
/// <summary>
/// Set to true once the default logger registered its callback.
/// Will remain true until shutdown.
/// </summary>
private static LogLevel sActiveLogLevel = LogLevel.None;
/// <summary>
/// Currently used log level.
/// </summary>
public static LogLevel ActiveLogLevel
{
get
{
return sActiveLogLevel;
}
}
/// <summary>
/// Log level for the platform specific code.
///
/// </summary>
private static LogLevel sNativeLogLevel = LogLevel.None;
/// <summary>
/// Native log level.
/// </summary>
public static LogLevel NativeLogLevel
{
get
{
return sNativeLogLevel;
}
}
private static readonly string LOG_PREFIX = typeof(UnityCallFactory).Name + ": ";
#if (!UNITY_WEBGL && !UNITY_WSA) || UNITY_EDITOR
private static WebRtcCSharp.PeerFactoryConfig sConfig = null;
public static WebRtcCSharp.PeerFactoryConfig Config
{
set
{
if (sInitState != InitState.Uninitialized)
{
//If this is triggered something else already initialized the asset before the configuration was set
//Check for a log message "awrtc:Starting init process for ..." to find the code that triggered the init
throw new InvalidOperationException("Configuration must be set before the first access to UnityCallFactory.Instance/EnsureInit. "
+ "Check the log to find the first access.");
}
sConfig = value;
}
get
{
return sConfig;
}
}
#endif
private static UnityCallFactory sInstance;
public static UnityCallFactory Instance
{
get
{
//return null if someone tries to access the singleton after its lifetime
if (sInitState == InitState.Disposing || sInitState == InitState.Disposed)
{
Debug.LogWarning(LOG_PREFIX + " is already destroyed due to shutdown! Returning null.");
return null;
}
if (sInitState == InitState.Uninitialized)
{
if (ALLOW_SYNCHRONOUS_INIT)
{
EnsureInit(null, null);
if (sInitState == InitState.Initializing)
{
Debug.LogWarning(LOG_PREFIX + "This platform requires an asynchronous init process. Use UnityCallFactory.EnsureInit to make sure "
+ " you don't access the factory before the init process is completed!");
}
}
else
{
//synchronous init is turned off but user tries to access it before calling the init method
//This is a user error! Please make sure to call InitAsync before using the singleton!
throw new InvalidOperationException("Accessed " + typeof(UnityCallFactory).Name + ".Instance before calling any of the Init methods!");
}
}
return sInstance;
}
}
public static bool IsInitialized
{
get
{
return sInstance != null;
}
}
private static void AddInitCallbacks(Action onSuccessCallback, Action<string> onFailureCallback)
{
sInitCallbacks.Add(new InitCallbacks(onSuccessCallback, onFailureCallback));
}
public static void EnsureInit(Action onSuccessCallback)
{
EnsureInit(onSuccessCallback, null);
}
public static void EnsureInit(Action onSuccessCallback, Action<string> onFailureCallback)
{
if(sInitState == InitState.Disposed)
{
sInitFailedMessage = "";
sInitState = InitState.Uninitialized;
}
if (sInitState == InitState.Uninitialized)
{
AddInitCallbacks(onSuccessCallback, onFailureCallback);
Init();
}
else if (sInitState == InitState.Initializing)
{
AddInitCallbacks(onSuccessCallback, onFailureCallback);
}
else if (sInitState == InitState.Initialized)
{
//we already completed. inform the user
TriggerSuccessCallback(onSuccessCallback);
}
else if (sInitState == InitState.Failed)
{
TriggerFailedCallback(onFailureCallback, sInitFailedMessage);
}
else
{
TriggerFailedCallback(onFailureCallback, "UnityCallFactory already disposed / destroyed!");
}
}
/// <summary>
/// Don't call directly. Use EnsureInit instead/
/// </summary>
private static void Init()
{
if (sInitState != InitState.Uninitialized)
throw new InvalidOperationException("Init can only be called once!");
if(Application.runInBackground == false && FORCE_RUN_IN_BACKGROUND)
{
//To avoid this warning just set Application.runInBackground = true yourself or trick it
//in the Unity project settings,
Debug.LogWarning("Setting Application.runInBackground to true. This is to ensure network connections can"
+ " be maintained while the application is in the background!");
Application.runInBackground = true;
}
Debug.Log(GLOBAL_LOG_PREFIX + "Starting init process for " + Application.platform);
sInitState = InitState.Initializing;
CreateSingleton();
//InitAsync will finish complete here for platforms that support synchronous init
//but will keep running for async platforms (uwp and webgl) until all dependencies are
//loaded / access to devices are allowed.
IEnumerator asyncInit = sInstance.CoroutineInitAsync();
sInstance.StartCoroutine(asyncInit);
}
/// <summary>
/// Convert exceptions to strings for now as not all WebGL apps support them
/// </summary>
/// <param name="e"></param>
private void TriggerOnInitFailed(Exception e)
{
Debug.LogException(e);
TriggerInitFailed("UnityCallFactory failed with following exception: " + e);
}
private static void TriggerInitFailed(string error)
{
Debug.LogError(LOG_PREFIX + "triggered TriggerInitFailed. It won't be able to run: " + error);
sInitState = InitState.Failed;
sInitFailedMessage = error;
foreach (var v in sInitCallbacks)
{
if (v != null)
TriggerFailedCallback(v.onFailure, error);
}
sInitCallbacks.Clear();
}
private static void TriggerInitSuccess()
{
Debug.Log(LOG_PREFIX + "initialized successfully ");
sInitState = InitState.Initialized;
foreach (var v in sInitCallbacks)
{
if (v != null)
TriggerSuccessCallback(v.onSuccess);
}
sInitCallbacks.Clear();
}
private static void TriggerFailedCallback(Action<string> failedCallback, string error)
{
try
{
if (failedCallback != null)
failedCallback(error);
}
catch (Exception e)
{
Debug.LogError(LOG_PREFIX + "User code threw exception:");
Debug.LogException(e);
}
}
private static void TriggerSuccessCallback(Action successCallback)
{
try
{
if (successCallback != null)
successCallback();
}
catch (Exception e)
{
Debug.LogError(LOG_PREFIX + "User code threw exception:");
Debug.LogException(e);
}
}
/// <summary>
///
/// </summary>
/// <returns></returns>
private IEnumerator CoroutineInitAsync()
{
if (sInitState != InitState.Initializing)
throw new InvalidOperationException("Expected to be called once during the init process only!");
//Some platforms need to load a dynamic library and initialize it properly
//this is done here.
bool staticInitRes = false;
try
{
staticInitRes = InitStatic();
}
catch (Exception e)
{
//This usually happens because files or folders were moved around / renamed / deleted from the project directory
//Could also be due to invalid project settings or trying to run this on Linux or another unsupported platform.
//might be best to reimport the asset from scratch
Debug.LogError(LOG_PREFIX + "Exception thrown during static init process. See log above for details. "
+ " This usually means some plugin files are missing or you are trying to run this on an unsupported platform!");
TriggerOnInitFailed(e);
yield break;
}
if (staticInitRes == false)
{
//Tried to use it on a platform that isn't supported?
string error = "Platform specific sync init process failed. UnityCallFactory can't be used. See log message above for details.";
Debug.LogError(LOG_PREFIX + error);
TriggerInitFailed(error);
yield break;
}
if (DEFAULT_LOG_LEVEL != LogLevel.None)
{
RequestLogLevel(DEFAULT_LOG_LEVEL);
}
if (NATIVE_VERBOSE_LOG)
{
sNativeLogLevel = LogLevel.Info;
}
UpdateNativeLogLevel();
//Waiting process for asynchronous initialization.
//this will be skipped immediately on most platforms
int waitCounter = 0;
InitStatusUpdate state;
while ((state = IsInitStaticComplete()) == InitStatusUpdate.Waiting /*|| waitCounter < 5*/)
{
if (waitCounter % 10 == 0)
{
Debug.Log(LOG_PREFIX + "is still waiting to complete the init process");
}
yield return new WaitForSecondsRealtime(0.1f);
waitCounter++;
}
//error during the asynchronous part?
if (state == InitStatusUpdate.Error)
{
//Tried to use it on a platform that isn't supported?
string error = "Platform specific async init process failed. UnityCallFactory can't be used. See log message above for details.";
TriggerInitFailed(error);
yield break;
}
try
{
sInstance.InitObj();
}
catch (Exception e)
{
//This could happen if someone mixed up different versions / files of the asset
//e.g. the C# code for one version but the native plugin for another
//or WebRTC failed completely trying to boot up e.g. due to driver errors,
//OS / security software blocked access to network hardware
Debug.LogError(LOG_PREFIX + "Failed to create the call factory. ");
TriggerOnInitFailed(e);
yield break;
}
TriggerInitSuccess();
}
private static void CreateSingleton()
{
GameObject singleton = new GameObject();
UnityEngine.Object.DontDestroyOnLoad(singleton);
singleton.name = typeof(UnityCallFactory).Name;
sInstance = singleton.AddComponent<UnityCallFactory>();
}
#if (!UNITY_WEBGL && !UNITY_WSA) || UNITY_EDITOR
private static void InitNativeWebsockets()
{
#if NET_2_0
Debug.LogWarning("You are using an old .NET / Mono version. This might cause security problems with encrypted websockets. Use .NET 4.x backend if possible.");
#endif
Byn.Awrtc.Native.WebsocketSharpClient.sslProtocol = TLS_VERSIONS;
//turn off the security check for certificates
//this allows using invalid certificates but allows man-in-the-middle attacks
//unless to implement your own checks below
if (ALLOW_UNCHECKED_REMOTE_CERTIFICATE)
{
Byn.Awrtc.Native.WebsocketSharpFactory.CertCallbackOverride =
(object sender, System.Security.Cryptography.X509Certificates.X509Certificate certificate,
System.Security.Cryptography.X509Certificates.X509Chain chain,
System.Net.Security.SslPolicyErrors sslPolicyErrors) =>
{
//don't warn if no error is returned (never seems to be the case)
if (sslPolicyErrors == System.Net.Security.SslPolicyErrors.None)
return true;
Debug.LogWarning(LOG_PREFIX + "Unity/Mono has no root certificates and is unable to check if certificates are trustworthy. "
+ "Continuing without check. If you need higher security set UnityCallFactory.ALLOW_UNCHECKED_REMOTE_CERTIFICATE = false and "
+ " install your certificate in mono or add your own check here.");
return true;
};
}
}
#endif
/// <summary>
/// Static init process that will load dynamic libraries and make sure
/// it can be accessed. This will do the very first call into
/// platform specific libraries and might fail if files are
/// corrupted or lost or if the user tries to run the application on
/// platforms that aren't supported.
///
/// In the future this call might be only the trigger the start of
/// an asynchronous init process.
/// </summary>
/// <returns>Returns true on success.</returns>
private static bool InitStatic()
{
bool successful;
#if (!UNITY_WSA && !UNITY_WEBGL) || UNITY_EDITOR
/*
If any of the lines below causes an exception you likely are missing
some important dynamic libraries (either C# dll or platform specific dll/framework files)
try downloading the asset again and run from a new project
*/
//If an exception leads you here your C# dll's are missing:
var versionCsPlugin = Awrtc.Native.NativeAwrtcFactory.GetVersion();
//and an exception here means the C++ plugin is missing:
var versionCppPlugin = Awrtc.Native.NativeAwrtcFactory.GetWrapperVersion();
var versionWebRtc = Awrtc.Native.NativeAwrtcFactory.GetWebRtcVersion();
Debug.Log(GLOBAL_LOG_PREFIX + "Version info: [" + versionCsPlugin + "] / [" + versionCppPlugin + "] / [" + versionWebRtc + "]");
InitNativeWebsockets();
#endif
if (Application.platform == RuntimePlatform.WebGLPlayer)
{
successful = StaticInitBrowser();
}
else if (Application.platform == RuntimePlatform.Android)
{
successful = StaticInitAndroid();
}
else if (IsNativePlatform())
{
//the other native platforms automatically load everything needed
successful = true;
}
else if (Application.platform == RuntimePlatform.WSAPlayerX86
|| Application.platform == RuntimePlatform.WSAPlayerARM
|| Application.platform == RuntimePlatform.WSAPlayerX64)
{
#if UNITY_WSA && !UNITY_EDITOR
//UnityEngine.WSA.Application.InvokeOnUIThread(() => {
Byn.Awrtc.Uwp.RtcGlobal.Init();
//}, false);
#endif
//this one is work in progress.
successful = true;
}
else
{
//fail
successful = false;
Debug.LogError(LOG_PREFIX + "Platform not supported: " + Application.platform);
}
return successful;
}
private enum InitStatusUpdate
{
Waiting,
Initialized,
Error
}
private static InitStatusUpdate IsInitStaticComplete()
{
if (Application.platform == RuntimePlatform.WSAPlayerX86
|| Application.platform == RuntimePlatform.WSAPlayerARM
|| Application.platform == RuntimePlatform.WSAPlayerX64)
{
#if UNITY_WSA && !UNITY_EDITOR
if(Byn.Awrtc.Uwp.RtcGlobal.InitState == Byn.Awrtc.Uwp.RtcGlobal.State.Initialized)
{
Debug.Log(LOG_PREFIX + "WSA plugin Initialized ...");
return InitStatusUpdate.Initialized;
}
else if (Byn.Awrtc.Uwp.RtcGlobal.InitState == Byn.Awrtc.Uwp.RtcGlobal.State.Initializing
|| Byn.Awrtc.Uwp.RtcGlobal.InitState == Byn.Awrtc.Uwp.RtcGlobal.State.Uninitialized)
{
Debug.Log(LOG_PREFIX + "WSA plugin is still initializing ...");
return InitStatusUpdate.Waiting;
}
else
{
//either failed to start or resulted in an error
Debug.LogError(LOG_PREFIX + "WSA plugin failed to initialize. See console for more information.");
return InitStatusUpdate.Error;
}
#else
return InitStatusUpdate.Error;
#endif
}
else if (Application.platform == RuntimePlatform.WebGLPlayer)
{
#if UNITY_WEBGL
var status = Byn.Awrtc.Browser.CAPI.Unity_PollInitState();
if (status == Browser.CAPI.InitState.Initializing)
{
return InitStatusUpdate.Waiting;
}
else if (status == Browser.CAPI.InitState.Initialized)
{
return InitStatusUpdate.Initialized;
}
Debug.LogError("WebGL plugin returned " + status);
#endif
return InitStatusUpdate.Error;
}
else
{
//all other platforms use still static init
//meaning if we got so far it is working
return InitStatusUpdate.Initialized;
}
}
/// <summary>
/// Initializes the singleton object itself. Will be called after
/// the static init completed successfully.
/// </summary>
private void InitObj()
{
#if UNITY_WEBGL && !UNITY_EDITOR
mFactory = new Byn.Awrtc.Browser.BrowserCallFactory();
#elif UNITY_WSA && !UNITY_EDITOR
mFactory = new Byn.Awrtc.Uwp.UwpCallFactory();
#else//native platforms only
#if UNITY_IOS
//Only works before the very first factory is created!
WebRtcCSharp.IosHelper.PreferH264(IOS_PREFER_H264_HARDWARE_ACC);
#endif
if(sConfig == null)
{
sConfig = new WebRtcCSharp.PeerFactoryConfig();
}
else
{
Debug.LogWarning("Setting custom config for the UnityCallFactory " + sConfig.audioLayer);
}
var factory = new Native.NativeAwrtcFactory(sConfig);
factory.LastCallDisposed += LeaveActiveCallState;
mFactory = factory;
#if UNITY_IOS
//Update iOS 14: Now the workaround is causing problems but
//new Unity versions can handle the audio turning off again
//Old: iOS13 workaround for WebRTC / Unity audio bug on ios
//WebRTC will deactivate the audio session once all calls ended
//This will keep it active as Unity relies on this session as well
WebRtcCSharp.IosHelper.InitAudioLayer();
#endif
#endif
}
/// <summary>
/// Unity will call this during shutdown. It will make sure all ICall objects and the factory
/// itself will be destroyed properly.
/// </summary>
protected void OnDestroy()
{
Dispose();
}
private IAwrtcFactory mFactory = null;
/// <summary>
/// Do not use. For debugging only.
/// </summary>
public IAwrtcFactory InternalFactory
{
get
{
return mFactory;
}
}
private UnityVideoInput mVideoInput = null;
public UnityVideoInput VideoInput
{
get
{
if (mFactory != null && mVideoInput == null)
{
IVideoInput platformVideoInput = null;
#if UNITY_WEBGL && !UNITY_EDITOR
platformVideoInput = new Browser.BrowserVideoInput();
#elif UNITY_WSA && !UNITY_EDITOR
platformVideoInput = (mFactory as Uwp.UwpCallFactory).VideoInput;
#else
platformVideoInput = (mFactory as Native.NativeAwrtcFactory).VideoInput;
#endif
if(platformVideoInput == null)
{
//It is possible there are new platforms in the future that still lack VideoInput support
throw new InvalidOperationException("Unable to access VideoInput. Make sure this platform supports custom video input!");
}
mVideoInput = new UnityVideoInput(platformVideoInput);
}
return mVideoInput;
}
}
private void Awake()
{
//to ensure that the object isn't created manually
if (sInitState != InitState.Initializing)
{
Debug.LogError(LOG_PREFIX + "Attempted to create the script UnityCallFactory manually."
+ " This is not possible. Use UnityCallFactory.Init methods to create it!");
Destroy(this);
}
}
private void Start()
{
}
private void Update()
{
}
/// <summary>
/// Check if this platform uses the bundled native c++ plugin
/// </summary>
/// <returns></returns>
private static bool IsNativePlatform()
{
//do not access any platform specific code here
#if UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN
return true;
#elif UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX
return true;
#elif UNITY_ANDROID
return true;
#elif UNITY_IOS
return true;
#else
return false;
#endif
}
private static bool StaticInitBrowser()
{
#if UNITY_WEBGL
Debug.Log(LOG_PREFIX + "Initialize browser specific plugin");
//check if the java script part is available
if (Byn.Awrtc.Browser.BrowserCallFactory.IsAvailable() == false)
{
//plugin is now loaded via jspre. if we end up here
//the awrtc.jslib has been excluded from the build
//Debug.LogError(LOG_PREFIX + "Failed to load the java script plugin. Make sure awrtc.jspre and jslib are included in the build. ");
//return false;
//Old dynamic system for loading the plugin. Replaced by jspre
Debug.Log("Plugin not yet loaded. Injecting plugin via eval");
//js part is missing -> inject the code into the browser
Byn.Awrtc.Browser.BrowserCallFactory.InjectJsCode();
bool successful = Byn.Awrtc.Browser.BrowserCallFactory.IsAvailable();
if (successful == false)
{
Debug.LogError("Failed to access the java script library. This might be because of browser incompatibility or a missing java script plugin!");
return false;
}
}
//check if the browser side has the necessary features to run this
if (Byn.Awrtc.Browser.CAPI.Unity_WebRtcNetwork_IsBrowserSupported() == false)
{
Debug.LogError(LOG_PREFIX + "Browser is missing some essential WebRTC features. Try a modern version of Chrome or Firefox.");
return false;
}
if (Byn.Awrtc.Browser.CAPI.Unity_MediaNetwork_HasUserMedia() == false)
{
//Please send your angry emails directly to Google.
Debug.LogWarning("Browser does not allow access to navigator.mediaDevices. Local camera & microphone won't work."
+ "This can be caused by user settings, plugins or extensions! Some browsers (Chrome) only allow access to user media if the page is loaded using HTTPS / WSS only");
}
UpdateLogLevel_WebGl();
Byn.Awrtc.Browser.CAPI.Unity_InitAsync(Browser.CAPI.InitMode.WaitForDevices, true);
return true;
#else
return false;
#endif
}
private static AndroidInitConfig sAndroidConfig = null;
public static AndroidInitConfig AndroidConfig
{
get
{
return sAndroidConfig;
}
set
{
if (sInstance != null)
{
throw new InvalidOperationException("AndroidConfig can only be set before the factory is initialized!");
}
sAndroidConfig = value;
}
}
private static bool StaticInitAndroid()
{
#if UNITY_ANDROID
Debug.Log(LOG_PREFIX + "Initialize android specific plugin");
bool successful;
try
{
//get context from unity activity
AndroidJavaClass unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
AndroidJavaObject activity = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity");
AndroidJavaObject context = activity.Call<AndroidJavaObject>("getApplicationContext");
//get the java plugin
AndroidJavaClass wrtc = new AndroidJavaClass("com.because_why_not.wrtc.WrtcAndroid");
//Debug.Log("Using context " + context.GetHashCode());
//initialize the java side
bool javaSideSuccessful;
javaSideSuccessful = wrtc.CallStatic<bool>("init", context);
//2019-05-22: Any V0.984 and higher after this date won't need the workaround below.
//Pre 2019-05-22: Workaround for Bug0117: If Application.Quit is used unity destroys the C# side
//but java/C++ side remains initialized causing an error during the next init process.
//In this case the error can be ignored with the side effect
//that a real error might be ignored as well:
//if(javaSideSuccessful == false)
//{
// Debug.LogWarning("WORKAROUND to avoid problems with Application.Quit. Error of the java library ignored");
// javaSideSuccessful = true;
//}
if (javaSideSuccessful)
{
Debug.Log(LOG_PREFIX + "android java plugin initialized");
}
else
{
Debug.LogError(LOG_PREFIX + "Failed to initialize android java plugin. See android logcat output for error information!");
return false;
}
if(sAndroidConfig != null)
{
Debug.LogWarning("Calling ConfigureCodecs");
wrtc.CallStatic("ConfigureCodecs", sAndroidConfig.hardwareAcceleration, sAndroidConfig.useTextures, sAndroidConfig.preferredCodec, sAndroidConfig.forcePreferredCodec);
}
else
{
//for manual testing uncomment below
//default is false for everything and VP8 is preferred codec
/**
* Call before after init call but before the factory is initialized. will be ignored if changed
* after that.
*
* @param hardwareAcceleration true to use hardware acceleration with software fallback. false
* to force software codecs
* @param useTextures Tries to use textures as output format if possible (NOT YET SUPPORTED IN C++!!!)
* @param preferredCodec H264, VP8, VP9 (case sensitive), null for default
* @param force true will only allow the preferred codec
*/
//public static void ConfigureCodecs(boolean hardwareAcceleration, boolean useTextures, String preferredCodec, boolean force)
//
//only way to get it to work on the quest is to turn off hardware acceleration again and use VP8 or VP9
//wrtc.CallStatic("ConfigureCodecs", /*hardwareAcceleration*/ false, /*useTextures*/ false,/*preferredCodec*/ "VP9",/*force*/ false);
//works well on samsung galaxy S6 and S9
//wrtc.CallStatic("ConfigureCodecs", /*hardwareAcceleration*/ true, /*useTextures*/ false,/*preferredCodec*/ "H264",/*force*/ false);
}
//initialize the c++ side
bool cppSideSuccessful = WebRtcCSharp.RTCPeerConnectionFactory.InitAndroidContext(context.GetRawObject());
if (cppSideSuccessful)
{
Debug.Log(LOG_PREFIX + "android cpp plugin successful initialized.");
}
else
{
Debug.LogError(LOG_PREFIX + "Failed to initialize android native plugin. See android logcat output for error information!");
}
successful = true;
}
catch (Exception e)
{
successful = false;
Debug.LogError(LOG_PREFIX + "Android specific init process failed due to an exception.");
Debug.LogException(e);
}
return successful;
#else
return false;
#endif
}
/// <summary>
/// Creates a new ICall object.
/// Only use this method to ensure that your software will keep working on other platforms supported in
/// future versions of this library.
/// </summary>
/// <param name="config">Network configuration</param>
/// <returns></returns>
public ICall Create(NetworkConfig config = null)
{
ICall call = mFactory.CreateCall(config);
if (call == null)
{
Debug.LogError("Creation of call object failed. Platform not supported? Platform specific dll not included?");
}
else
{
if (IsNativePlatform())
{
EnterActiveCallState();
}
}
return call;
}
public IMediaNetwork CreateMediaNetwork(NetworkConfig config)
{
return mFactory.CreateMediaNetwork(config);
}
public IWebRtcNetwork CreateBasicNetwork(string websocketUrl, IceServer[] iceServers = null)
{
return mFactory.CreateBasicNetwork(websocketUrl, iceServers);
}
/// <summary>
/// Returns a list containing the names of all available video devices.
///
/// They can be used to select a certain device using the class
/// MediaConfiguration and the method ICall.Configuration.
/// </summary>
/// <returns>Returns a list of video devices </returns>
public string[] GetVideoDevices()
{
if (mFactory != null)
return mFactory.GetVideoDevices();
return new string[] { };
}
/// <summary>
/// True if the video device can be chosen by the application. False if the environment (the browser usually)
/// will automatically choose a suitable device.
/// </summary>
/// <returns></returns>
public bool CanSelectVideoDevice()
{
if (mFactory != null)
{
return mFactory.CanSelectVideoDevice();
}
else
{
return false;
}
}
/// <summary>
/// Can be used if the given device name is considered a front facing video device.
/// Result will be false if back-facing or unknown.
/// </summary>
/// <returns>
/// True for front facing. False for back-facing or unknown
/// </returns>
/// <param name="deviceName">Device name.</param>
public bool IsFrontFacing(string deviceName)
{
#if UNITY_ANDROID
//On Android we need to use a custom implementation.
//WebRTC might use a different camera API than unity thus the naming
//can be different
foreach (var dev in UnityCallFactory.Instance.GetVideoDevices())
{
if (deviceName == dev)
{
return AndroidHelper.IsFrontFacing(dev);
}
}
#else
//For other platforms we reuse unity's WebCamTexture to check for the front facing flag.
//This must be hidden from the Android C# compilation otherwise unity always adds video permission
foreach (var dev in WebCamTexture.devices)
{
if (deviceName == dev.name)
{
return dev.isFrontFacing;
}
}
#endif
//unknown video device
return false;
}
/// <summary>
/// This method will try to guess the default device. For mobile devices this is the front facing
/// camera and for others it is the first camera in the device list.
/// In some cases it might return "" which will leave the decision which device to pick to the
/// platform specific layer or the platform itself (e.g. based on users browser configuration).
/// </summary>
/// <returns>Returns a device name or "" if none could be determined.</returns>
public string GetDefaultVideoDevice()
{
if (CanSelectVideoDevice())
{
var devices = GetVideoDevices();
if (devices != null && devices.Length > 0)
{
//return a front facing one
foreach (var dev in devices)
{
if (IsFrontFacing(dev))
{
//found a front facing device
return dev;
}
}
//failed to find a front facing device
//return the first
return devices[0];
}
}
//user selection not supported
//or no device found
return "";
}
/// <summary>
/// Used to switch Unity and the platform into a state more suitable
/// for audio / video streaming
///
/// So far it will:
/// * prevent the device from sleeping
/// * switch Android into communication mode
///
/// Only supported for native platforms.
/// Further updates might add more.
/// </summary>
private void EnterActiveCallState()
{
if (mHasActiveCall == false)
{
if (KEEP_AWAKE)
{
mSleepTimeoutBackup = Screen.sleepTimeout;
Screen.sleepTimeout = SleepTimeout.NeverSleep;
}
#if UNITY_ANDROID && !UNITY_EDITOR
if (ANDROID_SET_COMMUNICATION_MODE)
{
Debug.Log(LOG_PREFIX + "Switching android audio mode to MODE_IN_COMMUNICATION");
mAndroidPreviousAudioMode = AndroidHelper.GetMode();
AndroidHelper.SetModeInCommunicaion();
}
#endif
}
mHasActiveCall = true;
}
/// <summary>
/// Cleanup after all calls finished.
/// Called by the native factories LastCallDisposed handler.
/// </summary>
private void LeaveActiveCallState()
{
if (mHasActiveCall)
{
if (KEEP_AWAKE)
{
Screen.sleepTimeout = mSleepTimeoutBackup;
}
#if UNITY_ANDROID && !UNITY_EDITOR
if (Application.platform == RuntimePlatform.Android)
{
if (ANDROID_SET_COMMUNICATION_MODE)
{
Debug.Log(LOG_PREFIX + "Restore previous audio mode: " + mAndroidPreviousAudioMode);
AndroidHelper.SetMode(mAndroidPreviousAudioMode);
}
}
#endif
}
mHasActiveCall = false;
}
/// <summary>
/// Destroys the factory.
/// </summary>
public void Dispose()
{
//Watch out this call might also be triggered if a user manually created an object
//without following the proper init process
//check first if this is actually the singleton
if (sInstance == this)
{
sInitState = InitState.Disposing;
//Always log this. Disposing the internal factory will trigger a chain reaction of
//destroying every single ICall, IMediaNetwork and IBasicNetwork instance and
//doing the same with its platform specific components. Native platforms will have
//to wait for multiple threads to cleanup their memory before stopping them and
//returning from the call. Plenty of space for bugs and crashes.
if (mFactory != null)
{
//Debug.Log(LOG_PREFIX + "is being destroyed. All created calls will be destroyed as well!");
mFactory.Dispose();
mFactory = null;
//Debug.Log(LOG_PREFIX + "destroyed.");
}
sInitState = InitState.Disposed;
}
}
/// <summary>
/// Mobile native only:
/// Turns on/off the phones speaker.
///
/// </summary>
/// <param name="state"></param>
public void SetLoudspeakerStatus(bool state)
{
#if UNITY_IOS
WebRtcCSharp.IosHelper.SetLoudspeakerStatus(state);
#elif UNITY_ANDROID
//see AndroidHelper for more detailed control over android specific audio settings
AndroidHelper.SetSpeakerOn(state);
#else
Debug.LogError(LOG_PREFIX + "GetLoudspeakerStatus is only supported on mobile platforms.");
#endif
}
/// <summary>
/// Checks if the phones speaker is turned on. Only for mobile native platforms
/// </summary>
/// <returns></returns>
public bool GetLoudspeakerStatus()
{
#if UNITY_IOS
return WebRtcCSharp.IosHelper.GetLoudspeakerStatus();
#elif UNITY_ANDROID
//android will just crash if GetLoudspeakerStatus is used
//workaround via java
return AndroidHelper.IsSpeakerOn();
#else
Debug.LogError(LOG_PREFIX + "GetLoudspeakerStatus is only supported on mobile platforms.");
return false;
#endif
}
/// <summary>
/// Use RequestLogLevelStatic instead
/// <param name="requestedLevel"></param>
/// </summary>
public void RequestLogLevel(LogLevel requestedLevel)
{
RequestLogLevelStatic(requestedLevel);
}
/// <summary>
/// Will route the log messages from the C# side plugin's SLog class to
/// the Unity log. This method will only change the log level if
/// a more detailed log level is requested than previously.
/// This is to allow multiple components that run in parallel to use this
/// call without overwriting each others logging.
///
/// There are some special cases due to platform specific code:
/// WebGL: The browser plugin code won't log trough the unity logging
/// system and instead use the browser API directly. The log level will
/// still be changed by this call but it won't use the "OnLog" callback
///
/// Native: By default the C++ side won't be logged via Unity as
/// Unity can get buggy by triggering callbacks from parallel native
/// threads.
/// You can activate c++ side logging via the flag NATIVE_VERBOSE_LOG.
/// Output of the native log is done based on the platform and
/// the OnLog callback is not used.
/// (e.g. Xcode debug console, Android LogCat, visual studio debug console, ...)
/// <param name="requestedLevel">Requested level log level.
/// Will be ignored if the current log level is more detailed already.
/// </param>
/// </summary>
public static void RequestLogLevelStatic(LogLevel requestedLevel)
{
if (sActiveLogLevel > requestedLevel)
{
SetLogLevel(requestedLevel);
}
}
/// <summary>
/// Same as RequestLogLevelStatic but will overwrite any previous log level set.
/// </summary>
/// <param name="newLogLevel"></param>
public static void SetLogLevel(LogLevel newLogLevel)
{
//bug.Log(GLOBAL_LOG_PREFIX + "Changing log level to " + newLogLevel);
if (sActiveLogLevel == LogLevel.None)
{
//the log level was off completely -> register callback
//Do this only once to avoid overwriting a custom logger
//that might have been set after this call.
SLog.SetLogger(OnLog);
}
sActiveLogLevel = newLogLevel;
#if UNITY_WEBGL && !UNITY_EDITOR
//If initialized this will immediately change the log level. If this is called
//before init then it will be set later once the library was loaded
UpdateLogLevel_WebGl();
#endif
}
/// <summary>
/// Obsolete.
/// Unlike the original version this call won't reduce the log level
/// anymore. If just a single call requests verbose logging it
/// will stay active until the application shuts down.
/// This is to make sure multiple components don't overwrite each
/// others log level.
///
/// </summary>
/// <param name="requireLogging"> true to turn on the default logger within
/// UnityCallFactory. Set false if your component doesn't require any
/// logging.</param>
/// <param name="requireVerboseLogging">true if your component requires verbose logging.
/// False if it doesn't.</param>
[Obsolete("Use RequestLogLeve instead!")]
public void SetDefaultLogger(bool requireLogging,
bool requireVerboseLogging = false)
{
if (requireLogging)
{
RequestLogLevel(LogLevel.Warning);
}
if (requireVerboseLogging)
{
RequestLogLevel(LogLevel.Info);
}
}
/// <summary>
/// Changes the state of the native log level.
/// Note this can heavily impact performance and should only be used for debugging.
/// </summary>
/// <param name="loglevel"></param>
public static void SetNativeLogLevel(LogLevel loglevel)
{
#if UNITY_WEBGL && !UNITY_EDITOR
Debug.LogWarning("SetNativeLogLevel to " + loglevel + " ignored. This is not supported on this platform.");
#else
sNativeLogLevel = loglevel;
//if this is called before init UpdateNativeLogLevel will be triggered later.
if (sInitState == InitState.Initialized)
{
UpdateNativeLogLevel();
}
#endif
}
/// <summary>
/// Synchronizes C# side log level value & platform specific implementation.
///
/// Note this assumes the dll's have been fully loaded and initialized before this is called!
/// </summary>
private static void UpdateNativeLogLevel()
{
#if (!UNITY_WSA && !UNITY_WEBGL) || UNITY_EDITOR
WebRtcCSharp.LoggingSeverity logsev = (WebRtcCSharp.LoggingSeverity)sNativeLogLevel;
Native.NativeAwrtcFactory.SetNativeLogLevel(logsev);
if (NATIVE_VERBOSE_LOG_TO_UNITY)
{
//routing to unity log
Awrtc.Native.NativeAwrtcFactory.SetNativeLogToSLog(logsev);
}
#endif
}
/// <summary>
/// Call to the java script WebGL plugin to update its logging
/// policy if possible.
/// </summary>
private static void UpdateLogLevel_WebGl()
{
#if UNITY_WEBGL
if (Byn.Awrtc.Browser.BrowserCallFactory.IsAvailable())
{
int logLevel = (int)sActiveLogLevel;
Browser.CAPI.Unity_SLog_SetLogLevel(logLevel);
}
#endif
}
/// <summary>
/// Default logging callback.
/// </summary>
/// <param name="msg">Message.</param>
/// <param name="tags">Tags.</param>
private static void OnLog(object msg, string[] tags)
{
if (sActiveLogLevel == LogLevel.None)
return;
StringBuilder builder = new StringBuilder();
bool warning = false;
bool error = false;
if (LOG_TIME)
{
string timePrefix = DateTime.Now.ToString("(HH:mm:ss.fff)");
builder.Append(timePrefix);
}
builder.Append("TAGS:[");
foreach (var v in tags)
{
builder.Append(v);
builder.Append(",");
if (v == SLog.TAG_ERROR || v == SLog.TAG_EXCEPTION)
{
error = true;
}
else if (v == SLog.TAG_WARNING)
{
warning = true;
}
}
builder.Append("]");
builder.Append(msg);
if (error && sActiveLogLevel <= LogLevel.Error)
{
UnityLog(builder.ToString(), Debug.LogError);
}
else if (warning && sActiveLogLevel <= LogLevel.Warning)
{
UnityLog(builder.ToString(), Debug.LogWarning);
}
else if (sActiveLogLevel <= LogLevel.Info)
{
//UnityLog(builder.ToString(), Debug.Log);
}
}
private static void UnityLog(string s, Action<object> logcall)
{
if (s.Length > ANDROID_LOG_LEN && Application.platform == RuntimePlatform.Android)
{
foreach (string splitMsg in SplitLongMsgs(s, ANDROID_LOG_LEN))
{
logcall(GLOBAL_LOG_PREFIX + splitMsg);
}
}
else
{
logcall(GLOBAL_LOG_PREFIX + s);
}
}
/// Used internally to split up long log messages.
/// This is to avoid having the messages being cut off
/// by platforms like Android.
/// </summary>
/// <param name="s">log message to split</param>
/// <param name="maxLength">Max length of each message</param>
/// <returns>The split up log message.</returns>
private static string[] SplitLongMsgs(string s, int maxLength)
{
int count = s.Length / maxLength + 1;
string[] messages = new string[count];
for (int i = 0; i < count; i++)
{
int start = i * maxLength;
int length = s.Length - start;
if (length > maxLength)
length = maxLength;
messages[i] = "[" + (i + 1) + "/" + count + "]" + s.Substring(start, length);
}
return messages;
}
/// <summary>
/// Log level used to decide which internal logs to print to the
/// unity logger.
/// </summary>
public enum LogLevel
{
/// <summary>
/// Not yet used
/// </summary>
Verbose = 0,
/// <summary>
/// Logs most events & enables special debug log output for debug builds
/// </summary>
Info = 1,
/// <summary>
/// Errors and warnings
/// </summary>
Warning = 2,
/// <summary>
/// Errors only
/// </summary>
Error = 3,
/// <summary>
/// Logger turned off
/// </summary>
None = 4
}
}
}