using System.Collections.Generic; using Stary.Evo; using Unity.RenderStreaming; using Unity.XR.XREAL; using UnityEngine; using UnityEngine.UI; namespace Script { public interface IRenderStreamingSystem : ISystem { void SetUp(); void HangUp(); } public class RenderStreamingSystem : AbstractSystem, IRenderStreamingSystem { [Header("核心组件")] private RenderStreamingSettings settings; /// /// Host连接 /// private HostConnection hostConnection; [Header("Host本地视频")] private VideoStreamSender videoStreamSender; /// /// 麦克风流发送器 /// private AudioStreamSender microphoneStreamer; /// /// 每个Participant的UI信息 /// private readonly Dictionary participantUIs = new(); [Header("Participant视频容器")] private Transform participantVideoContainer; /// /// 渲染流管理 /// private SignalingManager _signalingManager; private YUVToRenderTexture _yuvToRenderTexture; protected override void OnInit() { participantVideoContainer = GameObject.Find("CanvasMain/ParticipantUI").transform; var renderStreaming = GameObject.Find("RenderStreaming").transform; _signalingManager = renderStreaming.GetComponent(); hostConnection = renderStreaming.GetComponent(); videoStreamSender = hostConnection.GetComponent(); microphoneStreamer = hostConnection.GetComponent(); _yuvToRenderTexture = renderStreaming.AddOrGetComponent(); 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().GetConnectionId(); hostConnection.CreateConnection(hostConnection.RoomConnectionId); if (_yuvToRenderTexture != null) { _yuvToRenderTexture.Play(); videoStreamSender.sourceTexture = _yuvToRenderTexture.localRenderTexture; } } 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 (_yuvToRenderTexture != null) _yuvToRenderTexture.Stop(); } #region 开启相关 /// /// 新Participant连接成功回调 /// 此时Participant的独立Receiver已创建,可以绑定视频显示 /// 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}"); } /// /// 动态创建单个Participant的视频显示UI /// 结构: [NameLabel] + [RawImage(视频画面)] /// private ParticipantUI CreateParticipantUI(string participantId) { var ui = new ParticipantUI(); // 根节点 ui.root = new GameObject($"ParticipantUI_{participantId}").AddComponent().gameObject; ui.root.transform.SetParent(participantVideoContainer, false); ui.root.transform.GetComponent().anchorMin = new Vector2(0,1); ui.root.transform.GetComponent().anchorMax = new Vector2(0,1); ui.root.transform.GetComponent().pivot = new Vector2(0.5f,0.5f); if (participantUIs.Count % 2 == 0) { ui.root.transform.rotation = Quaternion.Euler(0, -45, 0); } else { ui.root.transform.rotation = Quaternion.Euler(0, 45, 0); } // 名称标签 var labelObj = new GameObject("NameLabel"); labelObj.transform.SetParent(ui.root.transform, false); ui.nameLabel = labelObj.AddComponent(); ui.nameLabel.text = $"Participant: {participantId}"; ui.nameLabel.fontSize = 14; ui.nameLabel.color = Color.white; ui.nameLabel.alignment = TextAnchor.MiddleCenter; var labelLayout = labelObj.AddComponent(); labelLayout.preferredHeight = 20; // 视频画面 var imageObj = new GameObject("VideoImage"); imageObj.transform.SetParent(ui.root.transform, false); ui.videoImage = imageObj.AddComponent(); ui.videoImage.color = Color.black; var imageLayout = imageObj.AddComponent(); imageLayout.preferredHeight = 200; // AspectRatioFitter保持视频比例 var aspectRatio = imageObj.AddComponent(); aspectRatio.aspectMode = AspectRatioFitter.AspectMode.FitInParent; aspectRatio.aspectRatio = 16f / 9f; return ui; } /// /// Participant断开连接回调 /// 销毁其UI /// 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; } }