兼容完成
This commit is contained in:
256
Assets/Script/MultiParticipantHostSample.cs
Normal file
256
Assets/Script/MultiParticipantHostSample.cs
Normal file
@@ -0,0 +1,256 @@
|
||||
// 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>
|
||||
[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;
|
||||
|
||||
/// <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()
|
||||
{
|
||||
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}";
|
||||
}
|
||||
|
||||
/// <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 =>
|
||||
{
|
||||
if (ui.videoImage != 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;
|
||||
|
||||
// 名称标签
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Assets/Script/MultiParticipantHostSample.cs.meta
Normal file
3
Assets/Script/MultiParticipantHostSample.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d7f4e4288a9d4b26b7476ec311616fc2
|
||||
timeCreated: 1777451230
|
||||
100
Assets/Script/RenderStreamingSettings.cs
Normal file
100
Assets/Script/RenderStreamingSettings.cs
Normal file
@@ -0,0 +1,100 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Unity.RenderStreaming
|
||||
{
|
||||
internal enum SignalingType
|
||||
{
|
||||
WebSocket,
|
||||
Http,
|
||||
}
|
||||
internal class RenderStreamingSettings
|
||||
{
|
||||
public const int DefaultStreamWidth = 1280;
|
||||
public const int DefaultStreamHeight = 720;
|
||||
|
||||
private bool useDefaultSettings = true;
|
||||
private SignalingType signalingType = SignalingType.WebSocket;
|
||||
private string signalingAddress = "localhost";
|
||||
private int signalingInterval = 5000;
|
||||
private bool signalingSecured = false;
|
||||
private Vector2Int streamSize = new Vector2Int(DefaultStreamWidth, DefaultStreamHeight);
|
||||
private VideoCodecInfo receiverVideoCodec = null;
|
||||
private VideoCodecInfo senderVideoCodec = null;
|
||||
|
||||
public bool UseDefaultSettings
|
||||
{
|
||||
get { return useDefaultSettings; }
|
||||
set { useDefaultSettings = value; }
|
||||
}
|
||||
|
||||
public SignalingType SignalingType
|
||||
{
|
||||
get { return signalingType; }
|
||||
set { signalingType = value; }
|
||||
}
|
||||
|
||||
public string SignalingAddress
|
||||
{
|
||||
get { return signalingAddress; }
|
||||
set { signalingAddress = value; }
|
||||
}
|
||||
|
||||
public bool SignalingSecured
|
||||
{
|
||||
get { return signalingSecured; }
|
||||
set { signalingSecured = value; }
|
||||
}
|
||||
|
||||
public int SignalingInterval
|
||||
{
|
||||
get { return signalingInterval; }
|
||||
set { signalingInterval = value; }
|
||||
}
|
||||
|
||||
public SignalingSettings SignalingSettings
|
||||
{
|
||||
get
|
||||
{
|
||||
switch (signalingType)
|
||||
{
|
||||
case SignalingType.WebSocket:
|
||||
{
|
||||
var schema = signalingSecured ? "wss" : "ws";
|
||||
return new WebSocketSignalingSettings
|
||||
(
|
||||
url: $"{schema}://{signalingAddress}"
|
||||
);
|
||||
}
|
||||
case SignalingType.Http:
|
||||
{
|
||||
var schema = signalingSecured ? "https" : "http";
|
||||
return new HttpSignalingSettings
|
||||
(
|
||||
url: $"{schema}://{signalingAddress}",
|
||||
interval: signalingInterval
|
||||
);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public Vector2Int StreamSize
|
||||
{
|
||||
get { return streamSize; }
|
||||
set { streamSize = value; }
|
||||
}
|
||||
|
||||
public VideoCodecInfo ReceiverVideoCodec
|
||||
{
|
||||
get { return receiverVideoCodec; }
|
||||
set { receiverVideoCodec = value; }
|
||||
}
|
||||
|
||||
public VideoCodecInfo SenderVideoCodec
|
||||
{
|
||||
get { return senderVideoCodec; }
|
||||
set { senderVideoCodec = value; }
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Assets/Script/RenderStreamingSettings.cs.meta
Normal file
3
Assets/Script/RenderStreamingSettings.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2fb2fd1cab5048e5b3f1bbe28826d1b9
|
||||
timeCreated: 1777451952
|
||||
Reference in New Issue
Block a user