Files
webRtc/Assets/Script/MultiParticipantHost.cs
2026-05-14 21:25:42 +08:00

258 lines
10 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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
{
/// <summary>
/// 多Participant Host示例
/// Host显示自己的本地画面同时为每个加入的Participant动态创建RawImage显示其远程画面
///
/// 使用方法:
/// 1. 将此脚本挂载到场景中的GameObject上
/// 2. 在Inspector中关联各字段
/// 3. participantVideoContainer 需要挂载 GridLayoutGroup 或 VerticalLayoutGroup
/// </summary>
public class MultiParticipantHost : MonoBehaviour
{
[Header("核心组件")]
private SignalingManager renderStreaming;
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;
private RenderStreamingSettings settings;
/// <summary>
/// 每个Participant的UI信息
/// </summary>
private Dictionary<string, ParticipantUI> participantUIs = new Dictionary<string, ParticipantUI>();
private class ParticipantUI
{
public GameObject root;
public RawImage videoImage;
public Text nameLabel;
}
void Awake()
{
renderStreaming=GameObject.FindObjectOfType<SignalingManager>();
hostConnection=GameObject.FindObjectOfType<HostConnection>();
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 => hostConnection.RoomConnectionId = 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(hostConnection.RoomConnectionId);
}
private void HangUp()
{
hostConnection.DeleteConnection(hostConnection.RoomConnectionId);
// 清理所有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}";
}
/// <summary>
/// 新Participant连接成功回调
/// 此时Participant的独立Receiver已创建可以绑定视频显示
/// </summary>
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}");
}
/// <summary>
/// Participant断开连接回调
/// 销毁其UI
/// </summary>
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}");
}
/// <summary>
/// 动态创建单个Participant的视频显示UI
/// 结构: [NameLabel] + [RawImage(视频画面)]
/// </summary>
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<VerticalLayoutGroup>();
vlg.childControlWidth = true;
vlg.childControlHeight = false;
vlg.childForceExpandWidth = true;
vlg.childForceExpandHeight = false;
vlg.spacing = 2;
ui.root.transform.GetComponent<RectTransform>().anchorMin = Vector2.zero;
ui.root.transform.GetComponent<RectTransform>().anchorMax = Vector2.one;
// 名称标签
var labelObj = new GameObject("NameLabel");
labelObj.transform.SetParent(ui.root.transform, false);
ui.nameLabel = labelObj.AddComponent<Text>();
ui.nameLabel.text = $"Participant: {participantId}";
ui.nameLabel.fontSize = 14;
ui.nameLabel.color = Color.white;
ui.nameLabel.alignment = TextAnchor.MiddleCenter;
var labelLayout = labelObj.AddComponent<LayoutElement>();
labelLayout.preferredHeight = 20;
// 视频画面
var imageObj = new GameObject("VideoImage");
imageObj.transform.SetParent(ui.root.transform, false);
ui.videoImage = imageObj.AddComponent<RawImage>();
ui.videoImage.color = Color.black;
var imageLayout = imageObj.AddComponent<LayoutElement>();
imageLayout.preferredHeight = 200;
// AspectRatioFitter保持视频比例
var aspectRatio = imageObj.AddComponent<AspectRatioFitter>();
aspectRatio.aspectMode = AspectRatioFitter.AspectMode.FitInParent;
aspectRatio.aspectRatio = 16f / 9f;
return ui;
}
void OnDestroy()
{
if (hostConnection != null)
{
hostConnection.OnParticipantConnected -= HandleParticipantConnected;
hostConnection.OnParticipantDisconnected -= HandleParticipantDisconnected;
}
}
}
}