// ----------------------------------------------------------------------- // // Photon Voice API Framework for Photon - Copyright (C) 2015 Exit Games GmbH // // // Extends Photon Realtime API with media streaming functionality. // // developer@photonengine.com // ---------------------------------------------------------------------------- using System; using System.Collections.Generic; using System.Linq; using ExitGames.Client.Photon; using Photon.Realtime; namespace Photon.Voice { class VoiceEvent { /// /// Single event used for voice communications. /// /// Change if it conflicts with other event codes used in the same Photon room. public const byte Code = 202; // all photon voice events use single event code public const byte FrameCode = 203; // LoadBalancingTransport2 uses separate code for frame event serialized as byte[] } /// /// Extends LoadBalancingClient with media streaming functionality. /// /// /// Use your normal LoadBalancing workflow to join a Voice room. /// All standard LoadBalancing features are available. /// Use to work with media streams. /// public class LoadBalancingTransport : LoadBalancingClient, IVoiceTransport, IDisposable { // Channel is used only by local voice (to specify Enet channel) and ignored by remote voices which are all on the same channel. // Join / leave per channel is not supported. // It's important to call onJoinAllChannels() instead of onJoinChannel() to avoid ignoring local non-0 channel voices. internal const int REMOTE_VOICE_CHANNEL = 0; public virtual int GetPayloadFragmentSize(SendFrameParams par) { // rough estimate, no need to improve because this transport is obsolete int overhead = 3 * 2; // possible InterestGroup and Receivers: key, type, value if (par.TargetPlayers != null) { overhead += 3 + par.TargetPlayers.Length; // key, type, compressed length and array } return 1114 - overhead; // <- protocol 18 theoretical encoded; experimental encrypted: 1115, non-encrypted: 1130 } /// The implementation associated with this LoadBalancingTransport. public VoiceClient VoiceClient { get { return this.voiceClient; } } protected VoiceClient voiceClient; private PhotonTransportProtocol protocol; protected readonly bool cppCompatibilityMode; protected readonly ILogger logger; public bool IsChannelJoined(int channelId) { return this.State == ClientState.Joined; } /// /// Initializes a new . /// /// ILogger instance. If null, this instance LoadBalancingClient.DebugReturn implementation is used. /// Connection protocol (UDP or TCP). /// Use a protocol compatible with Voice C++ API. public LoadBalancingTransport(ILogger logger = null, ConnectionProtocol connectionProtocol = ConnectionProtocol.Udp, bool cppCompatibilityMode = false) : base(connectionProtocol) { if (logger == null) { logger = new LBCLogger(this); } this.ClientType = ClientAppType.Voice; this.cppCompatibilityMode = cppCompatibilityMode; base.EventReceived += onEventActionVoiceClient; base.StateChanged += onStateChangeVoiceClient; this.voiceClient = new VoiceClient(this, logger); // Pre-allocate a channel for each stream type, assuming the recommended channel setup is used (1 = audio, 2 = video, 3 = screen share). if (LoadBalancingPeer.ChannelCount < 4) { this.LoadBalancingPeer.ChannelCount = 4; } this.protocol = new PhotonTransportProtocol(voiceClient, logger); this.logger = logger; } /// /// This method dispatches all available incoming commands and then sends this client's outgoing commands. /// Call this method regularly (2 to 20 times a second). /// new public void Service() { base.Service(); this.voiceClient.Service(); } [Obsolete("Use LoadBalancingPeer::OpChangeGroups().")] public virtual bool ChangeAudioGroups(byte[] groupsToRemove, byte[] groupsToAdd) { return this.LoadBalancingPeer.OpChangeGroups(groupsToRemove, groupsToAdd); } // Photon transport specific: // Empty TargetActors is the same as null: sending to all except the local client. // if TargetActors is not null and non-empty, InterestGroup and ReceiverGroup are ignored // if TargetActors is null or empty and InterestGroup is set, ReceiverGroup is ignored RaiseEventOptions buildEvOptFromTargets(bool targetMe, int[] targetPlayers) { var opt = new RaiseEventOptions(); if (targetMe) { if (targetPlayers == null) // all others and me { opt.Receivers = ReceiverGroup.All; } else if (targetPlayers.Length == 0) // only me { opt.TargetActors = new int[] { this.LocalPlayer.ActorNumber }; } else // some others and me { opt.TargetActors = new int[targetPlayers.Length + 1]; Array.Copy(targetPlayers, opt.TargetActors, targetPlayers.Length); opt.TargetActors[targetPlayers.Length] = this.LocalPlayer.ActorNumber; } } else { if (opt.TargetActors != null && opt.TargetActors.Length == 0) // Voice Core does not do such calls but better check again because LoadBalancing sends to all except the local client if the list is empty { throw new ArgumentException("LoadBalancingTransport: no targets specified in Send* method call"); } opt.TargetActors = targetPlayers; } return opt; } #region nonpublic public void SendVoiceInfo(LocalVoice voice, int channelId, bool targetMe, int[] targetPlayers) { object content = protocol.buildVoicesInfo(voice); var sendOpt = new SendOptions() { DeliveryMode = DeliveryMode.Reliable, Channel = (byte)channelId, }; var opt = buildEvOptFromTargets(targetMe, targetPlayers); this.OpRaiseEvent(VoiceEvent.Code, content, opt, sendOpt); } public void SendVoiceRemove(LocalVoice voice, int channelId, bool targetMe, int[] targetPlayers) { object content = protocol.buildVoiceRemoveMessage(voice); var sendOpt = new SendOptions() { DeliveryMode = DeliveryMode.Reliable, Channel = (byte)channelId, }; var opt = buildEvOptFromTargets(targetMe, targetPlayers); this.OpRaiseEvent(VoiceEvent.Code, content, opt, sendOpt); } protected virtual byte FrameCode => VoiceEvent.Code; protected virtual object buildFrameMessage(byte voiceId, byte evNumber, byte frNumber, ArraySegment data, FrameFlags flags) { return protocol.buildFrameMessage(voiceId, evNumber, frNumber, data, flags); } public void SendFrame(ArraySegment data, FrameFlags flags, byte evNumber, byte frNumber, byte voiceId, int channelId, SendFrameParams par) { object content = buildFrameMessage(voiceId, evNumber, frNumber, data, flags); var sendOpt = new SendOptions() { DeliveryMode = ((flags & FrameFlags.Config) != 0) ? DeliveryMode.Reliable : // config frame should be send in sync with voice info cppCompatibilityMode ? par.Reliable ? DeliveryMode.Reliable : DeliveryMode.Unreliable : par.Reliable ? DeliveryMode.ReliableUnsequenced : DeliveryMode.UnreliableUnsequenced, Channel = (byte)channelId, Encrypt = par.Encrypt, }; var opt = buildEvOptFromTargets(par.TargetMe, par.TargetPlayers); opt.InterestGroup = par.InterestGroup; this.OpRaiseEvent(FrameCode, content, opt, sendOpt); while (this.LoadBalancingPeer.SendOutgoingCommands()) ; } public string ChannelIdStr(int channelId) { return null; } public string PlayerIdStr(int playerId) { return null; } protected virtual void onEventActionVoiceClient(EventData ev) { // check for voice event first if (ev.Code == VoiceEvent.Code) { // Payloads are arrays. If first array element is 0 than next is event subcode. Otherwise, the event is data frame with voiceId in 1st element. protocol.onVoiceEvent(ev[(byte)ParameterCode.CustomEventContent], REMOTE_VOICE_CHANNEL, ev.Sender, ev.Sender == this.LocalPlayer.ActorNumber); } else { int playerId; switch (ev.Code) { case (byte)EventCode.Join: playerId = ev.Sender; if (playerId == this.LocalPlayer.ActorNumber) { } else { this.voiceClient.onPlayerJoin(playerId); } break; case (byte)EventCode.Leave: { playerId = ev.Sender; if (playerId == this.LocalPlayer.ActorNumber) { this.voiceClient.onLeaveAllChannels(); } else { this.voiceClient.onPlayerLeave(playerId); } } break; } } } void onStateChangeVoiceClient(ClientState fromState, ClientState state) { switch (fromState) { case ClientState.Joined: this.voiceClient.onLeaveAllChannels(); break; } switch (state) { case ClientState.Joined: this.voiceClient.onJoinAllChannels(); break; } } #endregion /// /// Releases all resources used by the instance. /// public void Dispose() { this.voiceClient.Dispose(); } // Allows the underlying LoadBalancingClient.DebugReturn() logger to be used as a Transport/Voice logger. class LBCLogger : ILogger { LoadBalancingTransport lbt; public LBCLogger(LoadBalancingTransport lbt) { this.lbt = lbt; } public LogLevel Level { get { if (lbt.LoadBalancingPeer.DebugOut == DebugLevel.INFO) return LogLevel.Info; if (lbt.LoadBalancingPeer.DebugOut == DebugLevel.WARNING) return LogLevel.Warning; if (lbt.LoadBalancingPeer.DebugOut <= DebugLevel.ERROR) return LogLevel.Error; return LogLevel.Trace; } } public void Log(LogLevel level, string fmt, params object[] args) { if (this.Level >= level) { DebugLevel debugOut = DebugLevel.ALL; if (level == LogLevel.Info) debugOut = DebugLevel.INFO; else if (level == LogLevel.Warning) debugOut = DebugLevel.WARNING; else if (level == LogLevel.Error) debugOut = DebugLevel.ERROR; lbt.DebugReturn(debugOut, string.Format(fmt, args)); } } } } }