Files
webRtc/Packages/com.unity.renderstreaming@3.1.0-exp.9/Runtime/Scripts/HostConnection.cs
2026-04-30 15:35:47 +08:00

366 lines
14 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Collections.Generic;
using System.Linq;
using Unity.WebRTC;
using UnityEngine;
namespace Unity.RenderStreaming
{
/// <summary>
/// 每个Participant的接收器信息
/// VideoStreamReceiver/AudioStreamReceiver 只持有一个transceiver
/// 因此每个Participant需要独立的Receiver实例
/// </summary>
public class ParticipantStreams
{
public string participantId;
public VideoStreamReceiver videoReceiver;
public AudioStreamReceiver audioReceiver;
public GameObject gameObject;
}
/// <summary>
/// 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
/// </summary>
[AddComponentMenu("Render Streaming/Host Connection")]
public class HostConnection : SignalingHandlerBase,
IOfferHandler, IAddChannelHandler, IDisconnectHandler, IDeletedConnectionHandler,
IAddReceiverHandler, IParticipantJoinedHandler, IParticipantLeftHandler,
ICallRequestHandler, IMessageHandler
{
[SerializeField] private List<Component> streams = new List<Component>();
/// <summary>
/// 当前连接的所有Participant的participantId列表
/// </summary>
private List<string> participantIds = new List<string>();
/// <summary>
/// 房间的connectionIdHost创建连接时使用的ID
/// </summary>
private string roomConnectionId;
/// <summary>
/// 每个Participant的独立Receiverkey=participantId
/// VideoStreamReceiver/AudioStreamReceiver内部只持有一个transceiver
/// 必须为每个Participant创建独立的实例
/// </summary>
private Dictionary<string, ParticipantStreams> m_participantStreams = new Dictionary<string, ParticipantStreams>();
/// <summary>
/// Participant连接成功事件提供该Participant的Receiver引用
/// 外部脚本可订阅此事件来创建对应的UI显示如RawImage
/// </summary>
public event System.Action<ParticipantStreams> OnParticipantConnected;
/// <summary>
/// Participant断开连接事件
/// </summary>
public event System.Action<string> OnParticipantDisconnected;
public override IEnumerable<Component> Streams => streams;
public void AddComponent(Component component)
{
streams.Add(component);
}
public void RemoveComponent(Component component)
{
streams.Remove(component);
}
/// <summary>
/// 创建Host连接即创建房间
/// Unity作为第一个连接的客户端服务器会分配host角色(polite=false)
/// </summary>
/// <param name="connectionId">房间ID</param>
public override void CreateConnection(string connectionId)
{
roomConnectionId = connectionId;
base.CreateConnection(connectionId);
}
/// <summary>
/// 删除Host连接关闭房间
/// Host离开时服务器会自动断开所有Participant
/// </summary>
public override void DeleteConnection(string connectionId)
{
// 清理所有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<IStreamSender>())
{
RemoveSender(connectionId, sender);
}
}
}
/// <summary>
/// 断开指定Participant的连接资源销毁其独立的Receiver
/// </summary>
private void DisconnectParticipant(string participantId)
{
// 清理独立Receiver
if (m_participantStreams.TryGetValue(participantId, out var ps))
{
// 移除Sender关联
foreach (var sender in streams.OfType<IStreamSender>())
{
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<IDataChannel>().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<IStreamSender>())
{
RemoveSender(participantId, sender);
}
foreach (var receiver in streams.OfType<IStreamReceiver>())
{
RemoveReceiver(participantId, receiver);
}
foreach (var channel in streams.OfType<IDataChannel>().Where(c => c.ConnectionId == participantId))
{
RemoveChannel(participantId, channel);
}
}
OnParticipantDisconnected?.Invoke(participantId);
}
/// <summary>
/// 收到Participant的offer时触发
/// 为每个Participant创建独立的VideoStreamReceiver和AudioStreamReceiver
/// 然后添加共享的Sender并回复answer
/// </summary>
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<VideoStreamReceiver>();
ps.videoReceiver.renderMode = VideoRenderMode.APIOnly;
this.streams.Add(ps.videoReceiver);
ps.audioReceiver = go.AddComponent<AudioStreamReceiver>();
this.streams.Add(ps.audioReceiver);
var audioSource = go.AddComponent<AudioSource>();
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<IStreamSender>())
{
AddSender(data.connectionId, source);
}
foreach (var channel in streams.OfType<IDataChannel>().Where(c => c.IsLocal))
{
AddChannel(data.connectionId, channel);
}
// 不再手动调用 SendAnswer
// SignalingManagerInternal.OnOffer 会在 OnGotDescription 完成后自动调用 SendAnswer
}
/// <summary>
/// 收到远程Track时触发
/// 使用该Participant的独立Receiver来接收
/// </summary>
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);
}
/// <summary>
/// 收到远程DataChannel时触发
/// </summary>
public void OnAddChannel(SignalingEventData data)
{
var channel = streams.OfType<IDataChannel>().
FirstOrDefault(r => !r.IsConnected && !r.IsLocal);
channel?.SetChannel(data.connectionId, data.channel);
}
/// <summary>
/// 新Participant加入房间通知
/// 在私有模式下服务器会通知Host有新的Participant加入
/// 此时不做额外处理等收到Participant的offer时再建立连接
/// </summary>
public void OnParticipantJoined(SignalingEventData eventData)
{
Debug.Log($"[HostConnection] Participant joined: connectionId={eventData.connectionId}, participantId={eventData.participantId}");
}
/// <summary>
/// Participant离开房间通知
/// 清理该Participant的连接资源
/// </summary>
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);
}
}
/// <summary>
/// 收到呼叫请求
/// </summary>
public void OnCallRequest(SignalingEventData eventData)
{
Debug.Log($"[HostConnection] Call request from: {eventData.connectionId}, data: {eventData.message}");
}
/// <summary>
/// 收到自定义消息
/// </summary>
public void OnMessage(SignalingEventData eventData)
{
Debug.Log($"[HostConnection] Message from: {eventData.connectionId}, participantId: {eventData.participantId}, message: {eventData.message}");
}
/// <summary>
/// 获取当前连接的Participant数量
/// </summary>
public int ParticipantCount => participantIds.Count;
/// <summary>
/// 获取所有Participant的ID列表
/// </summary>
public IReadOnlyList<string> ParticipantIds => participantIds.AsReadOnly();
/// <summary>
/// 获取指定Participant的Receiver信息
/// </summary>
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<AudioStreamReceiver>().FirstOrDefault();
if (kind == TrackKind.Video)
return streams.OfType<VideoStreamReceiver>().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();
}
}
}