兼容完成
This commit is contained in:
@@ -1,21 +1,35 @@
|
||||
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
|
||||
/// 支持多个Participant连接,每个Participant拥有独立的PeerConnection和Receiver
|
||||
///
|
||||
/// 工作原理:
|
||||
/// - Host通过CreateConnection(connectionId)创建房间连接
|
||||
/// - 当Participant加入时,服务器发送participant-joined通知
|
||||
/// - WebSocketSignaling将participantId作为内部connectionId使用,
|
||||
/// 因此每个Participant会自动创建独立的PeerConnection
|
||||
/// - 当收到Participant的offer时,自动添加Stream并发送answer
|
||||
/// - 当Participant离开时,自动清理对应的连接资源
|
||||
/// - 当收到Participant的offer时,为该Participant创建独立的Receiver并添加Sender
|
||||
/// - 当Participant离开时,自动清理对应的连接资源和Receiver
|
||||
/// </summary>
|
||||
[AddComponentMenu("Render Streaming/Host Connection")]
|
||||
public class HostConnection : SignalingHandlerBase,
|
||||
@@ -35,6 +49,24 @@ namespace Unity.RenderStreaming
|
||||
/// </summary>
|
||||
private string roomConnectionId;
|
||||
|
||||
/// <summary>
|
||||
/// 每个Participant的独立Receiver(key=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)
|
||||
@@ -97,56 +129,107 @@ namespace Unity.RenderStreaming
|
||||
if (!participantIds.Contains(connectionId) && connectionId != roomConnectionId)
|
||||
return;
|
||||
|
||||
participantIds.Remove(connectionId);
|
||||
|
||||
foreach (var sender in streams.OfType<IStreamSender>())
|
||||
if (participantIds.Contains(connectionId))
|
||||
{
|
||||
RemoveSender(connectionId, sender);
|
||||
DisconnectParticipant(connectionId);
|
||||
participantIds.Remove(connectionId);
|
||||
}
|
||||
foreach (var receiver in streams.OfType<IStreamReceiver>())
|
||||
else
|
||||
{
|
||||
RemoveReceiver(connectionId, receiver);
|
||||
}
|
||||
foreach (var channel in streams.OfType<IDataChannel>().Where(c => c.ConnectionId == connectionId))
|
||||
{
|
||||
RemoveChannel(connectionId, channel);
|
||||
foreach (var sender in streams.OfType<IStreamSender>())
|
||||
{
|
||||
RemoveSender(connectionId, sender);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 断开指定Participant的连接资源
|
||||
/// 断开指定Participant的连接资源,销毁其独立的Receiver
|
||||
/// </summary>
|
||||
private void DisconnectParticipant(string participantId)
|
||||
{
|
||||
foreach (var sender in streams.OfType<IStreamSender>())
|
||||
// 清理独立Receiver
|
||||
if (m_participantStreams.TryGetValue(participantId, out var ps))
|
||||
{
|
||||
RemoveSender(participantId, sender);
|
||||
// 移除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);
|
||||
}
|
||||
foreach (var receiver in streams.OfType<IStreamReceiver>())
|
||||
else
|
||||
{
|
||||
RemoveReceiver(participantId, receiver);
|
||||
}
|
||||
foreach (var channel in streams.OfType<IDataChannel>().Where(c => c.ConnectionId == participantId))
|
||||
{
|
||||
RemoveChannel(participantId, channel);
|
||||
// 回退:无独立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时触发
|
||||
/// WebSocketSignaling已经将participantId作为内部connectionId
|
||||
/// 所以每个Participant的offer会自动创建独立的PeerConnection
|
||||
/// 为每个Participant创建独立的VideoStreamReceiver和AudioStreamReceiver,
|
||||
/// 然后添加共享的Sender并回复answer
|
||||
/// </summary>
|
||||
public void OnOffer(SignalingEventData data)
|
||||
{
|
||||
// 记录新的Participant连接
|
||||
if (!participantIds.Contains(data.connectionId))
|
||||
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);
|
||||
}
|
||||
|
||||
// 为该Participant添加所有Stream
|
||||
// 在 SetRemoteDescription 之前添加 Sender 和 Channel
|
||||
// 这样 transceiver 会正确匹配 offer 中的媒体行
|
||||
foreach (var source in streams.OfType<IStreamSender>())
|
||||
{
|
||||
AddSender(data.connectionId, source);
|
||||
@@ -156,18 +239,38 @@ namespace Unity.RenderStreaming
|
||||
AddChannel(data.connectionId, channel);
|
||||
}
|
||||
|
||||
// 发送answer给该Participant
|
||||
SendAnswer(data.connectionId);
|
||||
// 不再手动调用 SendAnswer
|
||||
// SignalingManagerInternal.OnOffer 会在 OnGotDescription 完成后自动调用 SendAnswer
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 收到远程Track时触发
|
||||
/// 使用该Participant的独立Receiver来接收
|
||||
/// </summary>
|
||||
public void OnAddReceiver(SignalingEventData data)
|
||||
{
|
||||
var track = data.transceiver.Receiver.Track;
|
||||
IStreamReceiver receiver = GetReceiver(track.Kind);
|
||||
SetReceiver(data.connectionId, receiver, data.transceiver);
|
||||
|
||||
// 优先使用该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>
|
||||
@@ -231,13 +334,32 @@ namespace Unity.RenderStreaming
|
||||
/// </summary>
|
||||
public IReadOnlyList<string> ParticipantIds => participantIds.AsReadOnly();
|
||||
|
||||
IStreamReceiver GetReceiver(WebRTC.TrackKind kind)
|
||||
/// <summary>
|
||||
/// 获取指定Participant的Receiver信息
|
||||
/// </summary>
|
||||
public bool TryGetParticipantStreams(string participantId, out ParticipantStreams ps)
|
||||
{
|
||||
if (kind == WebRTC.TrackKind.Audio)
|
||||
return m_participantStreams.TryGetValue(participantId, out ps);
|
||||
}
|
||||
|
||||
IStreamReceiver GetReceiver(TrackKind kind)
|
||||
{
|
||||
if (kind == TrackKind.Audio)
|
||||
return streams.OfType<AudioStreamReceiver>().FirstOrDefault();
|
||||
if (kind == WebRTC.TrackKind.Video)
|
||||
if (kind == TrackKind.Video)
|
||||
return streams.OfType<VideoStreamReceiver>().FirstOrDefault();
|
||||
throw new System.ArgumentException();
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -453,8 +453,17 @@ namespace Unity.RenderStreaming
|
||||
pc = CreatePeerConnection(connectionId, e.polite);
|
||||
}
|
||||
|
||||
// 先触发 onGotOffer,让 Handler 在 SetRemoteDescription 之前添加 transceiver
|
||||
// 这样 transceiver 会在 offer 的媒体行内正确匹配
|
||||
onGotOffer?.Invoke(connectionId, e.sdp);
|
||||
|
||||
// 然后设置远程描述,完成后自动 SendAnswer
|
||||
RTCSessionDescription description = new RTCSessionDescription { type = RTCSdpType.Offer, sdp = e.sdp };
|
||||
_startCoroutine(pc.OnGotDescription(description, () => onGotOffer?.Invoke(connectionId, e.sdp)));
|
||||
_startCoroutine(pc.OnGotDescription(description, () =>
|
||||
{
|
||||
// SetRemoteDescription 成功后,自动创建并发送 answer
|
||||
pc.SendAnswer();
|
||||
}));
|
||||
}
|
||||
|
||||
void OnParticipantJoinedHandler(ISignaling signaling, ParticipantEventData e)
|
||||
|
||||
@@ -59,7 +59,11 @@ namespace Unity.RenderStreaming
|
||||
{
|
||||
get { return m_Codec; }
|
||||
}
|
||||
|
||||
public VideoRenderMode renderMode
|
||||
{
|
||||
get { return m_RenderMode; }
|
||||
set { m_RenderMode = value; }
|
||||
}
|
||||
/// <summary>
|
||||
/// The width of the received video stream.
|
||||
/// </summary>
|
||||
|
||||
Reference in New Issue
Block a user