// D:\Project\Unity\webrtc\Assets\Script\MultiParticipantHostSample.cs
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
namespace Unity.RenderStreaming
{
///
/// 多Participant Host示例
/// Host显示自己的本地画面,同时为每个加入的Participant动态创建RawImage显示其远程画面
///
/// 使用方法:
/// 1. 将此脚本挂载到场景中的GameObject上
/// 2. 在Inspector中关联各字段
/// 3. participantVideoContainer 需要挂载 GridLayoutGroup 或 VerticalLayoutGroup
///
[RequireComponent(typeof(HostConnection))]
public class MultiParticipantHostSample : MonoBehaviour
{
#pragma warning disable 0649
[Header("核心组件")]
[SerializeField] private SignalingManager renderStreaming;
[SerializeField] private HostConnection hostConnection;
[Header("Host本地视频")]
[SerializeField] private VideoStreamSender videoStreamSender;
[SerializeField] private AudioStreamSender microphoneStreamer;
[SerializeField] private RawImage localVideoImage;
[SerializeField] private Toggle audioLoopbackToggle;
[Header("Participant视频容器")]
[Tooltip("用于放置动态创建的Participant视频UI的父物体,需挂载LayoutGroup")]
[SerializeField] private Transform participantVideoContainer;
[Header("UI控件")]
[SerializeField] private Button startButton;
[SerializeField] private Button setUpButton;
[SerializeField] private Button hangUpButton;
[SerializeField] private InputField connectionIdInput;
[SerializeField] private Dropdown webcamSelectDropdown;
[SerializeField] private Dropdown microphoneSelectDropdown;
#pragma warning restore 0649
private string connectionId;
private RenderStreamingSettings settings;
///
/// 每个Participant的UI信息
///
private Dictionary participantUIs = new Dictionary();
private class ParticipantUI
{
public GameObject root;
public RawImage videoImage;
public Text nameLabel;
}
void Awake()
{
startButton.interactable = true;
// 初始化UI
setUpButton.interactable = true;
hangUpButton.interactable = false;
connectionIdInput.interactable = true;
startButton.onClick.AddListener(() =>
{
videoStreamSender.enabled = true;
startButton.interactable = false;
webcamSelectDropdown.interactable = false;
microphoneStreamer.enabled = true;
microphoneSelectDropdown.interactable = false;
setUpButton.interactable = true;
});
setUpButton.onClick.AddListener(SetUp);
hangUpButton.onClick.AddListener(HangUp);
connectionIdInput.onValueChanged.AddListener(input => connectionId = input);
connectionIdInput.text = $"{Random.Range(0, 99999):D5}";
// 本地视频:Sender启动后显示
videoStreamSender.OnStartedStream += id =>
{
if (videoStreamSender.sourceWebCamTexture != null)
localVideoImage.texture = videoStreamSender.sourceWebCamTexture;
};
// 订阅HostConnection事件
hostConnection.OnParticipantConnected += HandleParticipantConnected;
hostConnection.OnParticipantDisconnected += HandleParticipantDisconnected;
if (settings == null)
settings = new RenderStreamingSettings();
if (settings != null)
{
videoStreamSender.width = (uint)settings.StreamSize.x;
videoStreamSender.height = (uint)settings.StreamSize.y;
}
audioLoopbackToggle.onValueChanged.AddListener(isOn =>
{
microphoneStreamer.loopback = isOn;
});
microphoneStreamer.OnStartedStream += id => microphoneStreamer.loopback = audioLoopbackToggle.isOn;
microphoneSelectDropdown.onValueChanged.AddListener(index => microphoneStreamer.sourceDeviceIndex = index);
microphoneSelectDropdown.options =
Microphone.devices.Select(x => new Dropdown.OptionData(x)).ToList();
}
void Start()
{
if (renderStreaming.runOnAwake)
return;
if (settings != null)
renderStreaming.useDefaultSettings = settings.UseDefaultSettings;
if (settings?.SignalingSettings != null)
renderStreaming.SetSignalingSettings(settings.SignalingSettings);
renderStreaming.Run();
}
private void SetUp()
{
setUpButton.interactable = false;
hangUpButton.interactable = true;
connectionIdInput.interactable = false;
videoStreamSender.enabled = true;
if (settings != null)
videoStreamSender.SetCodec(settings.SenderVideoCodec);
hostConnection.CreateConnection(connectionId);
}
private void HangUp()
{
hostConnection.DeleteConnection(connectionId);
// 清理所有Participant UI
foreach (var ui in participantUIs.Values)
{
if (ui.root != null)
Destroy(ui.root);
}
participantUIs.Clear();
localVideoImage.texture = null;
setUpButton.interactable = true;
hangUpButton.interactable = false;
connectionIdInput.interactable = true;
connectionIdInput.text = $"{Random.Range(0, 99999):D5}";
}
///
/// 新Participant连接成功回调
/// 此时Participant的独立Receiver已创建,可以绑定视频显示
///
private void HandleParticipantConnected(ParticipantStreams ps)
{
// 创建Participant UI
var ui = CreateParticipantUI(ps.participantId);
participantUIs[ps.participantId] = ui;
// 绑定视频:当Receiver收到纹理时更新RawImage
ps.videoReceiver.OnUpdateReceiveTexture += texture =>
{
ui.videoImage.color = Color.white;
// 防止纹理为null时导致RawImage闪黑(重协商/track切换时可能短暂为null)
if (ui.videoImage != null && texture != null)
ui.videoImage.texture = texture;
};
// 绑定音频:AudioSource已在HostConnection中配置
ps.audioReceiver.OnUpdateReceiveAudioSource += source =>
{
if (source != null && !source.isPlaying)
{
source.loop = true;
source.Play();
}
};
Debug.Log($"[MultiParticipantHost] Participant UI created: {ps.participantId}");
}
///
/// Participant断开连接回调
/// 销毁其UI
///
private void HandleParticipantDisconnected(string participantId)
{
if (participantUIs.TryGetValue(participantId, out var ui))
{
if (ui.root != null)
Destroy(ui.root);
participantUIs.Remove(participantId);
}
Debug.Log($"[MultiParticipantHost] Participant UI removed: {participantId}");
}
///
/// 动态创建单个Participant的视频显示UI
/// 结构: [NameLabel] + [RawImage(视频画面)]
///
private ParticipantUI CreateParticipantUI(string participantId)
{
var ui = new ParticipantUI();
// 根节点
ui.root = new GameObject($"ParticipantUI_{participantId}");
ui.root.transform.SetParent(participantVideoContainer, false);
// 添加VerticalLayoutGroup使内容垂直排列
var vlg = ui.root.AddComponent();
vlg.childControlWidth = true;
vlg.childControlHeight = false;
vlg.childForceExpandWidth = true;
vlg.childForceExpandHeight = false;
vlg.spacing = 2;
ui.root.transform.GetComponent().anchorMin = Vector2.zero;
ui.root.transform.GetComponent().anchorMax = Vector2.one;
// 名称标签
var labelObj = new GameObject("NameLabel");
labelObj.transform.SetParent(ui.root.transform, false);
ui.nameLabel = labelObj.AddComponent();
ui.nameLabel.text = $"Participant: {participantId}";
ui.nameLabel.fontSize = 14;
ui.nameLabel.color = Color.white;
ui.nameLabel.alignment = TextAnchor.MiddleCenter;
var labelLayout = labelObj.AddComponent();
labelLayout.preferredHeight = 20;
// 视频画面
var imageObj = new GameObject("VideoImage");
imageObj.transform.SetParent(ui.root.transform, false);
ui.videoImage = imageObj.AddComponent();
ui.videoImage.color = Color.black;
var imageLayout = imageObj.AddComponent();
imageLayout.preferredHeight = 200;
// AspectRatioFitter保持视频比例
var aspectRatio = imageObj.AddComponent();
aspectRatio.aspectMode = AspectRatioFitter.AspectMode.FitInParent;
aspectRatio.aspectRatio = 16f / 9f;
return ui;
}
void OnDestroy()
{
if (hostConnection != null)
{
hostConnection.OnParticipantConnected -= HandleParticipantConnected;
hostConnection.OnParticipantDisconnected -= HandleParticipantDisconnected;
}
}
}
}