361 lines
13 KiB
C#
361 lines
13 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using Newtonsoft.Json;
|
||
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);
|
||
_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();
|
||
|
||
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];
|
||
var avatar = _profileSprites[_profileSpriteIndex].name.Replace("(Clone)", "");
|
||
this.GetSystem<IGlobalConfigSystem>().SetConnectionTexture(avatar);
|
||
_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}的房间";
|
||
}
|
||
|
||
private string OnClickRandomUserId()
|
||
{
|
||
return $"user_{Random.Range(100, 999)}";
|
||
}
|
||
|
||
private void OnClickStartButton()
|
||
{
|
||
this.GetSystem<IGlobalConfigSystem>().SetUserId(OnClickRandomUserId());
|
||
this.GetSystem<IGlobalConfigSystem>().SetConnectionTimeType(_timeDropdown.value);
|
||
this.GetSystem<IGlobalConfigSystem>().SetConnectionId(_meetingId.text);
|
||
hostConnection.RoomConnectionId = this.GetSystem<IGlobalConfigSystem>().GetConnectionId();
|
||
this.GetSystem<IGlobalConfigSystem>().SetConnectionName(_meetingNameInput.text);
|
||
if (!SignalingMessageHelper.IsReady())
|
||
{
|
||
Debug.LogError("Signaling 未就绪");
|
||
return;
|
||
}
|
||
|
||
var avatar = _profileImage.sprite.name.Replace("(Clone)", "");
|
||
var userInfo = new
|
||
{
|
||
type = "host-userInfo",
|
||
data = new
|
||
{
|
||
id = this.GetSystem<IGlobalConfigSystem>().GetUserId(),
|
||
name = _meetingNameInput.text,
|
||
avatar = $"{this.GetSystem<IGlobalConfigSystem>().IP}/images/head/{avatar}.png"
|
||
}
|
||
};
|
||
SignalingMessageHelper.SendMessage(JsonConvert.SerializeObject(userInfo));
|
||
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
|
||
}
|
||
} |