Streamingle_URP/Assets/Scripts/Streamdeck/StreamDeckServerManager.cs
user 41270a34f5 Refactor: 전체 에디터 UXML 전환 + 대시보드/런타임 UI + 한글화 + NanumGothic 폰트
- 모든 컨트롤러 에디터를 IMGUI → UI Toolkit(UXML/USS)으로 전환
  (Camera, Item, Event, Avatar, System, StreamDeck, OptiTrack, Facial)
- StreamingleCommon.uss 공통 테마 + 개별 에디터 USS 스타일시트
- SystemController 서브매니저 분리 (OptiTrack, Facial, Recording, Screenshot 등)
- 런타임 컨트롤 패널 (ESC 토글, 좌측 오버레이, 150% 스케일)
- 웹 대시보드 서버 (StreamingleDashboardServer) + 리타게팅 통합
- 설정 도구(StreamingleControllerSetupTool) UXML 재작성 + 원클릭 설정
- SimplePoseTransfer UXML 에디터 추가
- 전체 UXML 한글화 + NanumGothic 폰트 적용
- Streamingle.Debug → Streamingle.Debugging 네임스페이스 변경 (Debug.Log 충돌 해결)
- 불필요 코드 제거 (rawkey.cs, RetargetingHTTPServer, OptitrackSkeletonAnimator 등)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 02:51:43 +09:00

745 lines
24 KiB
C#

