// ---------------------------------------------------------------------------- // // Photon Voice - Copyright (C) 2018 Exit Games GmbH // // // Component that should be attached to a networked PUN prefab that has // PhotonView. It will bind remote Recorder with local Speaker of the same // networked prefab. This component makes automatic voice stream routing easy // for players' characters/avatars. // // developer@photonengine.com // ---------------------------------------------------------------------------- namespace Photon.Voice.PUN { using Pun; using UnityEngine; using Unity; /// /// Component that should be attached to a networked PUN prefab that has . /// It will bind remote with local of the same networked prefab. /// This component makes automatic voice stream routing easy for players' characters/avatars. /// [AddComponentMenu("Photon Voice/Photon Voice View")] [RequireComponent(typeof(PhotonView))] [HelpURL("https://doc.photonengine.com/en-us/voice/v2/getting-started/voice-for-pun")] public class PhotonVoiceView : VoiceComponent { #region Private Fields private PhotonView photonView; [SerializeField] private Recorder recorderInUse; [SerializeField] private Speaker speakerInUse; private bool onEnableCalledOnce; #endregion #region Public Fields /// If true, a Recorder component will be added to the same GameObject if not found already. public bool AutoCreateRecorderIfNotFound; /// If true, PhotonVoiceNetwork.PrimaryRecorder will be used by this PhotonVoiceView public bool UsePrimaryRecorder; /// If true, a Speaker component will be setup to be used for the DebugEcho mode public bool SetupDebugSpeaker; #endregion #region Properties /// The Recorder component currently used by this PhotonVoiceView public Recorder RecorderInUse { get { return this.recorderInUse; } set { if (value != this.recorderInUse) { this.recorderInUse = value; this.IsRecorder = false; } if (this.RequiresRecorder) { this.SetupRecorderInUse(); } else if (this.IsPhotonViewReady) { if (this.Logger.IsWarningEnabled) { this.Logger.LogWarning("No need to set Recorder as the PhotonView does not belong to local player"); } } } } /// The Speaker component currently used by this PhotonVoiceView public Speaker SpeakerInUse { get { return this.speakerInUse; } set { if (this.speakerInUse != value) { this.speakerInUse = value; this.IsSpeaker = false; } if (this.RequiresSpeaker) { this.SetupSpeakerInUse(); } else if (this.IsPhotonViewReady) { if (this.Logger.IsWarningEnabled) { this.Logger.LogWarning("Speaker not set because the PhotonView does not belong to a remote player or SetupDebugSpeaker is disabled"); } } } } /// If true, this PhotonVoiceView is setup and ready to be used public bool IsSetup { get { return this.IsPhotonViewReady && (!this.RequiresRecorder || this.IsRecorder) && (!this.RequiresSpeaker || this.IsSpeaker); } } /// If true, this PhotonVoiceView has a Speaker setup for playback of received audio frames from remote audio source public bool IsSpeaker { get; private set; } /// If true, this PhotonVoiceView has a Speaker that is currently playing received audio frames from remote audio source public bool IsSpeaking { get { return this.IsSpeaker && this.SpeakerInUse.IsPlaying; } } /// If true, this PhotonVoiceView has a Recorder setup for transmission of audio stream from local audio source public bool IsRecorder { get; private set; } /// If true, this PhotonVoiceView has a Recorder that is currently transmitting audio stream from local audio source public bool IsRecording { get { return this.IsRecorder && this.RecorderInUse.IsCurrentlyTransmitting; } } /// If true, the SpeakerInUse is linked to the remote voice stream public bool IsSpeakerLinked { get { return this.IsSpeaker && this.SpeakerInUse.IsLinked; } } /// If true, the PhotonView attached to the same GameObject has a valid ViewID > 0 public bool IsPhotonViewReady { get { return this.photonView && this.photonView != null && this.photonView.ViewID > 0; } } internal bool RequiresSpeaker { get { return this.SetupDebugSpeaker || this.IsPhotonViewReady && !this.photonView.IsMine; } } internal bool RequiresRecorder { get { return this.IsPhotonViewReady && this.photonView.IsMine; } } #endregion #region Private Methods protected override void Awake() { base.Awake(); this.photonView = this.GetComponent(); this.Init(); } private void OnEnable() { if (this.onEnableCalledOnce) { this.Init(); } else { this.onEnableCalledOnce = true; } } private void Start() { this.Init(); } private void CheckLateLinking() { if (PhotonVoiceNetwork.Instance.Client.InRoom) { if (this.IsSpeaker) { if (!this.IsSpeakerLinked) { PhotonVoiceNetwork.Instance.CheckLateLinking(this.SpeakerInUse, this.photonView.ViewID); } else if (this.Logger.IsDebugEnabled) { this.Logger.LogDebug("Speaker already linked"); } } else if (this.Logger.IsDebugEnabled) { this.Logger.LogDebug("PhotonVoiceView does not have a Speaker and may not need late linking check"); } } else if (this.Logger.IsDebugEnabled) { this.Logger.LogDebug("Voice client is still not in a room, skipping late linking check"); } } internal void Setup() { if (this.IsSetup) { if (this.Logger.IsDebugEnabled) { this.Logger.LogDebug("PhotonVoiceView already setup"); } return; } this.SetupRecorderInUse(); this.SetupSpeakerInUse(); } private bool SetupRecorder() { if (this.recorderInUse == null) // not manually assigned by user { if (this.UsePrimaryRecorder) { if (PhotonVoiceNetwork.Instance.PrimaryRecorder != null) { this.recorderInUse = PhotonVoiceNetwork.Instance.PrimaryRecorder; return this.SetupRecorder(this.recorderInUse); } if (this.Logger.IsErrorEnabled) { this.Logger.LogError("PrimaryRecorder is not set."); } } Recorder[] recorders = this.GetComponentsInChildren(); if (recorders.Length > 0) { this.recorderInUse = recorders[0]; if (recorders.Length > 1 && this.Logger.IsWarningEnabled) { this.Logger.LogWarning("Multiple Recorder components found attached to the GameObject or its children."); } return this.SetupRecorder(this.recorderInUse); } if (!this.AutoCreateRecorderIfNotFound) { if (this.Logger.IsWarningEnabled) { this.Logger.LogWarning("No Recorder found to be setup."); } return false; } this.recorderInUse = this.gameObject.AddComponent(); } return this.SetupRecorder(this.recorderInUse); } private bool SetupRecorder(Recorder recorder) { if (recorder == null) { if (this.Logger.IsWarningEnabled) { this.Logger.LogWarning("Cannot setup a null Recorder."); } return false; } if (!this.IsPhotonViewReady) { if (this.Logger.IsWarningEnabled) { this.Logger.LogWarning("Recorder setup cannot be done before assigning a valid ViewID to the PhotonView attached to the same GameObject as the PhotonVoiceView."); } return false; } recorder.UserData = this.photonView.ViewID; if (!recorder.IsInitialized) { this.RecorderInUse.Init(PhotonVoiceNetwork.Instance); } if (recorder.RequiresRestart) { recorder.RestartRecording(); } return recorder.IsInitialized && recorder.UserData is int && this.photonView.ViewID == (int) recorder.UserData; } private bool SetupSpeaker() { if (this.speakerInUse == null) // not manually assigned by user { Speaker[] speakers = this.GetComponentsInChildren(true); if (speakers.Length > 0) { this.speakerInUse = speakers[0]; if (speakers.Length > 1 && this.Logger.IsWarningEnabled) { this.Logger.LogWarning("Multiple Speaker components found attached to the GameObject or its children. Using the first one we found."); } } if (this.speakerInUse == null) { if (!PhotonVoiceNetwork.Instance.AutoCreateSpeakerIfNotFound) { return false; } if (PhotonVoiceNetwork.Instance.SpeakerPrefab != null) { GameObject go = Instantiate(PhotonVoiceNetwork.Instance.SpeakerPrefab, this.transform, false); speakers = go.GetComponentsInChildren(true); if (speakers.Length > 0) { this.speakerInUse = speakers[0]; if (speakers.Length > 1 && this.Logger.IsWarningEnabled) { this.Logger.LogWarning("Multiple Speaker components found attached to the GameObject (PhotonVoiceNetwork.SpeakerPrefab) or its children. Using the first one we found."); } } if (this.speakerInUse == null) { if (this.Logger.IsErrorEnabled) { this.Logger.LogError("SpeakerPrefab does not have a component of type Speaker in its hierarchy."); } Destroy(go); return false; } } else { this.speakerInUse = this.gameObject.AddComponent(); } } } return this.SetupSpeaker(this.speakerInUse); } private bool SetupSpeaker(Speaker speaker) { if (speaker == null) { if (this.Logger.IsWarningEnabled) { this.Logger.LogWarning("Cannot setup a null Speaker"); } return false; } AudioSource audioSource = speaker.GetComponent(); if (audioSource == null) { if (this.Logger.IsWarningEnabled) { this.Logger.LogWarning("Unexpected: no AudioSource found attached to the same GameObject as the Speaker component"); } return false; } if (audioSource.mute) { if (this.Logger.IsWarningEnabled) { this.Logger.LogWarning("audioSource.mute is true, playback may not work properly"); } } if (audioSource.volume <= 0f) { if (this.Logger.IsWarningEnabled) { this.Logger.LogWarning("audioSource.volume is zero, playback may not work properly"); } } if (!audioSource.enabled) { if (this.Logger.IsWarningEnabled) { this.Logger.LogWarning("audioSource.enabled is false, playback may not work properly"); } } return true; } internal void SetupRecorderInUse() { if (this.IsRecorder) { if (this.Logger.IsInfoEnabled) { this.Logger.LogInfo("Recorder already setup"); } return; } if (!this.RequiresRecorder) { if (this.IsPhotonViewReady) { if (this.Logger.IsInfoEnabled) { this.Logger.LogInfo("Recorder not needed"); } } return; } this.IsRecorder = this.SetupRecorder(); if (!this.IsRecorder) { if (this.Logger.IsWarningEnabled) { this.Logger.LogWarning("Recorder not setup for PhotonVoiceView: playback may not work properly."); } } else { if (!this.RecorderInUse.IsRecording && !this.RecorderInUse.AutoStart) { if (this.Logger.IsWarningEnabled) { this.Logger.LogWarning("PhotonVoiceView.RecorderInUse.AutoStart is false, don't forget to start recording manually using recorder.StartRecording() or recorder.IsRecording = true."); } } if (!this.RecorderInUse.TransmitEnabled) { if (this.Logger.IsWarningEnabled) { this.Logger.LogWarning("PhotonVoiceView.RecorderInUse.TransmitEnabled is false, don't forget to set it to true to enable transmission."); } } if (!this.RecorderInUse.isActiveAndEnabled && this.RecorderInUse.RecordOnlyWhenEnabled) { if (this.Logger.IsWarningEnabled) { this.Logger.LogWarning("PhotonVoiceView.RecorderInUse may not work properly as RecordOnlyWhenEnabled is set to true and recorder is disabled or attached to an inactive GameObject."); } } } } internal void SetupSpeakerInUse() { if (this.IsSpeaker) { if (this.Logger.IsInfoEnabled) { this.Logger.LogInfo("Speaker already setup"); } return; } if (!this.RequiresSpeaker) { if (this.IsPhotonViewReady) { if (this.Logger.IsInfoEnabled) { this.Logger.LogInfo("Speaker not needed"); } } return; } this.IsSpeaker = this.SetupSpeaker(); if (!this.IsSpeaker) { if (this.Logger.IsWarningEnabled) { this.Logger.LogWarning("Speaker not setup for PhotonVoiceView: voice chat will not work."); } } else { this.CheckLateLinking(); } } #endregion #region Public Methods /// /// Initializes this PhotonVoiceView for Voice usage based on the PhotonView, Recorder and Speaker components. /// /// /// The initialization should happen automatically. /// Call this method explicitly if this does not succeed. /// The initialization is a two steps operation: step one is the setup of Recorder and Speaker to be used. /// Step two is the late-linking -if needed- of the SpeakerInUse and corresponding remote voice info -if any- via ViewID. /// public void Init() { if (this.IsPhotonViewReady) { this.Setup(); this.CheckLateLinking(); } else if (this.Logger.IsDebugEnabled) { this.Logger.LogDebug("Tried to initialize PhotonVoiceView but PhotonView does not have a valid allocated ViewID yet."); } } #endregion } }