1072 lines
40 KiB
C#
1072 lines
40 KiB
C#
// ----------------------------------------------------------------------------
|
|
// <copyright file="Recorder.cs" company="Exit Games GmbH">
|
|
// Photon Voice for Unity - Copyright (C) 2018 Exit Games GmbH
|
|
// </copyright>
|
|
// <summary>
|
|
// Component that represents a client voice connection to Photon Servers.
|
|
// </summary>
|
|
// <author>developer@photonengine.com</author>
|
|
// ----------------------------------------------------------------------------
|
|
|
|
#define USE_NEW_TRANSPORT
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using ExitGames.Client.Photon;
|
|
using Photon.Realtime;
|
|
using UnityEngine;
|
|
using UnityEngine.Serialization;
|
|
#if UNITY_5_5_OR_NEWER
|
|
using UnityEngine.Profiling;
|
|
#endif
|
|
|
|
namespace Photon.Voice.Unity
|
|
{
|
|
/// <summary> Component that represents a client voice connection to Photon Servers. </summary>
|
|
[AddComponentMenu("Photon Voice/Voice Connection")]
|
|
[DisallowMultipleComponent]
|
|
[HelpURL("https://doc.photonengine.com/en-us/voice/v2/getting-started/voice-intro")]
|
|
public class VoiceConnection : ConnectionHandler, ILoggable
|
|
{
|
|
#region Private Fields
|
|
|
|
private VoiceLogger logger;
|
|
|
|
[SerializeField]
|
|
private DebugLevel logLevel = DebugLevel.INFO;
|
|
|
|
/// <summary>Key to save the "Best Region Summary" in the Player Preferences.</summary>
|
|
private const string PlayerPrefsKey = "VoiceCloudBestRegion";
|
|
|
|
private LoadBalancingTransport client;
|
|
[SerializeField]
|
|
private bool enableSupportLogger = false;
|
|
|
|
private SupportLogger supportLoggerComponent;
|
|
|
|
/// <summary>
|
|
/// time [ms] between consecutive SendOutgoingCommands calls
|
|
/// </summary>
|
|
[SerializeField]
|
|
private int updateInterval = 50;
|
|
|
|
private int nextSendTickCount;
|
|
|
|
#if UNITY_EDITOR || !UNITY_ANDROID && !UNITY_IOS
|
|
[SerializeField]
|
|
private bool runInBackground = true;
|
|
#endif
|
|
|
|
/// <summary>
|
|
/// time [ms] between statistics calculations
|
|
/// </summary>
|
|
[SerializeField]
|
|
private int statsResetInterval = 1000;
|
|
|
|
private int nextStatsTickCount;
|
|
|
|
private float statsReferenceTime;
|
|
private int referenceFramesLost;
|
|
private int referenceFramesReceived;
|
|
|
|
[SerializeField]
|
|
private GameObject speakerPrefab;
|
|
|
|
private bool cleanedUp;
|
|
|
|
protected List<RemoteVoiceLink> cachedRemoteVoices = new List<RemoteVoiceLink>();
|
|
|
|
[SerializeField]
|
|
[FormerlySerializedAs("PrimaryRecorder")]
|
|
private Recorder primaryRecorder;
|
|
|
|
private bool primaryRecorderInitialized;
|
|
|
|
[SerializeField]
|
|
private DebugLevel globalRecordersLogLevel = DebugLevel.INFO;
|
|
[SerializeField]
|
|
private DebugLevel globalSpeakersLogLevel = DebugLevel.INFO;
|
|
|
|
#pragma warning disable 414
|
|
[SerializeField]
|
|
[HideInInspector]
|
|
private int globalPlaybackDelay = 200;
|
|
#pragma warning restore 414
|
|
|
|
[SerializeField]
|
|
private PlaybackDelaySettings globalPlaybackDelaySettings = new PlaybackDelaySettings
|
|
{
|
|
MinDelaySoft = PlaybackDelaySettings.DEFAULT_LOW,
|
|
MaxDelaySoft = PlaybackDelaySettings.DEFAULT_HIGH,
|
|
MaxDelayHard = PlaybackDelaySettings.DEFAULT_MAX
|
|
};
|
|
|
|
private List<Speaker> linkedSpeakers = new List<Speaker>();
|
|
private List<Recorder> initializedRecorders = new List<Recorder>();
|
|
|
|
#endregion
|
|
|
|
#region Public Fields
|
|
|
|
/// <summary> Settings to be used by this voice connection</summary>
|
|
public AppSettings Settings;
|
|
#if UNITY_EDITOR
|
|
[HideInInspector]
|
|
public bool ShowSettings = true;
|
|
#endif
|
|
|
|
/// <summary> Special factory to link Speaker components with incoming remote audio streams</summary>
|
|
public Func<int, byte, object, Speaker> SpeakerFactory;
|
|
/// <summary> Fires when a speaker has been linked to a remote audio stream</summary>
|
|
public event Action<Speaker> SpeakerLinked;
|
|
/// <summary> Fires when a remote voice stream is added</summary>
|
|
public event Action<RemoteVoiceLink> RemoteVoiceAdded;
|
|
|
|
#if UNITY_PS4 || UNITY_SHARLIN
|
|
/// <summary>PlayStation user ID of the local user</summary>
|
|
/// <remarks>Pass the userID of the local PlayStation user who should receive any incoming audio. This value is used by Photon Voice when sending output to the headphones on the PlayStation.
|
|
/// If you don't provide a user ID, then Photon Voice uses the user ID of the user at index 0 in the list of local users
|
|
/// and in case that there are multiple local users, the audio output might be sent to the headphones of a different user than intended.</remarks>
|
|
public int PlayStationUserID = 0; // set from your games code
|
|
#endif
|
|
|
|
/// <summary>Configures the minimal Time.timeScale at which Voice client will dispatch incoming messages within LateUpdate.</summary>
|
|
/// <remarks>
|
|
/// It may make sense to dispatch incoming messages, even if the timeScale is near 0.
|
|
/// In some cases, stopping the game time makes sense, so this option defaults to -1f, which is "off".
|
|
/// Without dispatching messages, Voice client won't change state and does not handle updates.
|
|
/// </remarks>
|
|
public float MinimalTimeScaleToDispatchInFixedUpdate = -1f;
|
|
|
|
/// <summary> Auto instantiate a GameObject and attach a Speaker component to link to a remote audio stream if no candidate could be found </summary>
|
|
public bool AutoCreateSpeakerIfNotFound = true;
|
|
|
|
/// <summary>Limits the number of datagrams that are created in each LateUpdate.</summary>
|
|
/// <remarks>Helps spreading out sending of messages minimally.</remarks>
|
|
public int MaxDatagrams = 3;
|
|
|
|
/// <summary>Signals that outgoing messages should be sent in the next LateUpdate call.</summary>
|
|
/// <remarks>Up to MaxDatagrams are created to send queued messages.</remarks>
|
|
public bool SendAsap;
|
|
|
|
#endregion
|
|
|
|
#region Properties
|
|
/// <summary> Logger used by this component</summary>
|
|
public VoiceLogger Logger
|
|
{
|
|
get
|
|
{
|
|
if (this.logger == null)
|
|
{
|
|
this.logger = new VoiceLogger(this, string.Format("{0}.{1}", this.name, this.GetType().Name), this.logLevel);
|
|
}
|
|
return this.logger;
|
|
}
|
|
protected set { this.logger = value; }
|
|
}
|
|
/// <summary> Log level for this component</summary>
|
|
public DebugLevel LogLevel
|
|
{
|
|
get
|
|
{
|
|
if (this.Logger != null)
|
|
{
|
|
this.logLevel = this.Logger.LogLevel;
|
|
}
|
|
return this.logLevel;
|
|
}
|
|
set
|
|
{
|
|
this.logLevel = value;
|
|
if (this.Logger == null)
|
|
{
|
|
return;
|
|
}
|
|
this.Logger.LogLevel = this.logLevel;
|
|
}
|
|
}
|
|
|
|
public new LoadBalancingTransport Client
|
|
{
|
|
get
|
|
{
|
|
if (this.client == null)
|
|
{
|
|
#if USE_NEW_TRANSPORT
|
|
this.client = new LoadBalancingTransport2(this.Logger);
|
|
#else
|
|
this.client = new LoadBalancingTransport(this.Logger);
|
|
#endif
|
|
this.client.ClientType = ClientAppType.Voice;
|
|
this.client.VoiceClient.OnRemoteVoiceInfoAction += this.OnRemoteVoiceInfo;
|
|
this.client.StateChanged += this.OnVoiceStateChanged;
|
|
this.client.OpResponseReceived += this.OnOperationResponseReceived;
|
|
base.Client = this.client;
|
|
this.StartFallbackSendAckThread();
|
|
}
|
|
return this.client;
|
|
}
|
|
}
|
|
|
|
/// <summary>Returns underlying Photon Voice client.</summary>
|
|
public VoiceClient VoiceClient { get { return this.Client.VoiceClient; } }
|
|
|
|
/// <summary>Returns Photon Voice client state.</summary>
|
|
public ClientState ClientState { get { return this.Client.State; } }
|
|
|
|
/// <summary>Number of frames received per second.</summary>
|
|
public float FramesReceivedPerSecond { get; private set; }
|
|
/// <summary>Number of frames lost per second.</summary>
|
|
public float FramesLostPerSecond { get; private set; }
|
|
/// <summary>Percentage of lost frames.</summary>
|
|
public float FramesLostPercent { get; private set; }
|
|
|
|
/// <summary> Prefab that contains Speaker component to be instantiated when receiving a new remote audio source info</summary>
|
|
public GameObject SpeakerPrefab
|
|
{
|
|
get { return this.speakerPrefab; }
|
|
set
|
|
{
|
|
if (value != this.speakerPrefab)
|
|
{
|
|
if (value != null && value.GetComponentInChildren<Speaker>() == null)
|
|
{
|
|
#if UNITY_EDITOR
|
|
Debug.LogError("SpeakerPrefab must have a component of type Speaker in its hierarchy.", this);
|
|
#else
|
|
if (this.Logger.IsErrorEnabled)
|
|
{
|
|
this.Logger.LogError("SpeakerPrefab must have a component of type Speaker in its hierarchy.");
|
|
}
|
|
#endif
|
|
return;
|
|
}
|
|
this.speakerPrefab = value;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
#if UNITY_EDITOR
|
|
public List<RemoteVoiceLink> CachedRemoteVoices
|
|
{
|
|
get { return this.cachedRemoteVoices; }
|
|
}
|
|
#endif
|
|
|
|
/// <summary> Main Recorder to be used for transmission by default</summary>
|
|
public Recorder PrimaryRecorder
|
|
{
|
|
get
|
|
{
|
|
if (!this.primaryRecorderInitialized)
|
|
{
|
|
this.TryInitializePrimaryRecorder();
|
|
}
|
|
return this.primaryRecorder;
|
|
}
|
|
set
|
|
{
|
|
this.primaryRecorder = value;
|
|
this.primaryRecorderInitialized = false;
|
|
this.TryInitializePrimaryRecorder();
|
|
}
|
|
}
|
|
|
|
public DebugLevel GlobalRecordersLogLevel
|
|
{
|
|
get { return this.globalRecordersLogLevel; }
|
|
set
|
|
{
|
|
this.globalRecordersLogLevel = value;
|
|
for (int i = 0; i < this.initializedRecorders.Count; i++)
|
|
{
|
|
Recorder recorder = this.initializedRecorders[i];
|
|
if (!recorder.IgnoreGlobalLogLevel)
|
|
{
|
|
recorder.LogLevel = this.globalRecordersLogLevel;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public DebugLevel GlobalSpeakersLogLevel
|
|
{
|
|
get { return this.globalSpeakersLogLevel; }
|
|
set
|
|
{
|
|
this.globalSpeakersLogLevel = value;
|
|
for (int i = 0; i < this.linkedSpeakers.Count; i++)
|
|
{
|
|
Speaker speaker = this.linkedSpeakers[i];
|
|
if (!speaker.IgnoreGlobalLogLevel)
|
|
{
|
|
speaker.LogLevel = this.globalSpeakersLogLevel;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
[Obsolete("Use SetGlobalPlaybackDelayConfiguration methods instead")]
|
|
public int GlobalPlaybackDelay
|
|
{
|
|
get
|
|
{
|
|
return this.globalPlaybackDelaySettings.MinDelaySoft;
|
|
}
|
|
set
|
|
{
|
|
if (value >= 0 && value <= this.globalPlaybackDelaySettings.MaxDelaySoft)
|
|
{
|
|
this.globalPlaybackDelaySettings.MinDelaySoft = value;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>Used to store and access the "Best Region Summary" in the Player Preferences.</summary>
|
|
public string BestRegionSummaryInPreferences
|
|
{
|
|
get
|
|
{
|
|
return PlayerPrefs.GetString(PlayerPrefsKey, null);
|
|
}
|
|
set
|
|
{
|
|
if (string.IsNullOrEmpty(value))
|
|
{
|
|
PlayerPrefs.DeleteKey(PlayerPrefsKey);
|
|
}
|
|
else
|
|
{
|
|
PlayerPrefs.SetString(PlayerPrefsKey, value);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>Gets the global value in ms above which the audio player tries to keep the delay.</summary>
|
|
public int GlobalPlaybackDelayMinSoft
|
|
{
|
|
get
|
|
{
|
|
return this.globalPlaybackDelaySettings.MinDelaySoft;
|
|
}
|
|
}
|
|
|
|
/// <summary>Gets the global value in ms below which the audio player tries to keep the delay.</summary>
|
|
public int GlobalPlaybackDelayMaxSoft
|
|
{
|
|
get
|
|
{
|
|
return this.globalPlaybackDelaySettings.MaxDelaySoft;
|
|
}
|
|
}
|
|
|
|
/// <summary>Gets the global value in ms that audio play delay will not exceed.</summary>
|
|
public int GlobalPlaybackDelayMaxHard
|
|
{
|
|
get
|
|
{
|
|
return this.globalPlaybackDelaySettings.MaxDelayHard;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Public Methods
|
|
|
|
/// <summary>
|
|
/// Connect to Photon server using <see cref="Settings"/>
|
|
/// </summary>
|
|
/// <param name="overwriteSettings">Overwrites <see cref="Settings"/> before connecting</param>
|
|
/// <returns>If true voice connection command was sent from client</returns>
|
|
public bool ConnectUsingSettings(AppSettings overwriteSettings = null)
|
|
{
|
|
if (this.Client.LoadBalancingPeer.PeerState != PeerStateValue.Disconnected)
|
|
{
|
|
if (this.Logger.IsWarningEnabled)
|
|
{
|
|
this.Logger.LogWarning("ConnectUsingSettings() failed. Can only connect while in state 'Disconnected'. Current state: {0}", this.Client.LoadBalancingPeer.PeerState);
|
|
}
|
|
return false;
|
|
}
|
|
if (AppQuits)
|
|
{
|
|
if (this.Logger.IsWarningEnabled)
|
|
{
|
|
this.Logger.LogWarning("Can't connect: Application is closing. Unity called OnApplicationQuit().");
|
|
}
|
|
return false;
|
|
}
|
|
if (overwriteSettings != null)
|
|
{
|
|
this.Settings = overwriteSettings;
|
|
}
|
|
if (this.Settings == null)
|
|
{
|
|
if (this.Logger.IsErrorEnabled)
|
|
{
|
|
this.Logger.LogError("Settings are null");
|
|
}
|
|
return false;
|
|
}
|
|
if (string.IsNullOrEmpty(this.Settings.AppIdVoice) && string.IsNullOrEmpty(this.Settings.Server))
|
|
{
|
|
if (this.Logger.IsErrorEnabled)
|
|
{
|
|
this.Logger.LogError("Provide an AppId or a Server address in Settings to be able to connect");
|
|
}
|
|
return false;
|
|
}
|
|
if (this.Settings.IsMasterServerAddress && string.IsNullOrEmpty(this.Client.UserId))
|
|
{
|
|
this.Client.UserId = Guid.NewGuid().ToString(); // this is a workaround to use when connecting to self-hosted Photon Server v4, which does not return a UserId to the client if generated randomly server side
|
|
}
|
|
if (string.IsNullOrEmpty(this.Settings.BestRegionSummaryFromStorage))
|
|
{
|
|
this.Settings.BestRegionSummaryFromStorage = this.BestRegionSummaryInPreferences;
|
|
}
|
|
return this.client.ConnectUsingSettings(this.Settings);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes the Recorder component to be able to transmit audio.
|
|
/// </summary>
|
|
/// <param name="rec">The Recorder to be initialized.</param>
|
|
public void InitRecorder(Recorder rec)
|
|
{
|
|
if (rec == null)
|
|
{
|
|
if (this.Logger.IsErrorEnabled)
|
|
{
|
|
this.Logger.LogError("rec is null.");
|
|
}
|
|
return;
|
|
}
|
|
rec.Init(this);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the global configuration for the playback behaviour in case of delays.
|
|
/// </summary>
|
|
/// <param name="gpds">Playback delay configuration struct.</param>
|
|
public void SetPlaybackDelaySettings(PlaybackDelaySettings gpds)
|
|
{
|
|
this.SetGlobalPlaybackDelaySettings(gpds.MinDelaySoft, gpds.MaxDelaySoft, gpds.MaxDelayHard);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the global configuration for the playback behaviour in case of delays.
|
|
/// </summary>
|
|
/// <param name="low">In milliseconds, audio player tries to keep the playback delay above this value.</param>
|
|
/// <param name="high">In milliseconds, audio player tries to keep the playback below above this value.</param>
|
|
/// <param name="max">In milliseconds, audio player guarantees that the playback delay never exceeds this value.</param>
|
|
public void SetGlobalPlaybackDelaySettings(int low, int high, int max)
|
|
{
|
|
if (low >= 0 && low < high)
|
|
{
|
|
if (max < high)
|
|
{
|
|
max = high;
|
|
}
|
|
this.globalPlaybackDelaySettings.MinDelaySoft = low;
|
|
this.globalPlaybackDelaySettings.MaxDelaySoft = high;
|
|
this.globalPlaybackDelaySettings.MaxDelayHard = max;
|
|
for (int i = 0; i < this.linkedSpeakers.Count; i++)
|
|
{
|
|
this.linkedSpeakers[i].SetPlaybackDelaySettings(this.globalPlaybackDelaySettings);
|
|
}
|
|
}
|
|
else if (this.Logger.IsErrorEnabled)
|
|
{
|
|
this.Logger.LogError("Wrong playback delay config values, make sure 0 <= Low < High, low={0}, high={1}, max={2}", low, high, max);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tries to link local Speaker with remote voice stream using UserData.
|
|
/// Useful if Speaker created after stream is started.
|
|
/// </summary>
|
|
/// <param name="speaker">Speaker ot try linking.</param>
|
|
/// <param name="userData">UserData object used to bind local Speaker with remote voice stream.</param>
|
|
/// <returns></returns>
|
|
public virtual bool TryLateLinkingUsingUserData(Speaker speaker, object userData)
|
|
{
|
|
if (!speaker || speaker == null)
|
|
{
|
|
if (this.Logger.IsWarningEnabled)
|
|
{
|
|
this.Logger.LogWarning("Speaker is null or destroyed.");
|
|
}
|
|
return false;
|
|
}
|
|
if (speaker.IsLinked)
|
|
{
|
|
if (this.Logger.IsWarningEnabled)
|
|
{
|
|
this.Logger.LogWarning("Speaker already linked.");
|
|
}
|
|
return false;
|
|
}
|
|
if (!this.Client.InRoom)
|
|
{
|
|
if (this.Logger.IsWarningEnabled)
|
|
{
|
|
this.Logger.LogWarning("Client not joined to a voice room, client state: {0}.", Enum.GetName(typeof(ClientState), this.ClientState));
|
|
}
|
|
return false;
|
|
}
|
|
RemoteVoiceLink remoteVoice;
|
|
if (this.TryGetFirstVoiceStreamByUserData(userData, out remoteVoice))
|
|
{
|
|
if (this.Logger.IsInfoEnabled)
|
|
{
|
|
this.Logger.LogInfo("Speaker 'late-linking' for remoteVoice {0}.", remoteVoice);
|
|
}
|
|
this.LinkSpeaker(speaker, remoteVoice);
|
|
return speaker.IsLinked;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Private Methods
|
|
|
|
protected override void Awake()
|
|
{
|
|
base.Awake();
|
|
if (this.enableSupportLogger)
|
|
{
|
|
this.supportLoggerComponent = this.gameObject.AddComponent<SupportLogger>();
|
|
this.supportLoggerComponent.Client = this.Client;
|
|
this.supportLoggerComponent.LogTrafficStats = true;
|
|
}
|
|
#if UNITY_EDITOR || !UNITY_ANDROID && !UNITY_IOS
|
|
if (this.runInBackground)
|
|
{
|
|
Application.runInBackground = this.runInBackground;
|
|
}
|
|
#endif
|
|
if (!this.primaryRecorderInitialized)
|
|
{
|
|
this.TryInitializePrimaryRecorder();
|
|
}
|
|
}
|
|
|
|
protected virtual void Update()
|
|
{
|
|
this.VoiceClient.Service();
|
|
for (int i = 0; i < this.linkedSpeakers.Count; i++)
|
|
{
|
|
this.linkedSpeakers[i].Service();
|
|
}
|
|
for (int i = 0; i < this.initializedRecorders.Count; i++)
|
|
{
|
|
Recorder initializedRecorder = this.initializedRecorders[i];
|
|
if (initializedRecorder.MicrophoneDeviceChangeDetected)
|
|
{
|
|
initializedRecorder.HandleDeviceChange();
|
|
}
|
|
}
|
|
}
|
|
|
|
protected virtual void FixedUpdate()
|
|
{
|
|
#if VOICE_DISPATCH_IN_FIXEDUPDATE
|
|
this.Dispatch();
|
|
#elif VOICE_DISPATCH_IN_LATEUPDATE
|
|
// do not dispatch here
|
|
#else
|
|
if (Time.timeScale > this.MinimalTimeScaleToDispatchInFixedUpdate)
|
|
{
|
|
this.Dispatch();
|
|
}
|
|
#endif
|
|
}
|
|
|
|
/// <summary>Dispatches incoming network messages for Voice client. Called in FixedUpdate or LateUpdate.</summary>
|
|
/// <remarks>
|
|
/// It may make sense to dispatch incoming messages, even if the timeScale is near 0.
|
|
/// That can be configured with <see cref="MinimalTimeScaleToDispatchInFixedUpdate"/>.
|
|
///
|
|
/// Without dispatching messages, Voice client won't change state and does not handle updates.
|
|
/// </remarks>
|
|
protected void Dispatch()
|
|
{
|
|
bool doDispatch = true;
|
|
while (doDispatch)
|
|
{
|
|
// DispatchIncomingCommands() returns true of it found any command to dispatch (event, result or state change)
|
|
Profiler.BeginSample("[Photon Voice]: DispatchIncomingCommands");
|
|
doDispatch = this.Client.LoadBalancingPeer.DispatchIncomingCommands();
|
|
Profiler.EndSample();
|
|
}
|
|
}
|
|
|
|
private void LateUpdate()
|
|
{
|
|
#if VOICE_DISPATCH_IN_LATEUPDATE
|
|
this.Dispatch();
|
|
#elif VOICE_DISPATCH_IN_FIXEDUPDATE
|
|
// do not dispatch here
|
|
#else
|
|
// see MinimalTimeScaleToDispatchInFixedUpdate and FixedUpdate for explanation:
|
|
if (Time.timeScale <= this.MinimalTimeScaleToDispatchInFixedUpdate)
|
|
{
|
|
this.Dispatch();
|
|
}
|
|
#endif
|
|
|
|
int currentMsSinceStart = (int)(Time.realtimeSinceStartup * 1000); // avoiding Environment.TickCount, which could be negative on long-running platforms
|
|
if (this.SendAsap || currentMsSinceStart > this.nextSendTickCount)
|
|
{
|
|
this.SendAsap = false;
|
|
bool doSend = true;
|
|
int sendCounter = 0;
|
|
while (doSend && sendCounter < this.MaxDatagrams)
|
|
{
|
|
// Send all outgoing commands
|
|
Profiler.BeginSample("[Photon Voice]: SendOutgoingCommands");
|
|
doSend = this.Client.LoadBalancingPeer.SendOutgoingCommands();
|
|
sendCounter++;
|
|
Profiler.EndSample();
|
|
}
|
|
|
|
this.nextSendTickCount = currentMsSinceStart + this.updateInterval;
|
|
}
|
|
|
|
if (currentMsSinceStart > this.nextStatsTickCount)
|
|
{
|
|
if (this.statsResetInterval > 0)
|
|
{
|
|
this.CalcStatistics();
|
|
this.nextStatsTickCount = currentMsSinceStart + this.statsResetInterval;
|
|
}
|
|
}
|
|
}
|
|
|
|
protected override void OnDisable()
|
|
{
|
|
if (AppQuits)
|
|
{
|
|
this.CleanUp();
|
|
SupportClass.StopAllBackgroundCalls();
|
|
}
|
|
}
|
|
|
|
protected virtual void OnDestroy()
|
|
{
|
|
this.CleanUp();
|
|
}
|
|
|
|
protected virtual Speaker SimpleSpeakerFactory(int playerId, byte voiceId, object userData)
|
|
{
|
|
Speaker speaker = null;
|
|
if (this.SpeakerPrefab)
|
|
{
|
|
GameObject go = Instantiate(this.SpeakerPrefab);
|
|
Speaker[] speakers = go.GetComponentsInChildren<Speaker>(true);
|
|
if (speakers.Length > 0)
|
|
{
|
|
speaker = speakers[0];
|
|
if (speakers.Length > 1 && this.Logger.IsWarningEnabled)
|
|
{
|
|
this.Logger.LogWarning("Multiple Speaker components found attached to the GameObject (VoiceConnection.SpeakerPrefab) or its children. Using the first one we found.");
|
|
}
|
|
}
|
|
if (speaker == null)
|
|
{
|
|
if (this.Logger.IsErrorEnabled)
|
|
{
|
|
this.Logger.LogError("SpeakerPrefab does not have a component of type Speaker in its hierarchy.");
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
else if (this.AutoCreateSpeakerIfNotFound)
|
|
{
|
|
speaker = new GameObject().AddComponent<Speaker>();
|
|
}
|
|
else
|
|
{
|
|
return null;
|
|
}
|
|
|
|
// within a room, users are identified via the Realtime.Player class. this has a nickname and enables us to use custom properties, too
|
|
speaker.Actor = (this.Client.CurrentRoom != null) ? this.Client.CurrentRoom.GetPlayer(playerId) : null;
|
|
speaker.name = speaker.Actor != null && !string.IsNullOrEmpty(speaker.Actor.NickName) ? speaker.Actor.NickName : String.Format("Speaker for Player {0} Voice #{1}", playerId, voiceId);
|
|
speaker.OnRemoteVoiceRemoveAction += this.DeleteVoiceOnRemoteVoiceRemove;
|
|
return speaker;
|
|
}
|
|
|
|
internal void DeleteVoiceOnRemoteVoiceRemove(Speaker speaker)
|
|
{
|
|
if (speaker != null)
|
|
{
|
|
if (this.Logger.IsInfoEnabled)
|
|
{
|
|
this.Logger.LogInfo("Remote voice removed, delete speaker");
|
|
}
|
|
Destroy(speaker.gameObject);
|
|
}
|
|
}
|
|
|
|
private void OnRemoteVoiceInfo(int channelId, int playerId, byte voiceId, VoiceInfo voiceInfo, ref RemoteVoiceOptions options)
|
|
{
|
|
RemoteVoiceLink remoteVoice = new RemoteVoiceLink(voiceInfo, playerId, voiceId, channelId);
|
|
if (voiceInfo.Codec != Codec.AudioOpus)
|
|
{
|
|
if (this.Logger.IsDebugEnabled)
|
|
{
|
|
this.Logger.LogInfo("OnRemoteVoiceInfo skipped as codec is not Opus, {0}", remoteVoice);
|
|
}
|
|
return;
|
|
}
|
|
remoteVoice.Init(ref options);
|
|
|
|
if (this.Logger.IsInfoEnabled)
|
|
{
|
|
this.Logger.LogInfo("OnRemoteVoiceInfo {0}", remoteVoice);
|
|
}
|
|
for (int i = 0; i < this.cachedRemoteVoices.Count; i++)
|
|
{
|
|
RemoteVoiceLink remoteVoiceLink = this.cachedRemoteVoices[i];
|
|
if (remoteVoiceLink.Equals(remoteVoice))
|
|
{
|
|
if (this.Logger.IsWarningEnabled)
|
|
{
|
|
this.Logger.LogWarning("Possible duplicate remoteVoiceInfo cached:{0} vs. received:{1}", remoteVoiceLink, remoteVoice);
|
|
}
|
|
//this.cachedRemoteVoices.RemoveAt(i);
|
|
//break;
|
|
}
|
|
}
|
|
this.cachedRemoteVoices.Add(remoteVoice);
|
|
if (RemoteVoiceAdded != null)
|
|
{
|
|
RemoteVoiceAdded(remoteVoice);
|
|
}
|
|
remoteVoice.RemoteVoiceRemoved += delegate
|
|
{
|
|
if (this.Logger.IsInfoEnabled)
|
|
{
|
|
this.Logger.LogInfo("RemoteVoiceRemoved {0}", remoteVoice);
|
|
}
|
|
if (!this.cachedRemoteVoices.Remove(remoteVoice) && this.Logger.IsWarningEnabled)
|
|
{
|
|
this.Logger.LogWarning("Cached remote voice not removed {0}", remoteVoice);
|
|
}
|
|
};
|
|
Speaker speaker = null;
|
|
if (this.SpeakerFactory != null)
|
|
{
|
|
speaker = this.SpeakerFactory(playerId, voiceId, voiceInfo.UserData);
|
|
}
|
|
if (speaker == null)
|
|
{
|
|
speaker = this.SimpleSpeakerFactory(playerId, voiceId, voiceInfo.UserData);
|
|
}
|
|
else if (speaker.IsLinked)
|
|
{
|
|
if (this.Logger.IsWarningEnabled)
|
|
{
|
|
this.Logger.LogWarning("Overriding speaker link, old:{0} new:{1}", speaker.RemoteVoiceLink, remoteVoice);
|
|
}
|
|
speaker.OnRemoteVoiceRemove();
|
|
}
|
|
this.LinkSpeaker(speaker, remoteVoice);
|
|
}
|
|
|
|
protected virtual void OnVoiceStateChanged(ClientState fromState, ClientState toState)
|
|
{
|
|
if (this.Logger.IsDebugEnabled)
|
|
{
|
|
this.Logger.LogDebug("OnVoiceStateChanged from {0} to {1}", fromState, toState);
|
|
}
|
|
if (fromState == ClientState.Joined)
|
|
{
|
|
this.StopInitializedRecorders();
|
|
this.ClearRemoteVoicesCache();
|
|
}
|
|
switch (toState)
|
|
{
|
|
case ClientState.ConnectedToMasterServer:
|
|
{
|
|
if (this.Client.RegionHandler != null)
|
|
{
|
|
if (this.Settings != null)
|
|
{
|
|
this.Settings.BestRegionSummaryFromStorage = this.Client.RegionHandler.SummaryToCache;
|
|
}
|
|
this.BestRegionSummaryInPreferences = this.Client.RegionHandler.SummaryToCache;
|
|
}
|
|
break;
|
|
}
|
|
case ClientState.Joined:
|
|
{
|
|
this.StartInitializedRecorders();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
protected void CalcStatistics()
|
|
{
|
|
float now = Time.time;
|
|
int recv = this.VoiceClient.FramesReceived - this.referenceFramesReceived;
|
|
int lost = this.VoiceClient.FramesLost - this.referenceFramesLost;
|
|
float t = now - this.statsReferenceTime;
|
|
|
|
if (t > 0f)
|
|
{
|
|
if (recv + lost > 0)
|
|
{
|
|
this.FramesReceivedPerSecond = recv / t;
|
|
this.FramesLostPerSecond = lost / t;
|
|
this.FramesLostPercent = 100f * lost / (recv + lost);
|
|
}
|
|
else
|
|
{
|
|
this.FramesReceivedPerSecond = 0f;
|
|
this.FramesLostPerSecond = 0f;
|
|
this.FramesLostPercent = 0f;
|
|
}
|
|
}
|
|
|
|
this.referenceFramesReceived = this.VoiceClient.FramesReceived;
|
|
this.referenceFramesLost = this.VoiceClient.FramesLost;
|
|
this.statsReferenceTime = now;
|
|
}
|
|
|
|
private void CleanUp()
|
|
{
|
|
bool clientStillExists = this.client != null;
|
|
if (this.Logger.IsDebugEnabled)
|
|
{
|
|
this.Logger.LogDebug("Client exists? {0}, already cleaned up? {1}", clientStillExists, this.cleanedUp);
|
|
}
|
|
if (this.cleanedUp)
|
|
{
|
|
return;
|
|
}
|
|
this.StopFallbackSendAckThread();
|
|
if (clientStillExists)
|
|
{
|
|
this.client.StateChanged -= this.OnVoiceStateChanged;
|
|
this.client.OpResponseReceived -= this.OnOperationResponseReceived;
|
|
this.client.Disconnect();
|
|
if (this.client.LoadBalancingPeer != null)
|
|
{
|
|
this.client.LoadBalancingPeer.Disconnect();
|
|
this.client.LoadBalancingPeer.StopThread();
|
|
}
|
|
this.client.Dispose();
|
|
}
|
|
this.cleanedUp = true;
|
|
}
|
|
|
|
protected void LinkSpeaker(Speaker speaker, RemoteVoiceLink remoteVoice)
|
|
{
|
|
if (speaker != null)
|
|
{
|
|
if (!speaker.IgnoreGlobalLogLevel)
|
|
{
|
|
speaker.LogLevel = this.GlobalSpeakersLogLevel;
|
|
}
|
|
speaker.SetPlaybackDelaySettings(this.globalPlaybackDelaySettings);
|
|
#if UNITY_PS4 || UNITY_SHARLIN
|
|
speaker.PlayStationUserID = this.PlayStationUserID;
|
|
#endif
|
|
if (speaker.OnRemoteVoiceInfo(remoteVoice))
|
|
{
|
|
if (speaker.Actor == null)
|
|
{
|
|
if (this.Client.CurrentRoom == null)
|
|
{
|
|
if (this.Logger.IsErrorEnabled)
|
|
{
|
|
this.Logger.LogError("RemoteVoiceInfo event received while CurrentRoom is null");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Player player = this.Client.CurrentRoom.GetPlayer(remoteVoice.PlayerId);
|
|
if (player == null)
|
|
{
|
|
if (this.Logger.IsErrorEnabled)
|
|
{
|
|
this.Logger.LogError("RemoteVoiceInfo event received while respective actor not found in the room, {0}", remoteVoice);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
speaker.Actor = player;
|
|
}
|
|
}
|
|
}
|
|
if (this.Logger.IsInfoEnabled)
|
|
{
|
|
this.Logger.LogInfo("Speaker linked with remote voice {0}", remoteVoice);
|
|
}
|
|
this.linkedSpeakers.Add(speaker);
|
|
remoteVoice.RemoteVoiceRemoved += delegate
|
|
{
|
|
this.linkedSpeakers.Remove(speaker);
|
|
};
|
|
if (SpeakerLinked != null)
|
|
{
|
|
SpeakerLinked(speaker);
|
|
}
|
|
}
|
|
}
|
|
else if (this.Logger.IsWarningEnabled)
|
|
{
|
|
this.Logger.LogWarning("Speaker is null. Remote voice {0} not linked.", remoteVoice);
|
|
}
|
|
}
|
|
|
|
private void ClearRemoteVoicesCache()
|
|
{
|
|
if (this.cachedRemoteVoices.Count > 0)
|
|
{
|
|
if (this.Logger.IsInfoEnabled)
|
|
{
|
|
this.Logger.LogInfo("{0} cached remote voices info cleared", this.cachedRemoteVoices.Count);
|
|
}
|
|
this.cachedRemoteVoices.Clear();
|
|
}
|
|
}
|
|
|
|
private void TryInitializePrimaryRecorder()
|
|
{
|
|
if (this.primaryRecorder != null)
|
|
{
|
|
if (!this.primaryRecorder.IsInitialized)
|
|
{
|
|
this.primaryRecorder.Init(this);
|
|
}
|
|
this.primaryRecorderInitialized = this.primaryRecorder.IsInitialized;
|
|
}
|
|
}
|
|
|
|
#if UNITY_EDITOR
|
|
private void OnValidate()
|
|
{
|
|
if (this.globalPlaybackDelay > 0)
|
|
{
|
|
if (this.globalPlaybackDelaySettings.MinDelaySoft != this.globalPlaybackDelay)
|
|
{
|
|
this.globalPlaybackDelaySettings.MinDelaySoft = this.globalPlaybackDelay;
|
|
if (this.globalPlaybackDelaySettings.MaxDelaySoft <= this.globalPlaybackDelaySettings.MinDelaySoft)
|
|
{
|
|
this.globalPlaybackDelaySettings.MaxDelaySoft = 2 * this.globalPlaybackDelaySettings.MinDelaySoft;
|
|
if (this.globalPlaybackDelaySettings.MaxDelayHard < this.globalPlaybackDelaySettings.MaxDelaySoft)
|
|
{
|
|
this.globalPlaybackDelaySettings.MaxDelayHard = this.globalPlaybackDelaySettings.MaxDelaySoft + 1000;
|
|
}
|
|
}
|
|
}
|
|
this.globalPlaybackDelay = -1;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
internal void AddInitializedRecorder(Recorder rec)
|
|
{
|
|
this.initializedRecorders.Add(rec);
|
|
}
|
|
|
|
internal void RemoveInitializedRecorder(Recorder rec)
|
|
{
|
|
this.initializedRecorders.Remove(rec);
|
|
}
|
|
|
|
private void StartInitializedRecorders()
|
|
{
|
|
for (int i = 0; i < this.initializedRecorders.Count; i++)
|
|
{
|
|
Recorder rec = this.initializedRecorders[i];
|
|
rec.CheckAndAutoStart();
|
|
}
|
|
}
|
|
|
|
private void StopInitializedRecorders()
|
|
{
|
|
for (int i = 0; i < this.initializedRecorders.Count; i++)
|
|
{
|
|
Recorder rec = this.initializedRecorders[i];
|
|
if (rec.IsRecording && rec.RecordOnlyWhenJoined)
|
|
{
|
|
rec.StopRecordingInternal();
|
|
}
|
|
}
|
|
}
|
|
|
|
private bool TryGetFirstVoiceStreamByUserData(object userData, out RemoteVoiceLink remoteVoiceLink)
|
|
{
|
|
remoteVoiceLink = null;
|
|
if (userData == null)
|
|
{
|
|
return false;
|
|
}
|
|
if (this.Logger.IsWarningEnabled)
|
|
{
|
|
int found = 0;
|
|
for (int i = 0; i < this.cachedRemoteVoices.Count; i++)
|
|
{
|
|
RemoteVoiceLink remoteVoice = this.cachedRemoteVoices[i];
|
|
if (userData.Equals(remoteVoice.Info.UserData))
|
|
{
|
|
found++;
|
|
if (found == 1)
|
|
{
|
|
remoteVoiceLink = remoteVoice;
|
|
if (this.Logger.IsDebugEnabled)
|
|
{
|
|
this.Logger.LogWarning("(first) remote voice stream found by UserData:{0}", userData, remoteVoice);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
this.Logger.LogWarning("{0} remote voice stream found (so far) using same UserData:{0}", found, remoteVoice);
|
|
}
|
|
}
|
|
}
|
|
return found > 0;
|
|
}
|
|
for (int i = 0; i < this.cachedRemoteVoices.Count; i++)
|
|
{
|
|
RemoteVoiceLink remoteVoice = this.cachedRemoteVoices[i];
|
|
if (userData.Equals(remoteVoice.Info.UserData))
|
|
{
|
|
remoteVoiceLink = remoteVoice;
|
|
if (this.Logger.IsDebugEnabled)
|
|
{
|
|
this.Logger.LogWarning("(first) remote voice stream found by UserData:{0}", userData, remoteVoice);
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
protected virtual void OnOperationResponseReceived(OperationResponse operationResponse)
|
|
{
|
|
if (this.Logger.IsErrorEnabled && operationResponse.ReturnCode != ErrorCode.Ok && (operationResponse.OperationCode != OperationCode.JoinRandomGame || operationResponse.ReturnCode == ErrorCode.NoRandomMatchFound))
|
|
{
|
|
this.Logger.LogError("Operation {0} response error code {1} message {2}", operationResponse.OperationCode, operationResponse.ReturnCode, operationResponse.DebugMessage);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
}
|
|
|
|
namespace Photon.Voice
|
|
{
|
|
[Obsolete("Class renamed. Use LoadBalancingTransport instead.")]
|
|
public class LoadBalancingFrontend : LoadBalancingTransport
|
|
{
|
|
}
|
|
} |