// ---------------------------------------------------------------------------- // // Photon Voice - Copyright (C) 2018 Exit Games GmbH // // // This class can be used to automatically join/leave Voice rooms when // Photon Unity Networking (PUN) joins or leaves its rooms. The Voice room // will use the same name as PUN, but with a "_voice_" postfix. // It also sets a custom PUN Speaker factory to find the Speaker // component for a character's voice. For this to work, the voice's UserData // must be set to the character's PhotonView ID. // (see "PhotonVoiceView.cs") // // developer@photonengine.com // ---------------------------------------------------------------------------- using UnityEngine; using Photon.Pun; using Photon.Realtime; using Photon.Voice.Unity; namespace Photon.Voice.PUN { /// /// This class can be used to automatically sync client states between PUN and Voice. /// It also sets a custom PUN Speaker factory to find the Speaker component for a character's voice. /// For this to work attach a next to the of your player's prefab. /// [DisallowMultipleComponent] [AddComponentMenu("Photon Voice/Photon Voice Network")] [HelpURL("https://doc.photonengine.com/en-us/voice/v2/getting-started/voice-for-pun")] public class PhotonVoiceNetwork : VoiceConnection { #region Public Fields /// Suffix for voice room names appended to PUN room names. public const string VoiceRoomNameSuffix = "_voice_"; /// Auto connect voice client and join a voice room when PUN client is joined to a PUN room public bool AutoConnectAndJoin = true; /// Auto disconnect voice client when PUN client is not joined to a PUN room public bool AutoLeaveAndDisconnect = true; /// Whether or not Photon Voice client should follow PUN client if the latter is in offline mode. public bool WorkInOfflineMode = true; #endregion #region Private Fields private EnterRoomParams voiceRoomParams = new EnterRoomParams { RoomOptions = new RoomOptions { IsVisible = false } }; private bool clientCalledConnectAndJoin; private bool clientCalledDisconnect; private bool clientCalledConnectOnly; private bool internalDisconnect; private bool internalConnect; private static object instanceLock = new object(); private static PhotonVoiceNetwork instance; private static bool instantiated; [SerializeField] private bool usePunAppSettings = true; [SerializeField] private bool usePunAuthValues = true; #endregion #region Properties /// /// Singleton instance for PhotonVoiceNetwork /// public static PhotonVoiceNetwork Instance { get { lock (instanceLock) { if (AppQuits) { if (instance.Logger.IsWarningEnabled) { instance.Logger.LogWarning("PhotonVoiceNetwork Instance already destroyed on application quit. Won't create again - returning null."); } return null; } if (!instantiated) { PhotonVoiceNetwork[] objects = FindObjectsOfType(); if (objects == null || objects.Length < 1) { GameObject singleton = new GameObject(); singleton.name = "PhotonVoiceNetwork singleton"; instance = singleton.AddComponent(); if (instance.Logger.IsInfoEnabled) { instance.Logger.LogInfo("An instance of PhotonVoiceNetwork was automatically created in the scene."); } } else if (objects.Length >= 1) { instance = objects[0]; if (objects.Length > 1) { if (instance.Logger.IsErrorEnabled) { instance.Logger.LogError("{0} PhotonVoiceNetwork instances found. Using first one only and destroying all the other extra instances.", objects.Length); } for (int i = 1; i < objects.Length; i++) { Destroy(objects[i]); } } } instantiated = true; if (instance.Logger.IsDebugEnabled) { instance.Logger.LogDebug("PhotonVoiceNetwork singleton instance is now set."); } } return instance; } } set { lock (instanceLock) { if (value == null) { if (instantiated) { if (instance.Logger.IsErrorEnabled) { instance.Logger.LogError("Cannot set PhotonVoiceNetwork.Instance to null."); } } else { Debug.LogError("Cannot set PhotonVoiceNetwork.Instance to null."); } return; } if (instantiated) { if (instance.GetInstanceID() != value.GetInstanceID()) { if (instance.Logger.IsErrorEnabled) { instance.Logger.LogError("An instance of PhotonVoiceNetwork is already set. Destroying extra instance."); } Destroy(value); } return; } instance = value; instantiated = true; if (instance.Logger.IsDebugEnabled) { instance.Logger.LogDebug("PhotonVoiceNetwork singleton instance is now set."); } } } } /// /// Whether or not to use the same PhotonNetwork.AuthValues in PhotonVoiceNetwork.Instance.Client.AuthValues. /// This means that the same UserID will be used in both clients. /// If custom authentication is used and setup in PUN app, the same configuration should be done for the Voice app. /// public bool UsePunAuthValues { get { return this.usePunAuthValues; } set { this.usePunAuthValues = value; } } #endregion #region Public Methods /// /// Connect voice client to Photon servers and join a Voice room /// /// If true, connection command send from client public bool ConnectAndJoinRoom() { if (!PhotonNetwork.InRoom) { if (this.Logger.IsErrorEnabled) { this.Logger.LogError("Cannot connect and join if PUN is not joined."); } return false; } if (this.Connect()) { this.clientCalledConnectAndJoin = true; this.clientCalledDisconnect = false; return true; } if (this.Logger.IsErrorEnabled) { this.Logger.LogError("Connecting to server failed."); } return false; } /// /// Disconnect voice client from all Photon servers /// public void Disconnect() { if (!this.Client.IsConnected) { if (this.Logger.IsErrorEnabled) { this.Logger.LogError("Cannot Disconnect if not connected."); } return; } this.clientCalledDisconnect = true; this.clientCalledConnectAndJoin = false; this.clientCalledConnectOnly = false; this.Client.Disconnect(); } #endregion #region Private Methods protected override void Awake() { Instance = this; lock (instanceLock) { if (instantiated && instance.GetInstanceID() == this.GetInstanceID()) { base.Awake(); } } } private void OnEnable() { PhotonNetwork.NetworkingClient.StateChanged += this.OnPunStateChanged; this.FollowPun(); // in case this is enabled or activated late this.clientCalledConnectAndJoin = false; this.clientCalledConnectOnly = false; this.clientCalledDisconnect = false; this.internalDisconnect = false; } protected override void OnDisable() { base.OnDisable(); PhotonNetwork.NetworkingClient.StateChanged -= this.OnPunStateChanged; } protected override void OnDestroy() { base.OnDestroy(); lock (instanceLock) { if (instantiated && instance.GetInstanceID() == this.GetInstanceID()) { instantiated = false; if (instance.Logger.IsDebugEnabled) { instance.Logger.LogDebug("PhotonVoiceNetwork singleton instance is being reset because destroyed."); } instance = null; } } } private void OnPunStateChanged(ClientState fromState, ClientState toState) { if (this.Logger.IsDebugEnabled) { this.Logger.LogDebug("OnPunStateChanged from {0} to {1}", fromState, toState); } this.FollowPun(toState); } protected override void OnVoiceStateChanged(ClientState fromState, ClientState toState) { base.OnVoiceStateChanged(fromState, toState); if (toState == ClientState.Disconnected) { if (this.internalDisconnect) { this.internalDisconnect = false; } else if (!this.clientCalledDisconnect) { this.clientCalledDisconnect = this.Client.DisconnectedCause == DisconnectCause.DisconnectByClientLogic; } } else if (toState == ClientState.ConnectedToMasterServer) { if (this.internalConnect) { this.internalConnect = false; } else if (!this.clientCalledConnectOnly && !this.clientCalledConnectAndJoin) { this.clientCalledConnectOnly = true; this.clientCalledDisconnect = false; } } this.FollowPun(toState); } private void FollowPun(ClientState toState) { switch (toState) { case ClientState.Joined: case ClientState.Disconnected: case ClientState.ConnectedToMasterServer: this.FollowPun(); break; } } protected override Speaker SimpleSpeakerFactory(int playerId, byte voiceId, object userData) { if (!(userData is int)) { if (this.Logger.IsWarningEnabled) { this.Logger.LogWarning("UserData ({0}) does not contain PhotonViewId. Remote voice {1}/{2} not linked. Do you have a Recorder not used with a PhotonVoiceView? is this expected?", userData == null ? "null" : userData.ToString(), playerId, voiceId); } return null; } int photonViewId = (int)userData; PhotonView photonView = PhotonView.Find(photonViewId); if (photonView == null) { if (this.Logger.IsWarningEnabled) { this.Logger.LogWarning("No PhotonView with ID {0} found. Remote voice {1}/{2} not linked.", userData, playerId, voiceId); } return null; } PhotonVoiceView photonVoiceView = photonView.GetComponent(); if (photonVoiceView == null) { if (this.Logger.IsWarningEnabled) { this.Logger.LogWarning("No PhotonVoiceView attached to the PhotonView with ID {0}. Remote voice {1}/{2} not linked.", userData, playerId, voiceId); } return null; } if (!photonVoiceView.IgnoreGlobalLogLevel) { photonVoiceView.LogLevel = this.LogLevel; } if (!photonVoiceView.IsSpeaker) { photonVoiceView.SetupSpeakerInUse(); } return photonVoiceView.SpeakerInUse; } internal static string GetVoiceRoomName() { if (PhotonNetwork.InRoom) { return string.Format("{0}{1}", PhotonNetwork.CurrentRoom.Name, VoiceRoomNameSuffix); } return null; } private void ConnectOrJoin() { switch (this.ClientState) { case ClientState.PeerCreated: case ClientState.Disconnected: if (this.Logger.IsInfoEnabled) { this.Logger.LogInfo("PUN joined room, now connecting Voice client"); } if (!this.Connect()) { if (this.Logger.IsErrorEnabled) { this.Logger.LogError("Connecting to server failed."); } } else { this.internalConnect = this.AutoConnectAndJoin && !this.clientCalledConnectOnly && !this.clientCalledConnectAndJoin; } break; case ClientState.ConnectedToMasterServer: if (this.Logger.IsInfoEnabled) { this.Logger.LogInfo("PUN joined room, now joining Voice room"); } if (!this.JoinRoom(GetVoiceRoomName())) { if (this.Logger.IsErrorEnabled) { this.Logger.LogError("Joining a voice room failed."); } } break; default: if (this.Logger.IsWarningEnabled) { this.Logger.LogWarning("PUN joined room, Voice client is busy ({0}). Is this expected?", this.ClientState); } break; } } private bool Connect() { AppSettings settings = null; if (this.usePunAppSettings) { settings = new AppSettings(); settings = PhotonNetwork.PhotonServerSettings.AppSettings.CopyTo(settings); // creates an independent copy (cause we need to modify it slightly) if (!string.IsNullOrEmpty(PhotonNetwork.CloudRegion)) { settings.FixedRegion = PhotonNetwork.CloudRegion; // makes sure the voice connection follows into the same cloud region (as PUN uses now). } this.Client.SerializationProtocol = PhotonNetwork.NetworkingClient.SerializationProtocol; } // use the same user, authentication, auth-mode and encryption as PUN if (this.UsePunAuthValues) { if (PhotonNetwork.AuthValues != null) { if (this.Client.AuthValues == null) { this.Client.AuthValues = new AuthenticationValues(); } this.Client.AuthValues = PhotonNetwork.AuthValues.CopyTo(this.Client.AuthValues); } this.Client.AuthMode = PhotonNetwork.NetworkingClient.AuthMode; this.Client.EncryptionMode = PhotonNetwork.NetworkingClient.EncryptionMode; } return this.ConnectUsingSettings(settings); } private bool JoinRoom(string voiceRoomName) { if (string.IsNullOrEmpty(voiceRoomName)) { if (this.Logger.IsErrorEnabled) { this.Logger.LogError("Voice room name is null or empty."); } return false; } this.voiceRoomParams.RoomName = voiceRoomName; return this.Client.OpJoinOrCreateRoom(this.voiceRoomParams); } // Follow PUN client state // In case Voice client disconnects unexpectedly try to reconnect to the same room // In case Voice client is connected to the wrong room switch to the correct one private void FollowPun() { if (AppQuits) { return; } if (PhotonNetwork.OfflineMode && !this.WorkInOfflineMode) { return; } if (PhotonNetwork.NetworkClientState == this.ClientState) { if (PhotonNetwork.InRoom && this.AutoConnectAndJoin) { string expectedRoomName = GetVoiceRoomName(); string currentRoomName = this.Client.CurrentRoom.Name; if (!currentRoomName.Equals(expectedRoomName)) { if (this.Logger.IsWarningEnabled) { this.Logger.LogWarning( "Voice room mismatch: Expected:\"{0}\" Current:\"{1}\", leaving the second to join the first.", expectedRoomName, currentRoomName); } if (!this.Client.OpLeaveRoom(false)) { if (this.Logger.IsErrorEnabled) { this.Logger.LogError("Leaving the current voice room failed."); } } } } else if (this.ClientState == ClientState.ConnectedToMasterServer && this.AutoLeaveAndDisconnect && !this.clientCalledConnectAndJoin && !this.clientCalledConnectOnly) { if (this.Logger.IsWarningEnabled) { this.Logger.LogWarning("Unexpected: PUN and Voice clients have the same client state: ConnectedToMasterServer, Disconnecting Voice client."); } this.internalDisconnect = true; this.Client.Disconnect(); } return; } if (PhotonNetwork.InRoom) { if (this.clientCalledConnectAndJoin || this.AutoConnectAndJoin && !this.clientCalledDisconnect) { this.ConnectOrJoin(); } } else if (this.Client.InRoom && this.AutoLeaveAndDisconnect && !this.clientCalledConnectAndJoin && !this.clientCalledConnectOnly) { if (this.Logger.IsInfoEnabled) { this.Logger.LogInfo("PUN left room, disconnecting Voice"); } this.internalDisconnect = true; this.Client.Disconnect(); } } internal void CheckLateLinking(Speaker speaker, int viewId) { if (!speaker || speaker == null) { if (this.Logger.IsWarningEnabled) { this.Logger.LogWarning("Cannot check late linking for null Speaker"); } return; } if (viewId <= 0) { if (this.Logger.IsWarningEnabled) { this.Logger.LogWarning("Cannot check late linking for ViewID = {0} (<= 0)", viewId); } return; } if (!this.Client.InRoom) { if (this.Logger.IsWarningEnabled) { this.Logger.LogWarning("Cannot check late linking while not joined to a voice room, client state: {0}", System.Enum.GetName(typeof(ClientState), this.ClientState)); } return; } for (int i = 0; i < this.cachedRemoteVoices.Count; i++) { RemoteVoiceLink remoteVoice = this.cachedRemoteVoices[i]; if (remoteVoice.Info.UserData is int) { int photonViewId = (int)remoteVoice.Info.UserData; if (viewId == photonViewId) { if (this.Logger.IsInfoEnabled) { this.Logger.LogInfo("Speaker 'late-linking' for the PhotonView with ID {0} with remote voice {1}/{2}.", viewId, remoteVoice.PlayerId, remoteVoice.VoiceId); } this.LinkSpeaker(speaker, remoteVoice); break; } } else if (this.Logger.IsWarningEnabled) { this.Logger.LogWarning("VoiceInfo.UserData should be int/ViewId, received: {0}, do you have a Recorder not used with a PhotonVoiceView? is this expected?", remoteVoice.Info.UserData == null ? "null" : string.Format("{0} ({1})", remoteVoice.Info.UserData, remoteVoice.Info.UserData.GetType())); if (remoteVoice.PlayerId == viewId / PhotonNetwork.MAX_VIEW_IDS) { this.Logger.LogWarning("Player with ActorNumber {0} has started recording (voice # {1}) too early without setting a ViewId maybe? (before PhotonVoiceView setup)", remoteVoice.PlayerId, remoteVoice.VoiceId); } } } } #endregion } }