using UnityEngine;
using WebSocketSharp.Server;
using System.Collections.Generic;
using Newtonsoft.Json;
using System;
using System.Linq;
public class StreamDeckServerManager : MonoBehaviour
{
[Header("WebSocket 서버 설정")]
public int port = 64211;
[Header("대시보드 설정")]
public int dashboardPort = 64210;
public bool enableDashboard = true;
private WebSocketServer server;
private StreamingleDashboardServer dashboardServer;
private List<StreamDeckService> connectedClients = new List<StreamDeckService>();
public CameraManager cameraManager { get; private set; }
public ItemController itemController { get; private set; }
public EventController eventController { get; private set; }
public AvatarOutfitController avatarOutfitController { get; private set; }
public SystemController systemController { get; private set; }
public static StreamDeckServerManager Instance { get; private set; }
private readonly Queue<System.Action> mainThreadActions = new Queue<System.Action>();
private readonly object lockObject = new object();
public int ConnectedClientCount => connectedClients.Count;
void Awake()
{
Instance = this;
}
void Start()
{
cameraManager = FindObjectOfType<CameraManager>();
if (cameraManager == null)
{
Debug.LogError("[StreamDeckServerManager] CameraManager를 찾을 수 없습니다!");
return;
}
itemController = FindObjectOfType<ItemController>();
if (itemController == null)
Debug.LogWarning("[StreamDeckServerManager] ItemController를 찾을 수 없습니다. 아이템 컨트롤 기능이 비활성화됩니다.");
eventController = FindObjectOfType<EventController>();
if (eventController == null)
Debug.LogWarning("[StreamDeckServerManager] EventController를 찾을 수 없습니다. 이벤트 컨트롤 기능이 비활성화됩니다.");
avatarOutfitController = FindObjectOfType<AvatarOutfitController>();
if (avatarOutfitController == null)
Debug.LogWarning("[StreamDeckServerManager] AvatarOutfitController를 찾을 수 없습니다. 아바타 의상 컨트롤 기능이 비활성화됩니다.");
systemController = FindObjectOfType<SystemController>();
if (systemController == null)
Debug.LogWarning("[StreamDeckServerManager] SystemController를 찾을 수 없습니다. 시스템 컨트롤 기능이 비활성화됩니다.");
StartServer();
StartDashboardServer();
}
void Update()
{
lock (lockObject)
{
while (mainThreadActions.Count > 0)
{
var action = mainThreadActions.Dequeue();
try
{
action?.Invoke();
}
catch (Exception ex)
{
Debug.LogError($"[StreamDeckServerManager] 메인 스레드 작업 실행 오류: {ex.Message}");
}
}
}
}
void OnApplicationQuit()
{
StopServer();
StopDashboardServer();
}
#region Server Lifecycle
private void StartServer()
{
try
{
// 0.0.0.0 으로 바인딩하여 LAN 내 다른 기기에서도 접속 가능
server = new WebSocketServer(port);
server.AddWebSocketService<StreamDeckService>("/");
server.Start();
Debug.Log($"[StreamDeckServerManager] WebSocket 서버 시작됨, 포트: {port} (모든 인터페이스)");
}
catch (Exception e)
{
Debug.LogError($"[StreamDeckServerManager] 서버 시작 실패: {e.Message}");
}
}
private void StopServer()
{
if (server != null)
{
server.Stop();
server = null;
Debug.Log("[StreamDeckServerManager] WebSocket 서버 중지됨");
}
}
private void StartDashboardServer()
{
if (!enableDashboard) return;
int retargetingWsPort = systemController?.retargetingRemote?.retargetingWsPort ?? 0;
dashboardServer = new StreamingleDashboardServer(dashboardPort, port, retargetingWsPort);
dashboardServer.Start();
}
private void StopDashboardServer()
{
dashboardServer?.Stop();
dashboardServer = null;
}
#endregion
#region Client Management
public void OnClientConnected(StreamDeckService service)
{
lock (lockObject)
{
mainThreadActions.Enqueue(() =>
{
connectedClients.Add(service);
Debug.Log($"[StreamDeckServerManager] 클라이언트 연결됨. 총 연결: {connectedClients.Count}");
SendInitialData(service);
});
}
}
public void ProcessMessageOnMainThread(string messageData, StreamDeckService service)
{
lock (lockObject)
{
mainThreadActions.Enqueue(() =>
{
try
{
ProcessMessage(messageData, service);
}
catch (Exception ex)
{
Debug.LogError($"[StreamDeckServerManager] 메시지 처리 오류: {ex.Message}");
}
});
}
}
public void OnClientDisconnected(StreamDeckService service)
{
connectedClients.Remove(service);
Debug.Log($"[StreamDeckServerManager] 클라이언트 연결 해제됨. 총 연결: {connectedClients.Count}");
}
public void BroadcastMessage(string message)
{
foreach (var client in connectedClients.ToArray())
{
try
{
client.SendMessage(message);
}
catch (Exception e)
{
Debug.LogError($"[StreamDeckServerManager] 메시지 전송 실패: {e.Message}");
connectedClients.Remove(client);
}
}
}
#endregion
#region Initial Data
private void SendInitialData(StreamDeckService service)
{
if (cameraManager == null) return;
var initialData = new
{
type = "connection_established",
timestamp = DateTime.UtcNow.ToString("o"),
version = "1.0",
data = new
{
session_id = Guid.NewGuid().ToString(),
message = "유니티 서버에 연결되었습니다!",
camera_data = cameraManager.GetCameraListData(),
current_camera = cameraManager.GetCurrentCameraState(),
item_data = itemController?.GetItemListData(),
current_item = itemController?.GetCurrentItemState(),
event_data = eventController?.GetEventListData(),
current_event = eventController?.GetCurrentEventState(),
avatar_outfit_data = avatarOutfitController?.GetAvatarOutfitListData(),
current_avatar_outfit = avatarOutfitController?.GetCurrentAvatarOutfitState()
}
};
string json = JsonConvert.SerializeObject(initialData);
service.SendMessage(json);
Debug.Log("[StreamDeckServerManager] 초기 데이터 전송됨");
}
#endregion
#region Broadcast Notifications
public void NotifyCameraChanged()
{
if (cameraManager == null) return;
BroadcastJson(new
{
type = "camera_changed",
timestamp = DateTime.UtcNow.ToString("o"),
version = "1.0",
data = new
{
camera_data = cameraManager.GetCameraListData(),
current_camera = cameraManager.GetCurrentCameraState()
}
});
}
public void NotifyItemChanged()
{
if (itemController == null) return;
BroadcastJson(new
{
type = "item_changed",
timestamp = DateTime.UtcNow.ToString("o"),
version = "1.0",
data = new
{
item_data = itemController.GetItemListData(),
current_item = itemController.GetCurrentItemState()
}
});
}
public void NotifyEventChanged()
{
if (eventController == null) return;
BroadcastJson(new
{
type = "event_changed",
timestamp = DateTime.UtcNow.ToString("o"),
version = "1.0",
data = new
{
event_data = eventController.GetEventListData(),
current_event = eventController.GetCurrentEventState()
}
});
}
public void NotifyAvatarOutfitChanged()
{
if (avatarOutfitController == null) return;
BroadcastJson(new
{
type = "avatar_outfit_changed",
timestamp = DateTime.UtcNow.ToString("o"),
version = "1.0",
data = new
{
avatar_outfit_data = avatarOutfitController.GetAvatarOutfitListData(),
current_avatar_outfit = avatarOutfitController.GetCurrentAvatarOutfitState()
}
});
}
private void BroadcastJson(object data)
{
string json = JsonConvert.SerializeObject(data);
BroadcastMessage(json);
}
#endregion
#region Message Processing
private void ProcessMessage(string messageData, StreamDeckService service)
{
var message = JsonConvert.DeserializeObject<Dictionary<string, object>>(messageData);
string messageType = message.ContainsKey("type") ? message["type"].ToString() : null;
switch (messageType)
{
// 카메라
case "switch_camera":
HandleWithIndex(message, "camera_index", cameraManager, "cameraManager",
cameraManager?.cameraPresets?.Count ?? 0,
idx => { cameraManager.Set(idx); });
break;
case "get_camera_list":
HandleGetCameraList(service);
break;
case "toggle_drone_mode":
HandleToggleDroneMode(service);
break;
case "get_drone_state":
HandleGetDroneState(service);
break;
// 아이템
case "toggle_item":
HandleWithIndex(message, "item_index", itemController, "itemController",
itemController?.itemGroups?.Count ?? 0,
idx => { itemController.ToggleGroup(idx); NotifyItemChanged(); });
break;
case "set_item":
HandleWithIndex(message, "item_index", itemController, "itemController",
itemController?.itemGroups?.Count ?? 0,
idx => { itemController.Set(idx); NotifyItemChanged(); });
break;
case "get_item_list":
HandleGetItemList(service);
break;
// 이벤트
case "execute_event":
HandleWithIndex(message, "event_index", eventController, "eventController",
eventController?.eventGroups?.Count ?? 0,
idx => { eventController.ExecuteEvent(idx); });
break;
case "set_event":
HandleWithIndex(message, "event_index", eventController, "eventController",
eventController?.eventGroups?.Count ?? 0,
idx => { eventController.Set(idx); });
break;
case "get_event_list":
HandleGetEventList(service);
break;
// 아바타 의상
case "set_avatar_outfit":
HandleSetAvatarOutfit(message);
break;
case "get_avatar_outfit_list":
HandleGetAvatarOutfitList(service);
break;
// 시스템 상태 (대시보드용 신규)
case "get_system_status":
HandleGetSystemStatus(service);
break;
case "get_full_state":
SendInitialData(service);
break;
case "ping":
SendJson(service, new { type = "pong", timestamp = DateTime.UtcNow.ToString("o") });
break;
// SystemController 명령어들
case "toggle_optitrack_markers":
case "show_optitrack_markers":
case "hide_optitrack_markers":
case "reconnect_optitrack":
case "spawn_optitrack_client":
case "reconnect_facial_motion":
case "refresh_facial_motion_clients":
case "start_motion_recording":
case "stop_motion_recording":
case "toggle_motion_recording":
case "refresh_motion_recorders":
case "capture_screenshot":
case "capture_alpha_screenshot":
case "open_screenshot_folder":
case "refresh_avatar_head_colliders":
case "refresh_magica_cloth":
case "reset_magica_cloth":
case "reset_magica_cloth_keep_pose":
case "start_retargeting_remote":
case "stop_retargeting_remote":
case "toggle_retargeting_remote":
case "refresh_retargeting_characters":
HandleSystemCommand(message);
break;
case "test":
SendJson(service, new
{
type = "echo",
timestamp = DateTime.UtcNow.ToString("o"),
data = new { received_message = messageData }
});
break;
default:
Debug.Log($"[StreamDeckServerManager] 알 수 없는 메시지 타입: {messageType}");
break;
}
}
#endregion
#region Index Extraction Helper
/// <summary>
/// 메시지에서 인덱스를 추출합니다. JObject와 Dictionary 모두 지원합니다.
/// </summary>
private int? ExtractIndex(Dictionary<string, object> message, string key)
{
if (!message.ContainsKey("data")) return null;
var dataObject = message["data"];
string rawValue = null;
if (dataObject is Newtonsoft.Json.Linq.JObject jObject)
{
if (jObject.ContainsKey(key))
rawValue = jObject[key]?.ToString();
}
else if (dataObject is Dictionary<string, object> data)
{
if (data.ContainsKey(key))
rawValue = data[key]?.ToString();
}
if (rawValue != null && int.TryParse(rawValue, out int index))
return index;
return null;
}
/// <summary>
/// 인덱스 기반 핸들러를 간결하게 처리합니다.
/// </summary>
private void HandleWithIndex(Dictionary<string, object> message, string indexKey,
object controller, string controllerName, int maxCount, Action<int> action)
{
if (controller == null)
{
Debug.LogError($"[StreamDeckServerManager] {controllerName}가 null입니다!");
return;
}
int? index = ExtractIndex(message, indexKey);
if (index == null)
{
Debug.LogError($"[StreamDeckServerManager] '{indexKey}' 파싱 실패");
return;
}
if (index.Value < 0 || index.Value >= maxCount)
{
Debug.LogError($"[StreamDeckServerManager] 잘못된 인덱스: {index.Value}, 유효 범위: 0-{maxCount - 1}");
return;
}
action(index.Value);
}
#endregion
#region Response Helpers
private void SendJson(StreamDeckService service, object data)
{
string json = JsonConvert.SerializeObject(data);
service.SendMessage(json);
}
#endregion
#region Camera Handlers
private void HandleGetCameraList(StreamDeckService service)
{
if (cameraManager == null) return;
SendJson(service, new
{
type = "camera_list_response",
timestamp = DateTime.UtcNow.ToString("o"),
version = "1.0",
data = new
{
camera_data = cameraManager.GetCameraListData(),
current_camera = cameraManager.GetCurrentCameraState()
}
});
}
private void HandleToggleDroneMode(StreamDeckService service)
{
if (cameraManager == null) return;
cameraManager.ToggleDroneMode();
HandleGetDroneState(service);
}
private void HandleGetDroneState(StreamDeckService service)
{
if (cameraManager == null) return;
SendJson(service, new
{
type = "drone_state_response",
timestamp = DateTime.UtcNow.ToString("o"),
version = "1.0",
data = new
{
is_drone_mode = cameraManager.IsDroneModeActive,
current_camera = cameraManager.GetCurrentCameraState()
}
});
}
#endregion
#region Item/Event/Avatar Handlers
private void HandleGetItemList(StreamDeckService service)
{
if (itemController == null) return;
SendJson(service, new
{
type = "item_list_response",
timestamp = DateTime.UtcNow.ToString("o"),
version = "1.0",
data = new
{
item_data = itemController.GetItemListData(),
current_item = itemController.GetCurrentItemState()
}
});
}
private void HandleGetEventList(StreamDeckService service)
{
if (eventController == null) return;
SendJson(service, new
{
type = "event_list_response",
timestamp = DateTime.UtcNow.ToString("o"),
version = "1.0",
data = new
{
event_data = eventController.GetEventListData(),
current_event = eventController.GetCurrentEventState()
}
});
}
private void HandleSetAvatarOutfit(Dictionary<string, object> message)
{
if (avatarOutfitController == null)
{
Debug.LogError("[StreamDeckServerManager] avatarOutfitController가 null입니다!");
return;
}
int? avatarIndex = ExtractIndex(message, "avatar_index");
int? outfitIndex = ExtractIndex(message, "outfit_index");
if (avatarIndex == null || outfitIndex == null)
{
Debug.LogError("[StreamDeckServerManager] avatar_index 또는 outfit_index 파싱 실패");
return;
}
if (avatarIndex.Value < 0 || avatarIndex.Value >= (avatarOutfitController.avatars?.Count ?? 0))
{
Debug.LogError($"[StreamDeckServerManager] 잘못된 아바타 인덱스: {avatarIndex.Value}");
return;
}
Debug.Log($"[StreamDeckServerManager] 아바타 {avatarIndex.Value}번 의상을 {outfitIndex.Value}번으로 설정");
avatarOutfitController.SetAvatarOutfit(avatarIndex.Value, outfitIndex.Value);
}
private void HandleGetAvatarOutfitList(StreamDeckService service)
{
if (avatarOutfitController == null)
{
Debug.LogError("[StreamDeckServerManager] avatarOutfitController가 null입니다!");
return;
}
SendJson(service, new
{
type = "avatar_outfit_list_response",
timestamp = DateTime.UtcNow.ToString("o"),
version = "1.0",
data = new
{
avatar_outfit_data = avatarOutfitController.GetAvatarOutfitListData(),
current_avatar_outfit = avatarOutfitController.GetCurrentAvatarOutfitState()
}
});
}
#endregion
#region System Command Handler
private void HandleSystemCommand(Dictionary<string, object> message)
{
string messageType = message.ContainsKey("type") ? message["type"].ToString() : null;
if (systemController == null)
{
Debug.LogError("[StreamDeckServerManager] SystemController가 null입니다!");
return;
}
try
{
Dictionary<string, object> parameters = new Dictionary<string, object>();
if (message.ContainsKey("data"))
{
var dataObject = message["data"];
if (dataObject is Newtonsoft.Json.Linq.JObject jObject)
{
foreach (var prop in jObject.Properties())
{
parameters[prop.Name] = prop.Value.ToString();
}
}
}
systemController.ExecuteCommand(messageType, parameters);
}
catch (Exception ex)
{
Debug.LogError($"[StreamDeckServerManager] 시스템 명령어 실행 실패: {ex.Message}");
}
}
#endregion
#region System Status (Dashboard)
private void HandleGetSystemStatus(StreamDeckService service)
{
var statusData = new Dictionary<string, object>();
// OptiTrack
if (systemController != null)
{
statusData["optitrack"] = new
{
connected = systemController.optiTrack.IsOptitrackConnected(),
status = systemController.optiTrack.GetOptitrackConnectionStatus()
};
statusData["facial_motion"] = new
{
client_count = systemController.facialMotion.facialMotionClients?.Count ?? 0
};
statusData["recording"] = new
{
is_recording = systemController.IsRecording()
};
statusData["retargeting_remote"] = new
{
is_running = systemController.IsRetargetingRemoteRunning(),
url = systemController.GetRetargetingRemoteUrl()
};
}
statusData["websocket"] = new
{
connected_clients = connectedClients.Count,
port = port
};
statusData["dashboard"] = new
{
enabled = enableDashboard,
port = dashboardPort,
urls = dashboardServer?.BoundAddresses?.ToArray() ?? new string[0]
};
SendJson(service, new
{
type = "system_status_response",
timestamp = DateTime.UtcNow.ToString("o"),
version = "1.0",
data = statusData
});
}
#endregion
}
public class StreamDeckService : WebSocketBehavior
{
private StreamDeckServerManager serverManager;
protected override void OnOpen()
{
serverManager = StreamDeckServerManager.Instance;
if (serverManager != null)
{
serverManager.OnClientConnected(this);
}
Debug.Log("[StreamDeckService] WebSocket 연결 열림");
}
protected override void OnMessage(WebSocketSharp.MessageEventArgs e)
{
if (serverManager != null)
{
serverManager.ProcessMessageOnMainThread(e.Data, this);
}
}
public void SendMessage(string message)
{
try
{
Send(message);
}
catch (Exception ex)
{
Debug.LogError($"[StreamDeckService] 메시지 전송 실패: {ex.Message}");
}
}
protected override void OnClose(WebSocketSharp.CloseEventArgs e)
{
if (serverManager != null)
{
serverManager.OnClientDisconnected(this);
}
Debug.Log($"[StreamDeckService] WebSocket 연결 닫힘: {e.Reason}");
}
protected override void OnError(WebSocketSharp.ErrorEventArgs e)
{
Debug.LogError($"[StreamDeckService] WebSocket 오류: {e.Message}");
}
}