2026-04-29 15:13:24 +08:00
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
|
using System.Linq;
|
2026-04-30 15:35:47 +08:00
|
|
|
|
using Unity.WebRTC;
|
2026-04-29 15:13:24 +08:00
|
|
|
|
using UnityEngine;
|
|
|
|
|
|
|
|
|
|
|
|
namespace Unity.RenderStreaming
|
|
|
|
|
|
{
|
2026-04-30 15:35:47 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 每个Participant的接收器信息
|
|
|
|
|
|
/// VideoStreamReceiver/AudioStreamReceiver 只持有一个transceiver,
|
|
|
|
|
|
/// 因此每个Participant需要独立的Receiver实例
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public class ParticipantStreams
|
|
|
|
|
|
{
|
|
|
|
|
|
public string participantId;
|
|
|
|
|
|
public VideoStreamReceiver videoReceiver;
|
|
|
|
|
|
public AudioStreamReceiver audioReceiver;
|
|
|
|
|
|
public GameObject gameObject;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-29 15:13:24 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Host模式的连接处理器
|
|
|
|
|
|
/// 适用于私有模式下Unity作为Host(第一个加入房间的客户端)
|
2026-04-30 15:35:47 +08:00
|
|
|
|
/// 支持多个Participant连接,每个Participant拥有独立的PeerConnection和Receiver
|
2026-04-29 15:13:24 +08:00
|
|
|
|
///
|
|
|
|
|
|
/// 工作原理:
|
|
|
|
|
|
/// - Host通过CreateConnection(connectionId)创建房间连接
|
|
|
|
|
|
/// - 当Participant加入时,服务器发送participant-joined通知
|
|
|
|
|
|
/// - WebSocketSignaling将participantId作为内部connectionId使用,
|
|
|
|
|
|
/// 因此每个Participant会自动创建独立的PeerConnection
|
2026-04-30 15:35:47 +08:00
|
|
|
|
/// - 当收到Participant的offer时,为该Participant创建独立的Receiver并添加Sender
|
|
|
|
|
|
/// - 当Participant离开时,自动清理对应的连接资源和Receiver
|
2026-04-29 15:13:24 +08:00
|
|
|
|
/// </summary>
|
|
|
|
|
|
[AddComponentMenu("Render Streaming/Host Connection")]
|
|
|
|
|
|
public class HostConnection : SignalingHandlerBase,
|
|
|
|
|
|
IOfferHandler, IAddChannelHandler, IDisconnectHandler, IDeletedConnectionHandler,
|
|
|
|
|
|
IAddReceiverHandler, IParticipantJoinedHandler, IParticipantLeftHandler,
|
|
|
|
|
|
ICallRequestHandler, IMessageHandler
|
|
|
|
|
|
{
|
2026-05-24 14:39:45 +08:00
|
|
|
|
[SerializeField] private List<Component> streams = new();
|
2026-04-29 15:13:24 +08:00
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 当前连接的所有Participant的participantId列表
|
|
|
|
|
|
/// </summary>
|
2026-05-24 14:39:45 +08:00
|
|
|
|
private List<string> participantIds = new();
|
2026-04-29 15:13:24 +08:00
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 房间的connectionId(Host创建连接时使用的ID)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private string roomConnectionId;
|
|
|
|
|
|
|
2026-05-14 21:25:42 +08:00
|
|
|
|
public string RoomConnectionId
|
|
|
|
|
|
{
|
2026-05-24 14:39:45 +08:00
|
|
|
|
get => roomConnectionId;
|
|
|
|
|
|
set => roomConnectionId = value;
|
2026-05-14 21:25:42 +08:00
|
|
|
|
}
|
2026-05-24 14:39:45 +08:00
|
|
|
|
|
2026-04-30 15:35:47 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 每个Participant的独立Receiver(key=participantId)
|
|
|
|
|
|
/// VideoStreamReceiver/AudioStreamReceiver内部只持有一个transceiver,
|
|
|
|
|
|
/// 必须为每个Participant创建独立的实例
|
|
|
|
|
|
/// </summary>
|
2026-05-24 14:39:45 +08:00
|
|
|
|
private Dictionary<string, ParticipantStreams> m_participantStreams = new();
|
2026-04-30 15:35:47 +08:00
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Participant连接成功事件,提供该Participant的Receiver引用
|
|
|
|
|
|
/// 外部脚本可订阅此事件来创建对应的UI显示(如RawImage)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public event System.Action<ParticipantStreams> OnParticipantConnected;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Participant断开连接事件
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public event System.Action<string> OnParticipantDisconnected;
|
|
|
|
|
|
|
2026-04-29 15:13:24 +08:00
|
|
|
|
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连接
|
2026-04-30 23:27:34 +08:00
|
|
|
|
// 注意:不能在这里调用 base.DeleteConnection(participantId),否则会给每个Participant发送disconnect,
|
|
|
|
|
|
// 导致服务器返回多次通知。应该只发一次disconnect关闭房间,Participant的清理由服务器通知触发。
|
2026-05-24 14:39:45 +08:00
|
|
|
|
foreach (var participantId in participantIds.ToList()) DisconnectParticipant(participantId);
|
2026-04-29 15:13:24 +08:00
|
|
|
|
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;
|
|
|
|
|
|
|
2026-04-30 15:35:47 +08:00
|
|
|
|
if (participantIds.Contains(connectionId))
|
2026-04-29 15:13:24 +08:00
|
|
|
|
{
|
2026-04-30 15:35:47 +08:00
|
|
|
|
DisconnectParticipant(connectionId);
|
|
|
|
|
|
participantIds.Remove(connectionId);
|
2026-04-29 15:13:24 +08:00
|
|
|
|
}
|
2026-04-30 15:35:47 +08:00
|
|
|
|
else
|
2026-04-29 15:13:24 +08:00
|
|
|
|
{
|
2026-05-24 14:39:45 +08:00
|
|
|
|
foreach (var sender in streams.OfType<IStreamSender>()) RemoveSender(connectionId, sender);
|
2026-04-29 15:13:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2026-04-30 15:35:47 +08:00
|
|
|
|
/// 断开指定Participant的连接资源,销毁其独立的Receiver
|
2026-04-29 15:13:24 +08:00
|
|
|
|
/// </summary>
|
|
|
|
|
|
private void DisconnectParticipant(string participantId)
|
|
|
|
|
|
{
|
2026-04-30 15:35:47 +08:00
|
|
|
|
// 清理独立Receiver
|
|
|
|
|
|
if (m_participantStreams.TryGetValue(participantId, out var ps))
|
2026-04-29 15:13:24 +08:00
|
|
|
|
{
|
2026-04-30 15:35:47 +08:00
|
|
|
|
// 移除Sender关联
|
2026-04-30 23:27:34 +08:00
|
|
|
|
// foreach (var sender in streams.OfType<IStreamSender>())
|
|
|
|
|
|
// {
|
|
|
|
|
|
// RemoveSender(participantId, sender);
|
|
|
|
|
|
// }
|
2026-04-30 15:35:47 +08:00
|
|
|
|
// 移除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)
|
|
|
|
|
|
{
|
2026-05-24 14:39:45 +08:00
|
|
|
|
streams.Remove(ps.audioReceiver);
|
|
|
|
|
|
streams.Remove(ps.videoReceiver);
|
2026-04-30 15:35:47 +08:00
|
|
|
|
Destroy(ps.gameObject);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
m_participantStreams.Remove(participantId);
|
2026-04-29 15:13:24 +08:00
|
|
|
|
}
|
2026-04-30 15:35:47 +08:00
|
|
|
|
else
|
2026-04-29 15:13:24 +08:00
|
|
|
|
{
|
2026-04-30 15:35:47 +08:00
|
|
|
|
// 回退:无独立Receiver时走共享streams清理
|
2026-05-24 14:39:45 +08:00
|
|
|
|
foreach (var sender in streams.OfType<IStreamSender>()) RemoveSender(participantId, sender);
|
|
|
|
|
|
foreach (var receiver in streams.OfType<IStreamReceiver>()) RemoveReceiver(participantId, receiver);
|
2026-04-30 15:35:47 +08:00
|
|
|
|
foreach (var channel in streams.OfType<IDataChannel>().Where(c => c.ConnectionId == participantId))
|
|
|
|
|
|
RemoveChannel(participantId, channel);
|
2026-04-29 15:13:24 +08:00
|
|
|
|
}
|
2026-04-30 15:35:47 +08:00
|
|
|
|
|
|
|
|
|
|
OnParticipantDisconnected?.Invoke(participantId);
|
2026-04-29 15:13:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 收到Participant的offer时触发
|
2026-04-30 15:35:47 +08:00
|
|
|
|
/// 为每个Participant创建独立的VideoStreamReceiver和AudioStreamReceiver,
|
|
|
|
|
|
/// 然后添加共享的Sender并回复answer
|
2026-04-29 15:13:24 +08:00
|
|
|
|
/// </summary>
|
|
|
|
|
|
public void OnOffer(SignalingEventData data)
|
|
|
|
|
|
{
|
2026-05-24 14:39:45 +08:00
|
|
|
|
var isNewParticipant = !participantIds.Contains(data.connectionId);
|
2026-04-30 15:35:47 +08:00
|
|
|
|
|
|
|
|
|
|
if (isNewParticipant)
|
2026-04-29 15:13:24 +08:00
|
|
|
|
{
|
|
|
|
|
|
participantIds.Add(data.connectionId);
|
|
|
|
|
|
Debug.Log($"[HostConnection] Participant offer received: {data.connectionId}");
|
2026-04-30 15:35:47 +08:00
|
|
|
|
|
|
|
|
|
|
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;
|
2026-05-24 14:39:45 +08:00
|
|
|
|
streams.Add(ps.videoReceiver);
|
2026-04-30 15:35:47 +08:00
|
|
|
|
ps.audioReceiver = go.AddComponent<AudioStreamReceiver>();
|
2026-05-24 14:39:45 +08:00
|
|
|
|
streams.Add(ps.audioReceiver);
|
2026-04-30 15:35:47 +08:00
|
|
|
|
var audioSource = go.AddComponent<AudioSource>();
|
|
|
|
|
|
audioSource.loop = true;
|
|
|
|
|
|
ps.audioReceiver.targetAudioSource = audioSource;
|
|
|
|
|
|
|
|
|
|
|
|
m_participantStreams[data.connectionId] = ps;
|
|
|
|
|
|
OnParticipantConnected?.Invoke(ps);
|
2026-04-29 15:13:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-30 15:35:47 +08:00
|
|
|
|
// 在 SetRemoteDescription 之前添加 Sender 和 Channel
|
|
|
|
|
|
// 这样 transceiver 会正确匹配 offer 中的媒体行
|
2026-05-24 14:39:45 +08:00
|
|
|
|
foreach (var source in streams.OfType<IStreamSender>()) AddSender(data.connectionId, source);
|
2026-04-29 15:13:24 +08:00
|
|
|
|
foreach (var channel in streams.OfType<IDataChannel>().Where(c => c.IsLocal))
|
|
|
|
|
|
AddChannel(data.connectionId, channel);
|
|
|
|
|
|
|
2026-04-30 15:35:47 +08:00
|
|
|
|
// 不再手动调用 SendAnswer
|
|
|
|
|
|
// SignalingManagerInternal.OnOffer 会在 OnGotDescription 完成后自动调用 SendAnswer
|
2026-04-29 15:13:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 收到远程Track时触发
|
2026-04-30 15:35:47 +08:00
|
|
|
|
/// 使用该Participant的独立Receiver来接收
|
2026-04-29 15:13:24 +08:00
|
|
|
|
/// </summary>
|
|
|
|
|
|
public void OnAddReceiver(SignalingEventData data)
|
|
|
|
|
|
{
|
|
|
|
|
|
var track = data.transceiver.Receiver.Track;
|
2026-04-30 15:35:47 +08:00
|
|
|
|
|
|
|
|
|
|
// 优先使用该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);
|
2026-04-29 15:13:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 收到远程DataChannel时触发
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public void OnAddChannel(SignalingEventData data)
|
|
|
|
|
|
{
|
2026-05-24 14:39:45 +08:00
|
|
|
|
if (data.channel == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
Debug.LogWarning(
|
|
|
|
|
|
$"[HostConnection] OnAddChannel received null channel for connectionId: {data.connectionId}");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var channelLabel = data.channel.Label;
|
|
|
|
|
|
var channel = streams.OfType<IDataChannel>()
|
|
|
|
|
|
.FirstOrDefault(r => !r.IsConnected && !r.IsLocal && r.Label == channelLabel);
|
|
|
|
|
|
|
|
|
|
|
|
if (channel != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
channel.SetChannel(data.connectionId, data.channel);
|
|
|
|
|
|
Debug.Log(
|
|
|
|
|
|
$"[HostConnection] DataChannel assigned: label={channelLabel}, connectionId={data.connectionId}");
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
Debug.LogWarning(
|
|
|
|
|
|
$"[HostConnection] No matching DataChannel found for label: {channelLabel}, connectionId: {data.connectionId}");
|
|
|
|
|
|
Debug.LogWarning(
|
|
|
|
|
|
$"[HostConnection] Available channels: {string.Join(", ", streams.OfType<IDataChannel>().Select(c => $"{c.Label}(Connected={c.IsConnected}, Local={c.IsLocal})"))}");
|
|
|
|
|
|
}
|
2026-04-29 15:13:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 新Participant加入房间通知
|
|
|
|
|
|
/// 在私有模式下,服务器会通知Host有新的Participant加入
|
|
|
|
|
|
/// 此时不做额外处理,等收到Participant的offer时再建立连接
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public void OnParticipantJoined(SignalingEventData eventData)
|
|
|
|
|
|
{
|
2026-05-24 14:39:45 +08:00
|
|
|
|
Debug.Log(
|
|
|
|
|
|
$"[HostConnection] Participant joined: connectionId={eventData.connectionId}, participantId={eventData.participantId}");
|
2026-04-29 15:13:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Participant离开房间通知
|
|
|
|
|
|
/// 清理该Participant的连接资源
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public void OnParticipantLeft(SignalingEventData eventData)
|
|
|
|
|
|
{
|
2026-05-24 14:39:45 +08:00
|
|
|
|
var participantId = eventData.participantId;
|
2026-04-29 15:13:24 +08:00
|
|
|
|
if (!string.IsNullOrEmpty(participantId))
|
|
|
|
|
|
{
|
2026-05-24 14:39:45 +08:00
|
|
|
|
Debug.Log(
|
|
|
|
|
|
$"[HostConnection] Participant left: connectionId={eventData.connectionId}, participantId={participantId}");
|
2026-04-29 15:13:24 +08:00
|
|
|
|
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)
|
|
|
|
|
|
{
|
2026-05-24 14:39:45 +08:00
|
|
|
|
// eventData.
|
|
|
|
|
|
Debug.Log(
|
|
|
|
|
|
$"[HostConnection] Message from: {eventData.connectionId}, participantId: {eventData.participantId}, message: {eventData.message}");
|
|
|
|
|
|
|
|
|
|
|
|
foreach (var channel in streams.OfType<DataChannelBase>().Where(c => c.Label == "on-message"))
|
2026-05-12 23:04:08 +08:00
|
|
|
|
if (channel != null)
|
|
|
|
|
|
channel.OnMessage(eventData.message);
|
2026-04-29 15:13:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 获取当前连接的Participant数量
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public int ParticipantCount => participantIds.Count;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 获取所有Participant的ID列表
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public IReadOnlyList<string> ParticipantIds => participantIds.AsReadOnly();
|
|
|
|
|
|
|
2026-04-30 15:35:47 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 获取指定Participant的Receiver信息
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public bool TryGetParticipantStreams(string participantId, out ParticipantStreams ps)
|
|
|
|
|
|
{
|
|
|
|
|
|
return m_participantStreams.TryGetValue(participantId, out ps);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-24 14:39:45 +08:00
|
|
|
|
private IStreamReceiver GetReceiver(TrackKind kind)
|
2026-04-29 15:13:24 +08:00
|
|
|
|
{
|
2026-04-30 15:35:47 +08:00
|
|
|
|
if (kind == TrackKind.Audio)
|
2026-04-29 15:13:24 +08:00
|
|
|
|
return streams.OfType<AudioStreamReceiver>().FirstOrDefault();
|
2026-04-30 15:35:47 +08:00
|
|
|
|
if (kind == TrackKind.Video)
|
2026-04-29 15:13:24 +08:00
|
|
|
|
return streams.OfType<VideoStreamReceiver>().FirstOrDefault();
|
2026-04-30 15:35:47 +08:00
|
|
|
|
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();
|
2026-04-29 15:13:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-05-24 14:39:45 +08:00
|
|
|
|
}
|