// ----------------------------------------------------------------------------
//
// Photon Voice for Unity - Copyright (C) 2018 Exit Games GmbH
//
//
// Component that represents a client voice connection to Photon Servers.
//
// developer@photonengine.com
// ----------------------------------------------------------------------------
#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
{
/// Component that represents a client voice connection to Photon Servers.
[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;
/// Key to save the "Best Region Summary" in the Player Preferences.
private const string PlayerPrefsKey = "VoiceCloudBestRegion";
private LoadBalancingTransport client;
[SerializeField]
private bool enableSupportLogger = false;
private SupportLogger supportLoggerComponent;
///
/// time [ms] between consecutive SendOutgoingCommands calls
///
[SerializeField]
private int updateInterval = 50;
private int nextSendTickCount;
#if UNITY_EDITOR || !UNITY_ANDROID && !UNITY_IOS
[SerializeField]
private bool runInBackground = true;
#endif
///
/// time [ms] between statistics calculations
///
[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 cachedRemoteVoices = new List();
[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 linkedSpeakers = new List();
private List initializedRecorders = new List();
#endregion
#region Public Fields
/// Settings to be used by this voice connection
public AppSettings Settings;
#if UNITY_EDITOR
[HideInInspector]
public bool ShowSettings = true;
#endif
/// Special factory to link Speaker components with incoming remote audio streams
public Func SpeakerFactory;
/// Fires when a speaker has been linked to a remote audio stream
public event Action SpeakerLinked;
/// Fires when a remote voice stream is added
public event Action RemoteVoiceAdded;
#if UNITY_PS4 || UNITY_SHARLIN
/// PlayStation user ID of the local user
/// 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.
public int PlayStationUserID = 0; // set from your games code
#endif
/// Configures the minimal Time.timeScale at which Voice client will dispatch incoming messages within LateUpdate.
///
/// 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.
///
public float MinimalTimeScaleToDispatchInFixedUpdate = -1f;
/// Auto instantiate a GameObject and attach a Speaker component to link to a remote audio stream if no candidate could be found
public bool AutoCreateSpeakerIfNotFound = true;
/// Limits the number of datagrams that are created in each LateUpdate.
/// Helps spreading out sending of messages minimally.
public int MaxDatagrams = 3;
/// Signals that outgoing messages should be sent in the next LateUpdate call.
/// Up to MaxDatagrams are created to send queued messages.
public bool SendAsap;
#endregion
#region Properties
/// Logger used by this component
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; }
}
/// Log level for this component
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;
}
}
/// Returns underlying Photon Voice client.
public VoiceClient VoiceClient { get { return this.Client.VoiceClient; } }
/// Returns Photon Voice client state.
public ClientState ClientState { get { return this.Client.State; } }
/// Number of frames received per second.
public float FramesReceivedPerSecond { get; private set; }
/// Number of frames lost per second.
public float FramesLostPerSecond { get; private set; }
/// Percentage of lost frames.
public float FramesLostPercent { get; private set; }
/// Prefab that contains Speaker component to be instantiated when receiving a new remote audio source info
public GameObject SpeakerPrefab
{
get { return this.speakerPrefab; }
set
{
if (value != this.speakerPrefab)
{
if (value != null && value.GetComponentInChildren() == 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 CachedRemoteVoices
{
get { return this.cachedRemoteVoices; }
}
#endif
/// Main Recorder to be used for transmission by default
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;
}
}
}
/// Used to store and access the "Best Region Summary" in the Player Preferences.
public string BestRegionSummaryInPreferences
{
get
{
return PlayerPrefs.GetString(PlayerPrefsKey, null);
}
set
{
if (string.IsNullOrEmpty(value))
{
PlayerPrefs.DeleteKey(PlayerPrefsKey);
}
else
{
PlayerPrefs.SetString(PlayerPrefsKey, value);
}
}
}
/// Gets the global value in ms above which the audio player tries to keep the delay.
public int GlobalPlaybackDelayMinSoft
{
get
{
return this.globalPlaybackDelaySettings.MinDelaySoft;
}
}
/// Gets the global value in ms below which the audio player tries to keep the delay.
public int GlobalPlaybackDelayMaxSoft
{
get
{
return this.globalPlaybackDelaySettings.MaxDelaySoft;
}
}
/// Gets the global value in ms that audio play delay will not exceed.
public int GlobalPlaybackDelayMaxHard
{
get
{
return this.globalPlaybackDelaySettings.MaxDelayHard;
}
}
#endregion
#region Public Methods
///
/// Connect to Photon server using
///
/// Overwrites before connecting
/// If true voice connection command was sent from client
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);
}
///
/// Initializes the Recorder component to be able to transmit audio.
///
/// The Recorder to be initialized.
public void InitRecorder(Recorder rec)
{
if (rec == null)
{
if (this.Logger.IsErrorEnabled)
{
this.Logger.LogError("rec is null.");
}
return;
}
rec.Init(this);
}
///
/// Sets the global configuration for the playback behaviour in case of delays.
///
/// Playback delay configuration struct.
public void SetPlaybackDelaySettings(PlaybackDelaySettings gpds)
{
this.SetGlobalPlaybackDelaySettings(gpds.MinDelaySoft, gpds.MaxDelaySoft, gpds.MaxDelayHard);
}
///
/// Sets the global configuration for the playback behaviour in case of delays.
///
/// In milliseconds, audio player tries to keep the playback delay above this value.
/// In milliseconds, audio player tries to keep the playback below above this value.
/// In milliseconds, audio player guarantees that the playback delay never exceeds this value.
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);
}
}
///
/// Tries to link local Speaker with remote voice stream using UserData.
/// Useful if Speaker created after stream is started.
///
/// Speaker ot try linking.
/// UserData object used to bind local Speaker with remote voice stream.
///
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();
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
}
/// Dispatches incoming network messages for Voice client. Called in FixedUpdate or LateUpdate.
///
/// It may make sense to dispatch incoming messages, even if the timeScale is near 0.
/// That can be configured with .
///
/// Without dispatching messages, Voice client won't change state and does not handle updates.
///
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(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();
}
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
{
}
}