2026-05-17 11:35:43 +08:00
|
|
|
|
using System;
|
|
|
|
|
|
using System.Collections.Generic;
|
2026-05-18 23:31:04 +08:00
|
|
|
|
using Newtonsoft.Json;
|
2026-05-17 11:35:43 +08:00
|
|
|
|
using Script;
|
|
|
|
|
|
using Stary.Evo;
|
|
|
|
|
|
using Stary.Evo.UIFarme;
|
|
|
|
|
|
using UnityEngine;
|
|
|
|
|
|
using UnityEngine.U2D;
|
|
|
|
|
|
using UnityEngine.UI;
|
|
|
|
|
|
using Random = UnityEngine.Random;
|
|
|
|
|
|
|
|
|
|
|
|
namespace Unity.RenderStreaming
|
|
|
|
|
|
{
|
|
|
|
|
|
public class StartPanel : BasePanel
|
|
|
|
|
|
{
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 每个Participant的UI信息
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private readonly Dictionary<string, ParticipantUI> participantUIs = new();
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 返回按钮
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private Button _arrowLeft;
|
|
|
|
|
|
|
|
|
|
|
|
private Text _meetingId;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 房间号输入框
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private InputField _meetingNameInput;
|
|
|
|
|
|
|
|
|
|
|
|
private Image _profileImage;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 头像按钮
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private Button _profilePhoto;
|
|
|
|
|
|
|
|
|
|
|
|
private int _profileSpriteIndex;
|
|
|
|
|
|
private Sprite[] _profileSprites;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 随机房间号按钮
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private Button _randomMeetingId;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 头像Sprite Atlas
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private SpriteAtlas _spriteAtlas;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 开始按钮
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private Button _startButton;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 时间下拉选择框
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private Dropdown _timeDropdown;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Host连接
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private HostConnection hostConnection;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 本地视频显示
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private RawImage localVideoImage;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 麦克风流发送器
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private AudioStreamSender microphoneStreamer;
|
|
|
|
|
|
|
|
|
|
|
|
[Header("Participant视频容器")] private Transform participantVideoContainer;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 渲染流管理
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private SignalingManager renderStreaming;
|
|
|
|
|
|
|
|
|
|
|
|
[Header("核心组件")] private RenderStreamingSettings settings;
|
|
|
|
|
|
[Header("Host本地视频")] private VideoStreamSender videoStreamSender;
|
|
|
|
|
|
|
|
|
|
|
|
public override UITweenType TweenType => UITweenType.Fade;
|
|
|
|
|
|
|
|
|
|
|
|
public override string UIPath => "Canvas";
|
|
|
|
|
|
|
|
|
|
|
|
public override void Initialize(GameObject panelGo)
|
|
|
|
|
|
{
|
|
|
|
|
|
base.Initialize(panelGo);
|
2026-05-18 23:31:04 +08:00
|
|
|
|
_arrowLeft = panelGo.transform.Find("Herder/arrow-left").GetComponent<Button>();
|
|
|
|
|
|
_profilePhoto = panelGo.transform.Find("HeadPortraits/MeetingInfoCard").GetComponent<Button>();
|
|
|
|
|
|
_profileImage = _profilePhoto.transform.Find("mask/sprite").GetComponent<Image>();
|
|
|
|
|
|
_meetingNameInput = panelGo.transform.Find("card/search/InputField").GetComponent<InputField>();
|
|
|
|
|
|
_meetingId = panelGo.transform.Find("card/huiyiID/connectionId").GetComponent<Text>();
|
|
|
|
|
|
_randomMeetingId = panelGo.transform.Find("card/huiyiID/Button").GetComponent<Button>();
|
|
|
|
|
|
_timeDropdown = panelGo.transform.Find("card/time/Dropdown").GetComponent<Dropdown>();
|
|
|
|
|
|
_startButton = panelGo.transform.Find("invite").GetComponent<Button>();
|
|
|
|
|
|
_spriteAtlas = Resources.Load<SpriteAtlas>("SpriteAtlas");
|
|
|
|
|
|
if (_spriteAtlas != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
_profileSprites = new Sprite[_spriteAtlas.spriteCount];
|
|
|
|
|
|
_spriteAtlas.GetSprites(_profileSprites);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
_profileSpriteIndex = 0;
|
|
|
|
|
|
OnClickProfilePhoto();
|
|
|
|
|
|
|
2026-05-17 11:35:43 +08:00
|
|
|
|
renderStreaming = GameObject.FindObjectOfType<SignalingManager>();
|
|
|
|
|
|
hostConnection = GameObject.FindObjectOfType<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 (renderStreaming.runOnAwake)
|
|
|
|
|
|
return;
|
|
|
|
|
|
if (settings != null)
|
|
|
|
|
|
renderStreaming.useDefaultSettings = settings.UseDefaultSettings;
|
|
|
|
|
|
if (settings?.SignalingSettings != null)
|
|
|
|
|
|
renderStreaming.SetSignalingSettings(settings.SignalingSettings);
|
|
|
|
|
|
renderStreaming.Run();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public override void OnEnter(Action complete = null)
|
|
|
|
|
|
{
|
|
|
|
|
|
base.OnEnter(complete);
|
|
|
|
|
|
_arrowLeft.onClick.AddListener(OnClickArrowLeft);
|
|
|
|
|
|
_profilePhoto.onClick.AddListener(OnClickProfilePhoto);
|
|
|
|
|
|
_randomMeetingId.onClick.AddListener(OnClickRandomMeetingId);
|
|
|
|
|
|
|
|
|
|
|
|
_startButton.onClick.AddListener(OnClickStartButton);
|
|
|
|
|
|
if (hostConnection != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
hostConnection.OnParticipantConnected += HandleParticipantConnected;
|
|
|
|
|
|
hostConnection.OnParticipantDisconnected += HandleParticipantDisconnected;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 本地视频:Sender启动后显示
|
|
|
|
|
|
videoStreamSender.OnStartedStream += OnStartedStream;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public override void OnExit(float delay = 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
base.OnExit(delay);
|
|
|
|
|
|
_arrowLeft.onClick.RemoveListener(OnClickArrowLeft);
|
|
|
|
|
|
_profilePhoto.onClick.RemoveListener(OnClickProfilePhoto);
|
|
|
|
|
|
_randomMeetingId.onClick.RemoveListener(OnClickRandomMeetingId);
|
|
|
|
|
|
_startButton.onClick.RemoveListener(OnClickStartButton);
|
|
|
|
|
|
if (hostConnection != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
hostConnection.OnParticipantConnected -= HandleParticipantConnected;
|
|
|
|
|
|
hostConnection.OnParticipantDisconnected -= HandleParticipantDisconnected;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
videoStreamSender.OnStartedStream -= OnStartedStream;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void OnClickProfilePhoto()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (_profileSprites == null || _profileSprites.Length == 0) return;
|
|
|
|
|
|
_profileImage.sprite = _profileSprites[_profileSpriteIndex];
|
|
|
|
|
|
this.GetSystem<IGlobalConfigSystem>().SetConnectionTexture(_profileSprites[_profileSpriteIndex].texture);
|
|
|
|
|
|
_profileSpriteIndex = (_profileSpriteIndex + 1) % _profileSprites.Length;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void OnClickArrowLeft()
|
|
|
|
|
|
{
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void OnClickRandomMeetingId()
|
|
|
|
|
|
{
|
|
|
|
|
|
var meetingId = $"{Random.Range(100, 999)}-{Random.Range(100, 999)}-{Random.Range(100, 999)}";
|
|
|
|
|
|
_meetingId.text = meetingId;
|
|
|
|
|
|
if (string.IsNullOrEmpty(_meetingNameInput.text)) _meetingNameInput.text = $"{meetingId}的房间";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-18 23:31:04 +08:00
|
|
|
|
private string OnClickRandomUserId()
|
|
|
|
|
|
{
|
|
|
|
|
|
return $"user_{Random.Range(100, 999)}";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-17 11:35:43 +08:00
|
|
|
|
private void OnClickStartButton()
|
|
|
|
|
|
{
|
2026-05-18 23:31:04 +08:00
|
|
|
|
this.GetSystem<IGlobalConfigSystem>().SetUserId(OnClickRandomUserId());
|
2026-05-17 11:35:43 +08:00
|
|
|
|
this.GetSystem<IGlobalConfigSystem>().SetConnectionTimeType(_timeDropdown.value);
|
|
|
|
|
|
this.GetSystem<IGlobalConfigSystem>().SetConnectionId(_meetingId.text);
|
|
|
|
|
|
hostConnection.RoomConnectionId = this.GetSystem<IGlobalConfigSystem>().GetConnectionId();
|
|
|
|
|
|
this.GetSystem<IGlobalConfigSystem>().SetConnectionName(_meetingNameInput.text);
|
2026-05-18 23:31:04 +08:00
|
|
|
|
this.GetSystem<IGlobalConfigSystem>().SetConnectionTexture(_profileImage.sprite.texture);
|
|
|
|
|
|
if (!SignalingMessageHelper.IsReady())
|
|
|
|
|
|
{
|
|
|
|
|
|
Debug.LogError("Signaling 未就绪");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var avatar = _profileImage.sprite.name.Replace("(Clone)", "");
|
|
|
|
|
|
var userInfo = new
|
|
|
|
|
|
{
|
|
|
|
|
|
type = "host-userInfo",
|
|
|
|
|
|
data = new
|
|
|
|
|
|
{
|
|
|
|
|
|
connectionId = hostConnection.RoomConnectionId,
|
|
|
|
|
|
id = this.GetSystem<IGlobalConfigSystem>().GetUserId(),
|
|
|
|
|
|
name = _meetingNameInput.text,
|
|
|
|
|
|
avatar = $"{this.GetSystem<IGlobalConfigSystem>().IP}/images/head/{avatar}.png"
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
SignalingMessageHelper.SendMessage(JsonConvert.SerializeObject(userInfo));
|
2026-05-17 11:35:43 +08:00
|
|
|
|
SetUp();
|
|
|
|
|
|
PanelSystem.PopQueue<StartPanel>();
|
|
|
|
|
|
PanelSystem.PushQueue<MainPanel>();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void OnStartedStream(string id)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (videoStreamSender.sourceWebCamTexture != null)
|
|
|
|
|
|
localVideoImage.texture = videoStreamSender.sourceWebCamTexture;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private class ParticipantUI
|
|
|
|
|
|
{
|
|
|
|
|
|
public Text nameLabel;
|
|
|
|
|
|
public GameObject root;
|
|
|
|
|
|
public RawImage videoImage;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#region 开启相关
|
|
|
|
|
|
|
|
|
|
|
|
private void SetUp()
|
|
|
|
|
|
{
|
|
|
|
|
|
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)
|
|
|
|
|
|
GameObject.Destroy(ui.root);
|
|
|
|
|
|
|
|
|
|
|
|
participantUIs.Clear();
|
|
|
|
|
|
|
|
|
|
|
|
localVideoImage.texture = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <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
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|