兼容完成

This commit is contained in:
2026-04-30 15:35:47 +08:00
parent 231021d318
commit 2792916807
152 changed files with 5894 additions and 67947 deletions

View File

@@ -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的独立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)
@@ -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();
}
}
}

View File

@@ -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)

View File

@@ -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>