242 lines
8.6 KiB
C#
242 lines
8.6 KiB
C#
|
|
using System.Collections.Generic;
|
|||
|
|
using Stary.Evo;
|
|||
|
|
using Unity.RenderStreaming;
|
|||
|
|
using UnityEngine;
|
|||
|
|
using UnityEngine.UI;
|
|||
|
|
|
|||
|
|
namespace Script
|
|||
|
|
{
|
|||
|
|
public interface IRenderStreamingSystem : ISystem
|
|||
|
|
{
|
|||
|
|
void SetUp();
|
|||
|
|
void HangUp();
|
|||
|
|
|
|||
|
|
void SetLocalVideoImage(RawImage localVideoImage);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public class RenderStreamingSystem : AbstractSystem, IRenderStreamingSystem
|
|||
|
|
{
|
|||
|
|
[Header("核心组件")] private RenderStreamingSettings settings;
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Host连接
|
|||
|
|
/// </summary>
|
|||
|
|
private HostConnection hostConnection;
|
|||
|
|
|
|||
|
|
[Header("Host本地视频")] private VideoStreamSender videoStreamSender;
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 麦克风流发送器
|
|||
|
|
/// </summary>
|
|||
|
|
private AudioStreamSender microphoneStreamer;
|
|||
|
|
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 每个Participant的UI信息
|
|||
|
|
/// </summary>
|
|||
|
|
private readonly Dictionary<string, ParticipantUI> participantUIs = new();
|
|||
|
|
|
|||
|
|
|
|||
|
|
[Header("Participant视频容器")] private Transform participantVideoContainer;
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 渲染流管理
|
|||
|
|
/// </summary>
|
|||
|
|
private SignalingManager _signalingManager;
|
|||
|
|
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 本地视频显示
|
|||
|
|
/// </summary>
|
|||
|
|
private RawImage localVideoImage;
|
|||
|
|
|
|||
|
|
protected override void OnInit()
|
|||
|
|
{
|
|||
|
|
var renderStreaming = GameObject.Find("RenderStreaming").transform;
|
|||
|
|
_signalingManager = renderStreaming.GetComponent<SignalingManager>();
|
|||
|
|
hostConnection = renderStreaming.GetComponent<HostConnection>();
|
|||
|
|
videoStreamSender = hostConnection.GetComponent<VideoStreamSender>();
|
|||
|
|
microphoneStreamer = hostConnection.GetComponent<AudioStreamSender>();
|
|||
|
|
if (settings == null)
|
|||
|
|
settings = new RenderStreamingSettings();
|
|||
|
|
if (settings != null)
|
|||
|
|
{
|
|||
|
|
videoStreamSender.width = (uint)settings.StreamSize.x;
|
|||
|
|
videoStreamSender.height = (uint)settings.StreamSize.y;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (_signalingManager.runOnAwake)
|
|||
|
|
return;
|
|||
|
|
if (settings != null)
|
|||
|
|
_signalingManager.useDefaultSettings = settings.UseDefaultSettings;
|
|||
|
|
if (settings?.SignalingSettings != null)
|
|||
|
|
_signalingManager.SetSignalingSettings(settings.SignalingSettings);
|
|||
|
|
if (settings != null)
|
|||
|
|
videoStreamSender.SetCodec(settings.SenderVideoCodec);
|
|||
|
|
_signalingManager.Run();
|
|||
|
|
|
|||
|
|
|
|||
|
|
if (hostConnection != null)
|
|||
|
|
{
|
|||
|
|
hostConnection.OnParticipantConnected += HandleParticipantConnected;
|
|||
|
|
hostConnection.OnParticipantDisconnected += HandleParticipantDisconnected;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 本地视频:Sender启动后显示
|
|||
|
|
videoStreamSender.OnStartedStream += OnStartedStream;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
public override void Dispose()
|
|||
|
|
{
|
|||
|
|
if (hostConnection != null)
|
|||
|
|
{
|
|||
|
|
hostConnection.OnParticipantConnected -= HandleParticipantConnected;
|
|||
|
|
hostConnection.OnParticipantDisconnected -= HandleParticipantDisconnected;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
videoStreamSender.OnStartedStream -= OnStartedStream;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void OnStartedStream(string id)
|
|||
|
|
{
|
|||
|
|
if (videoStreamSender.sourceWebCamTexture != null && localVideoImage != null)
|
|||
|
|
localVideoImage.texture = videoStreamSender.sourceWebCamTexture;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public void SetUp()
|
|||
|
|
{
|
|||
|
|
videoStreamSender.enabled = true;
|
|||
|
|
microphoneStreamer.enabled = true;
|
|||
|
|
hostConnection.RoomConnectionId = this.GetSystem<IGlobalConfigSystem>().GetConnectionId();
|
|||
|
|
hostConnection.CreateConnection(hostConnection.RoomConnectionId);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public void HangUp()
|
|||
|
|
{
|
|||
|
|
videoStreamSender.enabled = false;
|
|||
|
|
microphoneStreamer.enabled = false;
|
|||
|
|
hostConnection.DeleteConnection(hostConnection.RoomConnectionId);
|
|||
|
|
|
|||
|
|
// 清理所有Participant UI
|
|||
|
|
foreach (var ui in participantUIs.Values)
|
|||
|
|
if (ui.root != null)
|
|||
|
|
GameObject.Destroy(ui.root);
|
|||
|
|
|
|||
|
|
participantUIs.Clear();
|
|||
|
|
if (localVideoImage != null)
|
|||
|
|
localVideoImage.texture = null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public void SetLocalVideoImage(RawImage localVideoImage)
|
|||
|
|
{
|
|||
|
|
this.localVideoImage = localVideoImage;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#region 开启相关
|
|||
|
|
|
|||
|
|
/// <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
|
|||
|
|
/// 结构: [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;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Participant断开连接回调
|
|||
|
|
/// 销毁其UI
|
|||
|
|
/// </summary>
|
|||
|
|
private void HandleParticipantDisconnected(string participantId)
|
|||
|
|
{
|
|||
|
|
if (participantUIs.TryGetValue(participantId, out var ui))
|
|||
|
|
{
|
|||
|
|
if (ui.root != null)
|
|||
|
|
GameObject.Destroy(ui.root);
|
|||
|
|
participantUIs.Remove(participantId);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Debug.Log($"[MultiParticipantHost] Participant UI removed: {participantId}");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#endregion
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public class ParticipantUI
|
|||
|
|
{
|
|||
|
|
public Text nameLabel;
|
|||
|
|
public GameObject root;
|
|||
|
|
public RawImage videoImage;
|
|||
|
|
}
|
|||
|
|
}
|