using System.Collections.Generic; using System.Linq; using Unity.WebRTC; using UnityEngine; namespace Unity.RenderStreaming { /// /// 每个Participant的接收器信息 /// VideoStreamReceiver/AudioStreamReceiver 只持有一个transceiver, /// 因此每个Participant需要独立的Receiver实例 /// public class ParticipantStreams { public string participantId; public VideoStreamReceiver videoReceiver; public AudioStreamReceiver audioReceiver; public GameObject gameObject; } /// /// Host模式的连接处理器 /// 适用于私有模式下Unity作为Host(第一个加入房间的客户端) /// 支持多个Participant连接,每个Participant拥有独立的PeerConnection和Receiver /// /// 工作原理: /// - Host通过CreateConnection(connectionId)创建房间连接 /// - 当Participant加入时,服务器发送participant-joined通知 /// - WebSocketSignaling将participantId作为内部connectionId使用, /// 因此每个Participant会自动创建独立的PeerConnection /// - 当收到Participant的offer时,为该Participant创建独立的Receiver并添加Sender /// - 当Participant离开时,自动清理对应的连接资源和Receiver /// [AddComponentMenu("Render Streaming/Host Connection")] public class HostConnection : SignalingHandlerBase, IOfferHandler, IAddChannelHandler, IDisconnectHandler, IDeletedConnectionHandler, IAddReceiverHandler, IParticipantJoinedHandler, IParticipantLeftHandler, ICallRequestHandler, IMessageHandler { [SerializeField] private List streams = new List(); /// /// 当前连接的所有Participant的participantId列表 /// private List participantIds = new List(); /// /// 房间的connectionId(Host创建连接时使用的ID) /// private string roomConnectionId; /// /// 每个Participant的独立Receiver(key=participantId) /// VideoStreamReceiver/AudioStreamReceiver内部只持有一个transceiver, /// 必须为每个Participant创建独立的实例 /// private Dictionary m_participantStreams = new Dictionary(); /// /// Participant连接成功事件,提供该Participant的Receiver引用 /// 外部脚本可订阅此事件来创建对应的UI显示(如RawImage) /// public event System.Action OnParticipantConnected; /// /// Participant断开连接事件 /// public event System.Action OnParticipantDisconnected; public override IEnumerable Streams => streams; public void AddComponent(Component component) { streams.Add(component); } public void RemoveComponent(Component component) { streams.Remove(component); } /// /// 创建Host连接(即创建房间) /// Unity作为第一个连接的客户端,服务器会分配host角色(polite=false) /// /// 房间ID public override void CreateConnection(string connectionId) { roomConnectionId = connectionId; base.CreateConnection(connectionId); } /// /// 删除Host连接(关闭房间) /// Host离开时,服务器会自动断开所有Participant /// public override void DeleteConnection(string connectionId) { // 清理所有Participant连接 // 注意:不能在这里调用 base.DeleteConnection(participantId),否则会给每个Participant发送disconnect, // 导致服务器返回多次通知。应该只发一次disconnect关闭房间,Participant的清理由服务器通知触发。 foreach (var participantId in participantIds.ToList()) { DisconnectParticipant(participantId); } participantIds.Clear(); if (connectionId == roomConnectionId) { base.DeleteConnection(connectionId); roomConnectionId = null; } else { base.DeleteConnection(connectionId); } } public void OnDeletedConnection(SignalingEventData eventData) { Disconnect(eventData.connectionId); } public void OnDisconnect(SignalingEventData eventData) { Disconnect(eventData.connectionId); } private void Disconnect(string connectionId) { if (!participantIds.Contains(connectionId) && connectionId != roomConnectionId) return; if (participantIds.Contains(connectionId)) { DisconnectParticipant(connectionId); participantIds.Remove(connectionId); } else { foreach (var sender in streams.OfType()) { RemoveSender(connectionId, sender); } } } /// /// 断开指定Participant的连接资源,销毁其独立的Receiver /// private void DisconnectParticipant(string participantId) { // 清理独立Receiver if (m_participantStreams.TryGetValue(participantId, out var ps)) { // 移除Sender关联 // foreach (var sender in streams.OfType()) // { // RemoveSender(participantId, sender); // } // 移除Receiver关联 if (ps.videoReceiver != null) RemoveReceiver(participantId, ps.videoReceiver); if (ps.audioReceiver != null) RemoveReceiver(participantId, ps.audioReceiver); // 移除DataChannel foreach (var channel in streams.OfType().Where(c => c.ConnectionId == participantId)) { RemoveChannel(participantId, channel); } // 销毁Receiver GameObject if (ps.gameObject != null) { this.streams.Remove(ps.audioReceiver); this.streams.Remove(ps.videoReceiver); Destroy(ps.gameObject); } m_participantStreams.Remove(participantId); } else { // 回退:无独立Receiver时走共享streams清理 foreach (var sender in streams.OfType()) { RemoveSender(participantId, sender); } foreach (var receiver in streams.OfType()) { RemoveReceiver(participantId, receiver); } foreach (var channel in streams.OfType().Where(c => c.ConnectionId == participantId)) { RemoveChannel(participantId, channel); } } OnParticipantDisconnected?.Invoke(participantId); } /// /// 收到Participant的offer时触发 /// 为每个Participant创建独立的VideoStreamReceiver和AudioStreamReceiver, /// 然后添加共享的Sender并回复answer /// public void OnOffer(SignalingEventData data) { bool isNewParticipant = !participantIds.Contains(data.connectionId); if (isNewParticipant) { participantIds.Add(data.connectionId); Debug.Log($"[HostConnection] Participant offer received: {data.connectionId}"); var ps = new ParticipantStreams { participantId = data.connectionId }; var go = new GameObject($"Participant_{data.connectionId}"); go.transform.SetParent(transform); ps.gameObject = go; ps.videoReceiver = go.AddComponent(); ps.videoReceiver.renderMode = VideoRenderMode.APIOnly; this.streams.Add(ps.videoReceiver); ps.audioReceiver = go.AddComponent(); this.streams.Add(ps.audioReceiver); var audioSource = go.AddComponent(); audioSource.loop = true; ps.audioReceiver.targetAudioSource = audioSource; m_participantStreams[data.connectionId] = ps; OnParticipantConnected?.Invoke(ps); } // 在 SetRemoteDescription 之前添加 Sender 和 Channel // 这样 transceiver 会正确匹配 offer 中的媒体行 foreach (var source in streams.OfType()) { AddSender(data.connectionId, source); } foreach (var channel in streams.OfType().Where(c => c.IsLocal)) { AddChannel(data.connectionId, channel); } // 不再手动调用 SendAnswer // SignalingManagerInternal.OnOffer 会在 OnGotDescription 完成后自动调用 SendAnswer } /// /// 收到远程Track时触发 /// 使用该Participant的独立Receiver来接收 /// public void OnAddReceiver(SignalingEventData data) { var track = data.transceiver.Receiver.Track; // 优先使用该Participant的独立Receiver if (m_participantStreams.TryGetValue(data.connectionId, out var ps)) { IStreamReceiver receiver = null; if (track.Kind == TrackKind.Video && ps.videoReceiver != null) receiver = ps.videoReceiver; else if (track.Kind == TrackKind.Audio && ps.audioReceiver != null) receiver = ps.audioReceiver; if (receiver != null) { SetReceiver(data.connectionId, receiver, data.transceiver); return; } } // 回退:使用共享streams中的Receiver var fallbackReceiver = GetReceiver(track.Kind); if (fallbackReceiver != null) SetReceiver(data.connectionId, fallbackReceiver, data.transceiver); } /// /// 收到远程DataChannel时触发 /// public void OnAddChannel(SignalingEventData data) { var channel = streams.OfType(). FirstOrDefault(r => !r.IsConnected && !r.IsLocal); channel?.SetChannel(data.connectionId, data.channel); } /// /// 新Participant加入房间通知 /// 在私有模式下,服务器会通知Host有新的Participant加入 /// 此时不做额外处理,等收到Participant的offer时再建立连接 /// public void OnParticipantJoined(SignalingEventData eventData) { Debug.Log($"[HostConnection] Participant joined: connectionId={eventData.connectionId}, participantId={eventData.participantId}"); } /// /// Participant离开房间通知 /// 清理该Participant的连接资源 /// public void OnParticipantLeft(SignalingEventData eventData) { string participantId = eventData.participantId; if (!string.IsNullOrEmpty(participantId)) { Debug.Log($"[HostConnection] Participant left: connectionId={eventData.connectionId}, participantId={participantId}"); DisconnectParticipant(participantId); participantIds.Remove(participantId); } } /// /// 收到呼叫请求 /// public void OnCallRequest(SignalingEventData eventData) { Debug.Log($"[HostConnection] Call request from: {eventData.connectionId}, data: {eventData.message}"); } /// /// 收到自定义消息 /// public void OnMessage(SignalingEventData eventData) { Debug.Log($"[HostConnection] Message from: {eventData.connectionId}, participantId: {eventData.participantId}, message: {eventData.message}"); } /// /// 获取当前连接的Participant数量 /// public int ParticipantCount => participantIds.Count; /// /// 获取所有Participant的ID列表 /// public IReadOnlyList ParticipantIds => participantIds.AsReadOnly(); /// /// 获取指定Participant的Receiver信息 /// public bool TryGetParticipantStreams(string participantId, out ParticipantStreams ps) { return m_participantStreams.TryGetValue(participantId, out ps); } IStreamReceiver GetReceiver(TrackKind kind) { if (kind == TrackKind.Audio) return streams.OfType().FirstOrDefault(); if (kind == TrackKind.Video) return streams.OfType().FirstOrDefault(); return null; } protected virtual void OnDestroy() { // 清理所有Participant的Receiver GameObject foreach (var ps in m_participantStreams.Values) { if (ps.gameObject != null) Destroy(ps.gameObject); } m_participantStreams.Clear(); } } }