RomaForwardedImageViewer.cs 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. using System;
  2. using UnityEngine;
  3. using UnityEngine.SceneManagement;
  4. using UnityEngine.UI;
  5. namespace LightGlue.Unity.Roma
  6. {
  7. /// <summary>
  8. /// 接收 Python 转发过来的 WiFi 图像流(FrameHeader 分片协议),并显示到 RawImage。
  9. /// Roma 新场景专用。
  10. /// </summary>
  11. public sealed class RomaForwardedImageViewer : MonoBehaviour
  12. {
  13. public static RomaForwardedImageViewer Instance { get; private set; }
  14. [Header("Receive (Python -> Unity image forward)")]
  15. public string bindIp = "0.0.0.0";
  16. [Tooltip("Python 转发到 Unity 的图像端口(与 Python --forward_port 对齐)")]
  17. public int forwardPort = 12366;
  18. [Tooltip("最大队列大小(只保留最新N帧)")]
  19. public int maxQueueSize = 2;
  20. [Tooltip("在途帧上限(防止网络抖动时占用过多内存)")]
  21. public int maxInFlightFrames = 8;
  22. [Tooltip("分片重组超时(ms)")]
  23. public int frameTimeoutMs = 500;
  24. [Header("UI")]
  25. public RawImage targetRawImage;
  26. [Tooltip("若 RawImage 未赋值,是否自动在场景中寻找第一个 RawImage")]
  27. public bool autoFindRawImage = true;
  28. [Tooltip("切场景后若 RawImage 被销毁,是否自动尝试重新绑定(FindObjectOfType<RawImage>)。")]
  29. public bool autoRebindRawImageOnSceneLoaded = true;
  30. [Tooltip("若始终找不到 RawImage,是否自动创建一个常驻的全屏 RawImage 用于显示。")]
  31. public bool autoCreatePersistentRawImage = true;
  32. [Tooltip("自动创建的常驻显示根节点名称")]
  33. public string persistentDisplayRootName = "RomaForwardedImageDisplay";
  34. [Header("Debug")]
  35. public bool logDecodeFailure = true;
  36. public float decodeFailureLogIntervalSeconds = 1.0f;
  37. [Header("Lifecycle")]
  38. [Tooltip("是否在 OnEnable 自动开始监听转发端口。若为 false,需要显式调用 StartReceiver()。")]
  39. public bool autoStartOnEnable = false;
  40. [Tooltip("是否让该组件所在 GameObject 常驻(DontDestroyOnLoad)。建议开启,避免退回场景后丢图像。")]
  41. public bool dontDestroyOnLoad = true;
  42. private RomaFrameHeaderJpegReceiver _receiver;
  43. private Texture2D _tex;
  44. private float _lastFailLogTime;
  45. private GameObject _persistentDisplayRoot;
  46. private void Awake()
  47. {
  48. if (Instance != null && Instance != this)
  49. {
  50. // 已有常驻实例在运行:销毁重复的,避免重复占用同一 UDP 端口
  51. Destroy(gameObject);
  52. return;
  53. }
  54. Instance = this;
  55. if (dontDestroyOnLoad)
  56. DontDestroyOnLoad(gameObject);
  57. }
  58. private void OnEnable()
  59. {
  60. SceneManager.sceneLoaded -= OnSceneLoaded;
  61. SceneManager.sceneLoaded += OnSceneLoaded;
  62. if (autoStartOnEnable)
  63. {
  64. StartReceiver();
  65. }
  66. }
  67. private void OnDisable()
  68. {
  69. SceneManager.sceneLoaded -= OnSceneLoaded;
  70. StopReceiver();
  71. }
  72. private void OnDestroy()
  73. {
  74. SceneManager.sceneLoaded -= OnSceneLoaded;
  75. if (Instance == this) Instance = null;
  76. }
  77. public void StartReceiver()
  78. {
  79. if (_receiver != null) return;
  80. EnsureRawImageBound();
  81. try
  82. {
  83. _receiver = new RomaFrameHeaderJpegReceiver(
  84. bindIp: bindIp,
  85. port: forwardPort,
  86. maxQueueSize: maxQueueSize,
  87. maxInFlightFrames: maxInFlightFrames,
  88. frameTimeoutMs: frameTimeoutMs);
  89. _receiver.Start();
  90. }
  91. catch (Exception ex)
  92. {
  93. // 典型原因:场景中出现重复实例,或端口被其他组件占用
  94. Debug.LogError($"[RomaViewer] StartReceiver failed on {bindIp}:{forwardPort}: {ex.Message}");
  95. try { _receiver?.Stop(); } catch { /* ignore */ }
  96. _receiver = null;
  97. return;
  98. }
  99. if (_tex == null)
  100. _tex = new Texture2D(2, 2, TextureFormat.RGB24, false);
  101. }
  102. public void StopReceiver()
  103. {
  104. try { _receiver?.Stop(); } catch { /* ignore */ }
  105. _receiver = null;
  106. }
  107. private void Update()
  108. {
  109. if (_receiver == null) return;
  110. byte[] latest = null;
  111. while (_receiver.TryDequeueJpeg(out var jpeg))
  112. latest = jpeg;
  113. if (latest == null || latest.Length < 4) return;
  114. if (targetRawImage == null)
  115. {
  116. // 场景切换后 UI 被销毁:尽量自动恢复显示
  117. EnsureRawImageBound();
  118. if (targetRawImage == null) return;
  119. }
  120. // 最基本校验:JPEG SOI/EOI
  121. if (latest[0] != 0xFF || latest[1] != 0xD8 || latest[^2] != 0xFF || latest[^1] != 0xD9)
  122. return;
  123. bool ok = false;
  124. try
  125. {
  126. ok = _tex.LoadImage(latest, markNonReadable: false);
  127. }
  128. catch (Exception)
  129. {
  130. ok = false;
  131. }
  132. if (ok)
  133. {
  134. targetRawImage.texture = _tex;
  135. }
  136. else if (logDecodeFailure && (Time.time - _lastFailLogTime) >= decodeFailureLogIntervalSeconds)
  137. {
  138. _lastFailLogTime = Time.time;
  139. Debug.LogWarning($"[RomaViewer] Failed to decode JPEG (size={latest.Length} bytes) from {bindIp}:{forwardPort}");
  140. }
  141. }
  142. private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
  143. {
  144. if (!autoRebindRawImageOnSceneLoaded) return;
  145. if (targetRawImage != null) return;
  146. EnsureRawImageBound();
  147. }
  148. private void EnsureRawImageBound()
  149. {
  150. if (targetRawImage != null) return;
  151. if (autoFindRawImage)
  152. targetRawImage = FindObjectOfType<RawImage>();
  153. if (targetRawImage != null) return;
  154. if (!autoCreatePersistentRawImage) return;
  155. if (_persistentDisplayRoot == null)
  156. {
  157. _persistentDisplayRoot = new GameObject(persistentDisplayRootName);
  158. DontDestroyOnLoad(_persistentDisplayRoot);
  159. var canvas = _persistentDisplayRoot.AddComponent<Canvas>();
  160. canvas.renderMode = RenderMode.ScreenSpaceOverlay;
  161. canvas.sortingOrder = short.MaxValue;
  162. _persistentDisplayRoot.AddComponent<CanvasScaler>();
  163. _persistentDisplayRoot.AddComponent<GraphicRaycaster>();
  164. var imgGo = new GameObject("RawImage");
  165. imgGo.transform.SetParent(_persistentDisplayRoot.transform, false);
  166. var raw = imgGo.AddComponent<RawImage>();
  167. var rt = raw.rectTransform;
  168. rt.anchorMin = Vector2.zero;
  169. rt.anchorMax = Vector2.one;
  170. rt.offsetMin = Vector2.zero;
  171. rt.offsetMax = Vector2.zero;
  172. raw.raycastTarget = false;
  173. targetRawImage = raw;
  174. }
  175. else
  176. {
  177. targetRawImage = _persistentDisplayRoot.GetComponentInChildren<RawImage>(true);
  178. }
  179. }
  180. }
  181. }