361 lines
13 KiB
C#
361 lines
13 KiB
C#
/*
|
|
* Copyright (C) 2021 because-why-not.com Limited
|
|
*
|
|
* Please refer to the license.txt for license information
|
|
*/
|
|
using Byn.Awrtc;
|
|
using Byn.Awrtc.Unity;
|
|
using Byn.Unity.Examples;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
using UnityEngine.UI;
|
|
|
|
namespace SHINT.WebRTC
|
|
{
|
|
public class Caller : MonoBehaviour
|
|
{
|
|
private const int MAX_CODE_LENGTH = 256;
|
|
|
|
public GameObject uVideoLayout;
|
|
public GameObject uVideoPrefab;
|
|
public Texture2D uNoImgTexture;
|
|
|
|
private ICall mCall;
|
|
public MediaConfig MediaConfig = new MediaConfig();
|
|
|
|
public NetworkConfig NetConfig = new NetworkConfig();
|
|
//public NetworkConfig NetConfig { get { return mNetConfig; } set { mNetConfig = value; } }
|
|
public RectTransform InCallPanel;
|
|
private class VideoData
|
|
{
|
|
public GameObject uiObject;
|
|
public Texture2D texture;
|
|
public RawImage image;
|
|
|
|
}
|
|
private Dictionary<ConnectionId, VideoData> mVideoUiElements = new Dictionary<ConnectionId, VideoData>();
|
|
|
|
public int ConnectionCount
|
|
{
|
|
get
|
|
{
|
|
return mVideoUiElements.Count - 1;
|
|
}
|
|
}
|
|
|
|
private string mOwnUserName = "User";
|
|
|
|
Dictionary<ConnectionId, string> mIdToUser;
|
|
|
|
protected virtual void OnCallFactoryReady()
|
|
{
|
|
//to trigger android permission requests
|
|
StartCoroutine(ExampleGlobals.RequestPermissions());
|
|
//use video and audio by default (the UI is toggled on by default as well it will change on click )
|
|
MediaConfig.Video = true;
|
|
MediaConfig.Audio = true;
|
|
MediaConfig.VideoDeviceName = UnityCallFactory.Instance.GetDefaultVideoDevice();
|
|
|
|
NetConfig.KeepSignalingAlive = true;
|
|
NetConfig.MaxIceRestart = 5;
|
|
IceServer ice = new IceServer("turn:t.y-not.app:443", "user_nov", "pass_nov");
|
|
NetConfig.IceServers.Add(ice);
|
|
NetConfig.SignalingUrl = "wss://s.y-not.app/testshared";
|
|
NetConfig.IsConference = true;
|
|
onActivate();
|
|
//Debug.Log("OnCallFactoryReady");
|
|
}
|
|
|
|
protected virtual void OnCallFactoryFailed(string error)
|
|
{
|
|
string fullErrorMsg = typeof(CallApp).Name + " can't start. The " + typeof(UnityCallFactory).Name + " failed to initialize with following error: " + error;
|
|
Debug.LogError(fullErrorMsg);
|
|
}
|
|
|
|
|
|
public event Action onActivate;
|
|
public void Active()
|
|
{
|
|
//Append("Setting up ...");
|
|
UnityCallFactory.RequestLogLevelStatic(UnityCallFactory.LogLevel.Info);
|
|
UnityCallFactory.EnsureInit(OnCallFactoryReady, OnCallFactoryFailed);
|
|
//lets just give them a random number for now.
|
|
mOwnUserName = mOwnUserName + "_" + (int)UnityEngine.Random.Range(0, 10000);
|
|
mIdToUser = new Dictionary<ConnectionId, string>();
|
|
}
|
|
public void Setup(bool useAudio = true, bool useVideo = true)
|
|
{
|
|
//setup the server
|
|
// Debug.Log("Creating ICall with " + NetConfig);
|
|
mCall = UnityCallFactory.Instance.Create(NetConfig);
|
|
if (mCall == null)
|
|
{
|
|
//Append("Failed to create the call");
|
|
return;
|
|
}
|
|
|
|
//Append("Call created!");
|
|
mCall.CallEvent += Call_CallEvent;
|
|
|
|
//setup local video element
|
|
SetupVideoUi(ConnectionId.INVALID);
|
|
mCall.Configure(MediaConfig);
|
|
onSetupEnd();
|
|
}
|
|
public event Action onSetupEnd;
|
|
|
|
/// <summary>
|
|
/// Destroys the call object and shows the setup screen again.
|
|
/// Called after a call ends or an error occurred.
|
|
/// </summary>
|
|
private void ResetCall()
|
|
{
|
|
foreach (var v in mVideoUiElements)
|
|
{
|
|
Destroy(v.Value.uiObject);
|
|
if (v.Value.texture != null)
|
|
Destroy(v.Value.texture);
|
|
}
|
|
mVideoUiElements.Clear();
|
|
CleanupCall();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handler of call events.
|
|
/// </summary>
|
|
/// <param name="sender"></param>
|
|
/// <param name="e"></param>
|
|
protected virtual void Call_CallEvent(object sender, CallEventArgs e)
|
|
{
|
|
switch (e.Type)
|
|
{
|
|
case CallEventType.CallAccepted:
|
|
//Outgoing call was successful or an incoming call arrived
|
|
OnNewCall(e as CallAcceptedEventArgs);
|
|
break;
|
|
case CallEventType.CallEnded:
|
|
OnCallEnded(e as CallEndedEventArgs);
|
|
break;
|
|
case CallEventType.ListeningFailed:
|
|
Append("Failed to listen for incoming calls! Server might be down!");
|
|
ResetCall();
|
|
break;
|
|
|
|
case CallEventType.ConnectionFailed:
|
|
{
|
|
//this should be impossible to happen in conference mode!
|
|
ErrorEventArgs args = e as ErrorEventArgs;
|
|
Append("Error: " + args.Info);
|
|
Debug.LogError(args.Info);
|
|
ResetCall();
|
|
}
|
|
break;
|
|
|
|
case CallEventType.FrameUpdate:
|
|
//new frame received from webrtc (either from local camera or network)
|
|
FrameUpdateEventArgs frameargs = e as FrameUpdateEventArgs;
|
|
UpdateFrame(frameargs);
|
|
break;
|
|
case CallEventType.Message:
|
|
{
|
|
//text message received
|
|
MessageEventArgs args = e as MessageEventArgs;
|
|
|
|
//due to timing issues it can happen that a message arrives before we get the NewUser notification
|
|
//if we get a message from a not yet known user we add them here
|
|
if (mIdToUser.ContainsKey(args.ConnectionId) == false)
|
|
{
|
|
AddNewConnection(args.ConnectionId);
|
|
}
|
|
|
|
if (mIdToUser[args.ConnectionId] == "unknown")
|
|
{
|
|
//don't know this user yet. First message is expected to be their username
|
|
string name = args.Content;
|
|
OnNewUserDiscovered(name, args.ConnectionId);
|
|
}
|
|
else
|
|
{
|
|
//known user so we likely got a regular text message form them
|
|
string name = mIdToUser[args.ConnectionId];
|
|
Append(name + ":" + args.Content);
|
|
}
|
|
break;
|
|
}
|
|
case CallEventType.WaitForIncomingCall:
|
|
{
|
|
//the chat app will wait for another app to connect via the same string
|
|
WaitForIncomingCallEventArgs args = e as WaitForIncomingCallEventArgs;
|
|
Append("Waiting for incoming call address: " + args.Address);
|
|
break;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
/// <summary>
|
|
/// Event triggers for a new incoming call
|
|
/// (in conference mode there is no difference between incoming / outgoing)
|
|
/// </summary>
|
|
/// <param name="args"></param>
|
|
private void OnNewCall(CallAcceptedEventArgs args)
|
|
{
|
|
SetupVideoUi(args.ConnectionId);
|
|
AddNewConnection(args.ConnectionId);
|
|
//let them know our username!
|
|
mCall.Send(mOwnUserName);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds a new user. Can be called several times without adding the user twice
|
|
/// </summary>
|
|
/// <param name="id">
|
|
/// ConnectionId the new user
|
|
/// </param>
|
|
private void AddNewConnection(ConnectionId id)
|
|
{
|
|
//new connection. we do not know who that is yet until we get the first message!
|
|
if (mIdToUser.ContainsKey(id) == false)
|
|
{
|
|
mIdToUser[id] = "unknown";
|
|
Append("New connection with ID " + id + " username not yet known");
|
|
}
|
|
|
|
}
|
|
|
|
private void OnNewUserDiscovered(string name, ConnectionId id)
|
|
{
|
|
//Debug.Log("Received first message from ConnectionId " + id + "! Their username is " + name);
|
|
//store for later use
|
|
mIdToUser[id] = name;
|
|
//Append("New user discovered name: " + name + " and connection id: " + id);
|
|
}
|
|
|
|
private void OnUserLeft(ConnectionId id)
|
|
{
|
|
if (mIdToUser.ContainsKey(id))
|
|
{
|
|
string name = mIdToUser[id];
|
|
//Append("User with name " + name + " and local ID " + id + " got disconnected");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates the connection specific data / ui
|
|
/// </summary>
|
|
/// <param name="id"></param>
|
|
private void SetupVideoUi(ConnectionId id)
|
|
{
|
|
//create texture + ui element
|
|
VideoData vd = new VideoData();
|
|
vd.uiObject = Instantiate(uVideoPrefab);
|
|
vd.uiObject.transform.SetParent(uVideoLayout.transform, false);
|
|
vd.image = vd.uiObject.GetComponentInChildren<RawImage>();
|
|
vd.image.texture = uNoImgTexture;
|
|
mVideoUiElements[id] = vd;
|
|
}
|
|
|
|
/// <summary>
|
|
/// User left. Cleanup connection specific data / ui
|
|
/// </summary>
|
|
/// <param name="args"></param>
|
|
private void OnCallEnded(CallEndedEventArgs args)
|
|
{
|
|
VideoData data;
|
|
if (mVideoUiElements.TryGetValue(args.ConnectionId, out data))
|
|
{
|
|
if (data.texture != null)
|
|
Destroy(data.texture);
|
|
Destroy(data.uiObject);
|
|
mVideoUiElements.Remove(args.ConnectionId);
|
|
}
|
|
|
|
OnUserLeft(args.ConnectionId);
|
|
}
|
|
|
|
private void UpdateFrame(FrameUpdateEventArgs args)
|
|
{
|
|
if (mVideoUiElements.ContainsKey(args.ConnectionId))
|
|
{
|
|
VideoData videoData = mVideoUiElements[args.ConnectionId];
|
|
//make sure not to overwrite / destroy our texture for missing image data
|
|
if (videoData.image.texture == this.uNoImgTexture)
|
|
videoData.image.texture = null;
|
|
bool mirror = args.IsRemote == false;
|
|
//converts the frame data to a texture and sets it to the raw image
|
|
UnityMediaHelper.UpdateRawImageTransform(videoData.image, args.Frame, mirror);
|
|
videoData.texture = videoData.image.texture as Texture2D;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Destroys the call. Used if unity destroys the object or if a call
|
|
/// ended / failed due to an error.
|
|
///
|
|
/// </summary>
|
|
private void CleanupCall()
|
|
{
|
|
if (mCall != null)
|
|
{
|
|
|
|
Debug.Log("Destroying call!");
|
|
mCall.Dispose();
|
|
mCall = null;
|
|
Debug.Log("Call destroyed");
|
|
}
|
|
}
|
|
private void OnDestroy()
|
|
{
|
|
CleanupCall();
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// toggle audio on / off
|
|
/// </summary>
|
|
/// <param name="state"></param>
|
|
public void AudioToggle(bool state)
|
|
{
|
|
MediaConfig.Audio = state;
|
|
}
|
|
|
|
/// <summary>
|
|
/// toggle video on / off
|
|
/// </summary>
|
|
/// <param name="state"></param>
|
|
public void VideoToggle(bool state)
|
|
{
|
|
MediaConfig.Video = state;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds a new message to the message view
|
|
/// </summary>
|
|
/// <param name="text"></param>
|
|
private void Append(string text)
|
|
{
|
|
Debug.Log("Chat: " + text);
|
|
}
|
|
|
|
public void Listen(string address)
|
|
{
|
|
mCall.Listen(address);
|
|
}
|
|
|
|
/// <summary>
|
|
/// The call object needs to be updated regularly to sync data received via webrtc with
|
|
/// unity. All events will be triggered during the update method in the unity main thread
|
|
/// to avoid multi threading errors
|
|
/// </summary>
|
|
private void Update()
|
|
{
|
|
if (mCall != null)
|
|
{
|
|
//update the call
|
|
mCall.Update();
|
|
}
|
|
}
|
|
}
|
|
|
|
} |