// 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; } } } }