DeltaVR/Assets/Photon/PhotonVoice/Code/VoiceConnection.cs
2022-06-29 14:45:17 +03:00

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
{
}
}