- 모든 컨트롤러 에디터를 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>
283 lines
8.8 KiB
C#
283 lines
8.8 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Net;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using UnityEngine;
|
|
|
|
/// <summary>
|
|
/// HTTP 서버 - Streamingle 웹 대시보드 UI를 제공합니다.
|
|
/// Resources/StreamingleDashboard/ 폴더에서 HTML, CSS, JS를 로드합니다.
|
|
/// RetargetingHTTPServer 패턴을 따릅니다.
|
|
/// </summary>
|
|
public class StreamingleDashboardServer
|
|
{
|
|
private HttpListener listener;
|
|
private Thread listenerThread;
|
|
private bool isRunning = false;
|
|
|
|
private int httpPort;
|
|
private int wsPort;
|
|
private int retargetingWsPort;
|
|
|
|
private string cachedCSS = "";
|
|
private string cachedTemplate = "";
|
|
private string cachedJS = "";
|
|
|
|
private List<string> boundAddresses = new List<string>();
|
|
|
|
public bool IsRunning => isRunning;
|
|
public IReadOnlyList<string> BoundAddresses => boundAddresses;
|
|
|
|
public StreamingleDashboardServer(int httpPort, int wsPort, int retargetingWsPort = 0)
|
|
{
|
|
this.httpPort = httpPort;
|
|
this.wsPort = wsPort;
|
|
this.retargetingWsPort = retargetingWsPort;
|
|
}
|
|
|
|
public void Start()
|
|
{
|
|
if (isRunning) return;
|
|
|
|
LoadResources();
|
|
|
|
try
|
|
{
|
|
listener = new HttpListener();
|
|
boundAddresses.Clear();
|
|
|
|
// http://+: 로 모든 인터페이스 바인딩 시도 (관리자 권한 또는 URL ACL 필요)
|
|
bool wildcardBound = false;
|
|
try
|
|
{
|
|
listener.Prefixes.Add($"http://+:{httpPort}/");
|
|
listener.Start();
|
|
wildcardBound = true;
|
|
|
|
boundAddresses.Add($"http://localhost:{httpPort}");
|
|
string localIP = GetLocalIPAddress();
|
|
if (!string.IsNullOrEmpty(localIP))
|
|
{
|
|
boundAddresses.Add($"http://{localIP}:{httpPort}");
|
|
}
|
|
Debug.Log("[StreamingleDashboard] 모든 인터페이스 바인딩 성공 (LAN 접속 가능)");
|
|
}
|
|
catch (Exception)
|
|
{
|
|
// 와일드카드 실패 시 리스너 재생성
|
|
try { listener.Close(); } catch { }
|
|
listener = new HttpListener();
|
|
|
|
// LAN IP 직접 바인딩 시도
|
|
string localIP = GetLocalIPAddress();
|
|
bool lanBound = false;
|
|
|
|
listener.Prefixes.Add($"http://localhost:{httpPort}/");
|
|
boundAddresses.Add($"http://localhost:{httpPort}");
|
|
|
|
listener.Prefixes.Add($"http://127.0.0.1:{httpPort}/");
|
|
boundAddresses.Add($"http://127.0.0.1:{httpPort}");
|
|
|
|
if (!string.IsNullOrEmpty(localIP))
|
|
{
|
|
try
|
|
{
|
|
listener.Prefixes.Add($"http://{localIP}:{httpPort}/");
|
|
boundAddresses.Add($"http://{localIP}:{httpPort}");
|
|
lanBound = true;
|
|
}
|
|
catch (Exception)
|
|
{
|
|
// LAN IP 바인딩도 실패
|
|
}
|
|
}
|
|
|
|
listener.Start();
|
|
|
|
if (lanBound)
|
|
Debug.Log($"[StreamingleDashboard] LAN IP 바인딩 성공 ({localIP})");
|
|
else
|
|
Debug.LogWarning("[StreamingleDashboard] LAN 바인딩 실패. localhost만 접속 가능. 관리자 권한으로 실행하거나 다음 명령어를 실행하세요:\n" +
|
|
$" netsh http add urlacl url=http://+:{httpPort}/ user=Everyone");
|
|
}
|
|
|
|
isRunning = true;
|
|
|
|
listenerThread = new Thread(HandleRequests);
|
|
listenerThread.IsBackground = true;
|
|
listenerThread.Start();
|
|
|
|
Debug.Log("[StreamingleDashboard] HTTP 서버 시작됨");
|
|
foreach (var addr in boundAddresses)
|
|
{
|
|
Debug.Log($"[StreamingleDashboard] 대시보드 접속: {addr}");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Debug.LogError($"[StreamingleDashboard] 서버 시작 실패: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
public void Stop()
|
|
{
|
|
if (!isRunning) return;
|
|
|
|
isRunning = false;
|
|
|
|
try
|
|
{
|
|
listener?.Stop();
|
|
listener?.Close();
|
|
}
|
|
catch (Exception) { }
|
|
|
|
Debug.Log("[StreamingleDashboard] HTTP 서버 중지됨");
|
|
}
|
|
|
|
private void LoadResources()
|
|
{
|
|
TextAsset cssAsset = Resources.Load<TextAsset>("StreamingleDashboard/dashboard_style");
|
|
TextAsset templateAsset = Resources.Load<TextAsset>("StreamingleDashboard/dashboard_template");
|
|
TextAsset jsAsset = Resources.Load<TextAsset>("StreamingleDashboard/dashboard_script");
|
|
|
|
cachedCSS = cssAsset != null ? cssAsset.text : GetFallbackCSS();
|
|
cachedTemplate = templateAsset != null ? templateAsset.text : GetFallbackTemplate();
|
|
cachedJS = jsAsset != null ? jsAsset.text : GetFallbackJS();
|
|
|
|
if (cssAsset == null || templateAsset == null || jsAsset == null)
|
|
{
|
|
Debug.LogWarning("[StreamingleDashboard] 일부 리소스를 로드할 수 없습니다. Fallback 사용 중.");
|
|
}
|
|
}
|
|
|
|
private void HandleRequests()
|
|
{
|
|
while (isRunning)
|
|
{
|
|
try
|
|
{
|
|
HttpListenerContext context = listener.GetContext();
|
|
ProcessRequest(context);
|
|
}
|
|
catch (HttpListenerException)
|
|
{
|
|
// 서버 종료 시 발생
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (isRunning)
|
|
{
|
|
Debug.LogError($"[StreamingleDashboard] 요청 처리 오류: {ex.Message}");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void ProcessRequest(HttpListenerContext context)
|
|
{
|
|
try
|
|
{
|
|
string path = context.Request.Url.AbsolutePath;
|
|
|
|
string responseContent;
|
|
string contentType;
|
|
|
|
if (path == "/" || path == "/index.html")
|
|
{
|
|
responseContent = GenerateHTML();
|
|
contentType = "text/html; charset=utf-8";
|
|
}
|
|
else if (path == "/style.css")
|
|
{
|
|
responseContent = cachedCSS;
|
|
contentType = "text/css; charset=utf-8";
|
|
}
|
|
else if (path == "/script.js")
|
|
{
|
|
responseContent = cachedJS
|
|
.Replace("{{WS_PORT}}", wsPort.ToString())
|
|
.Replace("{{RETARGETING_WS_PORT}}", retargetingWsPort.ToString());
|
|
contentType = "application/javascript; charset=utf-8";
|
|
}
|
|
else
|
|
{
|
|
context.Response.StatusCode = 404;
|
|
responseContent = "Not Found";
|
|
contentType = "text/plain";
|
|
}
|
|
|
|
byte[] buffer = Encoding.UTF8.GetBytes(responseContent);
|
|
context.Response.ContentType = contentType;
|
|
context.Response.ContentLength64 = buffer.Length;
|
|
context.Response.AddHeader("Cache-Control", "no-cache");
|
|
context.Response.OutputStream.Write(buffer, 0, buffer.Length);
|
|
context.Response.Close();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Debug.LogError($"[StreamingleDashboard] 응답 처리 오류: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
private string GenerateHTML()
|
|
{
|
|
string html = cachedTemplate;
|
|
html = html.Replace("{{CSS}}", cachedCSS);
|
|
html = html.Replace("{{JS}}", cachedJS
|
|
.Replace("{{WS_PORT}}", wsPort.ToString())
|
|
.Replace("{{RETARGETING_WS_PORT}}", retargetingWsPort.ToString()));
|
|
return html;
|
|
}
|
|
|
|
private string GetLocalIPAddress()
|
|
{
|
|
try
|
|
{
|
|
var host = Dns.GetHostEntry(Dns.GetHostName());
|
|
foreach (var ip in host.AddressList)
|
|
{
|
|
if (ip.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork)
|
|
{
|
|
return ip.ToString();
|
|
}
|
|
}
|
|
}
|
|
catch (Exception) { }
|
|
return "";
|
|
}
|
|
|
|
private string GetFallbackCSS()
|
|
{
|
|
return @"
|
|
body { font-family: sans-serif; background: #0f172a; color: #f1f5f9; padding: 20px; }
|
|
.error { color: #ef4444; padding: 20px; text-align: center; }
|
|
";
|
|
}
|
|
|
|
private string GetFallbackTemplate()
|
|
{
|
|
return @"
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset=""UTF-8"">
|
|
<title>Streamingle Dashboard</title>
|
|
<style>{{CSS}}</style>
|
|
</head>
|
|
<body>
|
|
<div class=""error"">
|
|
리소스 파일을 찾을 수 없습니다.<br>
|
|
Assets/Resources/StreamingleDashboard/ 폴더에 파일이 있는지 확인해주세요.
|
|
</div>
|
|
</body>
|
|
</html>";
|
|
}
|
|
|
|
private string GetFallbackJS()
|
|
{
|
|
return "console.error('JavaScript resource not found');";
|
|
}
|
|
}
|