Fix : 유니티에 의한 가변 프레임 버그 해결
This commit is contained in:
parent
71b9521372
commit
b4044a90f5
@ -37,7 +37,7 @@ public class OptitrackSkeletonAnimator_Mingle : MonoBehaviour
|
|||||||
|
|
||||||
[Header("본 1€ 필터 (속도 적응형 저역통과)")]
|
[Header("본 1€ 필터 (속도 적응형 저역통과)")]
|
||||||
[HideInInspector]
|
[HideInInspector]
|
||||||
public FilterStrength filterStrength = FilterStrength.Medium;
|
public FilterStrength filterStrength = FilterStrength.Off;
|
||||||
|
|
||||||
[Header("어깨 증폭")]
|
[Header("어깨 증폭")]
|
||||||
[Tooltip("어깨 회전을 증폭합니다. 1 = 원본, 2 = 2배. 하위 체인(상완)은 자동 역보정되어 손 위치가 유지됩니다.")]
|
[Tooltip("어깨 회전을 증폭합니다. 1 = 원본, 2 = 2배. 하위 체인(상완)은 자동 역보정되어 손 위치가 유지됩니다.")]
|
||||||
@ -60,6 +60,14 @@ public class OptitrackSkeletonAnimator_Mingle : MonoBehaviour
|
|||||||
|
|
||||||
[HideInInspector] public bool enableBoneFilter = true;
|
[HideInInspector] public bool enableBoneFilter = true;
|
||||||
|
|
||||||
|
[Header("프레임 보간")]
|
||||||
|
[Tooltip("OptiTrack 프레임 사이를 보간하여 Unity 가변 프레임에서도 부드러운 모션을 생성합니다. 약 1프레임(~8ms @120fps) 지연이 추가됩니다.")]
|
||||||
|
public bool enableInterpolation = true;
|
||||||
|
|
||||||
|
[Tooltip("보간 지연 시간(초). 0이면 자동(OptiTrack 프레임 간격 사용). 높을수록 부드럽지만 지연 증가.")]
|
||||||
|
[Range(0f, 0.05f)]
|
||||||
|
public float interpolationDelay = 0f;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 런타임에서 필터 강도를 변경합니다. StreamDeck/핫키 등에서 호출.
|
/// 런타임에서 필터 강도를 변경합니다. StreamDeck/핫키 등에서 호출.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -148,6 +156,16 @@ public class OptitrackSkeletonAnimator_Mingle : MonoBehaviour
|
|||||||
}
|
}
|
||||||
private Dictionary<Int32, BoneFilterState> m_filterStates = new Dictionary<Int32, BoneFilterState>();
|
private Dictionary<Int32, BoneFilterState> m_filterStates = new Dictionary<Int32, BoneFilterState>();
|
||||||
|
|
||||||
|
// 프레임 보간용 이중 버퍼 (prev/curr OptiTrack 프레임)
|
||||||
|
private Dictionary<Int32, Vector3> m_interpPrevPos = new Dictionary<Int32, Vector3>();
|
||||||
|
private Dictionary<Int32, Quaternion> m_interpPrevOri = new Dictionary<Int32, Quaternion>();
|
||||||
|
private Dictionary<Int32, Vector3> m_interpCurrPos = new Dictionary<Int32, Vector3>();
|
||||||
|
private Dictionary<Int32, Quaternion> m_interpCurrOri = new Dictionary<Int32, Quaternion>();
|
||||||
|
private OptitrackHiResTimer.Timestamp m_interpPrevTs;
|
||||||
|
private OptitrackHiResTimer.Timestamp m_interpCurrTs;
|
||||||
|
private bool m_interpHasCurr = false;
|
||||||
|
private bool m_interpReady = false;
|
||||||
|
|
||||||
// OptiTrack 본 이름 → FBX 노드 접미사 기본 매핑
|
// OptiTrack 본 이름 → FBX 노드 접미사 기본 매핑
|
||||||
public static readonly Dictionary<string, string> DefaultOptiToFbxSuffix = new Dictionary<string, string>
|
public static readonly Dictionary<string, string> DefaultOptiToFbxSuffix = new Dictionary<string, string>
|
||||||
{
|
{
|
||||||
@ -319,11 +337,12 @@ public class OptitrackSkeletonAnimator_Mingle : MonoBehaviour
|
|||||||
// 필터 활성화 상태 동기화 (프리셋 값은 SetFilterStrength()에서만 적용)
|
// 필터 활성화 상태 동기화 (프리셋 값은 SetFilterStrength()에서만 적용)
|
||||||
enableBoneFilter = filterStrength != FilterStrength.Off;
|
enableBoneFilter = filterStrength != FilterStrength.Off;
|
||||||
|
|
||||||
// MirrorMode 변경 감지 → 필터 상태 리셋 (불연속 튐 방지)
|
// MirrorMode 변경 감지 → 필터/보간 상태 리셋 (불연속 튐 방지)
|
||||||
bool currentMirrorMode = StreamingClient != null && StreamingClient.MirrorMode;
|
bool currentMirrorMode = StreamingClient != null && StreamingClient.MirrorMode;
|
||||||
if (currentMirrorMode != m_lastMirrorMode)
|
if (currentMirrorMode != m_lastMirrorMode)
|
||||||
{
|
{
|
||||||
m_filterStates.Clear();
|
m_filterStates.Clear();
|
||||||
|
ClearInterpolationBuffers();
|
||||||
m_lastMirrorMode = currentMirrorMode;
|
m_lastMirrorMode = currentMirrorMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -335,6 +354,7 @@ public class OptitrackSkeletonAnimator_Mingle : MonoBehaviour
|
|||||||
previousSkeletonName = SkeletonAssetName;
|
previousSkeletonName = SkeletonAssetName;
|
||||||
if (m_skeletonDef != null)
|
if (m_skeletonDef != null)
|
||||||
RebuildBoneIdMapping();
|
RebuildBoneIdMapping();
|
||||||
|
ClearInterpolationBuffers();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -355,6 +375,10 @@ public class OptitrackSkeletonAnimator_Mingle : MonoBehaviour
|
|||||||
m_lastFrameTimestamp = frameTs;
|
m_lastFrameTimestamp = frameTs;
|
||||||
m_hasLastFrameTimestamp = true;
|
m_hasLastFrameTimestamp = true;
|
||||||
|
|
||||||
|
// ── 프레임 보간: 두 OptiTrack 프레임 사이를 시간 기반으로 Lerp/Slerp ──
|
||||||
|
if (enableInterpolation)
|
||||||
|
InterpolateSnapshots(frameTs);
|
||||||
|
|
||||||
// ── Pass 1: Raw 데이터 적용 → IK 포인트 월드 위치 캡처 ──────────────────
|
// ── Pass 1: Raw 데이터 적용 → IK 포인트 월드 위치 캡처 ──────────────────
|
||||||
// 필터가 활성화되어 있을 때만 two-pass, 비활성이면 single-pass
|
// 필터가 활성화되어 있을 때만 two-pass, 비활성이면 single-pass
|
||||||
if (enableBoneFilter)
|
if (enableBoneFilter)
|
||||||
@ -625,6 +649,81 @@ public class OptitrackSkeletonAnimator_Mingle : MonoBehaviour
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── 프레임 보간 (Frame Interpolation) ────────────────────────────────────
|
||||||
|
// OptiTrack(고정 120fps)과 Unity(가변 프레임) 사이의 타이밍 불일치로 인한 떨림을 제거.
|
||||||
|
// 이전/현재 두 프레임을 버퍼링하고 하드웨어 타임스탬프 기반으로 Lerp/Slerp.
|
||||||
|
// 약 1 OptiTrack 프레임(~8ms @120fps)의 지연이 추가되지만 모션이 매끄러워짐.
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 보간 버퍼를 초기화합니다. MirrorMode 전환, 스켈레톤 변경 등 불연속 시점에 호출.
|
||||||
|
/// </summary>
|
||||||
|
private void ClearInterpolationBuffers()
|
||||||
|
{
|
||||||
|
m_interpPrevPos.Clear();
|
||||||
|
m_interpPrevOri.Clear();
|
||||||
|
m_interpCurrPos.Clear();
|
||||||
|
m_interpCurrOri.Clear();
|
||||||
|
m_interpHasCurr = false;
|
||||||
|
m_interpReady = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 두 OptiTrack 프레임 사이를 시간 기반으로 보간합니다.
|
||||||
|
/// m_snapshotPositions/Orientations를 보간된 결과로 덮어씁니다.
|
||||||
|
/// </summary>
|
||||||
|
private void InterpolateSnapshots(OptitrackHiResTimer.Timestamp frameTs)
|
||||||
|
{
|
||||||
|
// 새 프레임 감지 (타임스탬프가 변경되었으면 새 OptiTrack 프레임이 도착한 것)
|
||||||
|
if (frameTs.m_ticks != m_interpCurrTs.m_ticks)
|
||||||
|
{
|
||||||
|
// curr → prev 스왑 (딕셔너리 참조 교환으로 GC 방지)
|
||||||
|
(m_interpPrevPos, m_interpCurrPos) = (m_interpCurrPos, m_interpPrevPos);
|
||||||
|
(m_interpPrevOri, m_interpCurrOri) = (m_interpCurrOri, m_interpPrevOri);
|
||||||
|
m_interpPrevTs = m_interpCurrTs;
|
||||||
|
|
||||||
|
// 새 스냅샷 → curr 복사
|
||||||
|
m_interpCurrPos.Clear();
|
||||||
|
m_interpCurrOri.Clear();
|
||||||
|
foreach (var kvp in m_snapshotPositions)
|
||||||
|
m_interpCurrPos[kvp.Key] = kvp.Value;
|
||||||
|
foreach (var kvp in m_snapshotOrientations)
|
||||||
|
m_interpCurrOri[kvp.Key] = kvp.Value;
|
||||||
|
m_interpCurrTs = frameTs;
|
||||||
|
|
||||||
|
if (!m_interpReady && m_interpHasCurr)
|
||||||
|
m_interpReady = true;
|
||||||
|
m_interpHasCurr = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!m_interpReady) return;
|
||||||
|
|
||||||
|
// 프레임 간격 계산
|
||||||
|
float frameDuration = m_interpCurrTs.SecondsSince(m_interpPrevTs);
|
||||||
|
if (frameDuration < 0.001f || frameDuration > 0.1f) return; // 비정상 간격 무시
|
||||||
|
|
||||||
|
// 보간 계수: target_time = now - delay → 항상 prev~curr 사이에서 보간
|
||||||
|
float delay = interpolationDelay > 0f ? interpolationDelay : m_natNetDt;
|
||||||
|
float timeSincePrev = OptitrackHiResTimer.Now().SecondsSince(m_interpPrevTs);
|
||||||
|
float t = Mathf.Clamp01((timeSincePrev - delay) / frameDuration);
|
||||||
|
|
||||||
|
// m_snapshotPositions/Orientations를 보간 결과로 덮어쓰기
|
||||||
|
m_snapshotPositions.Clear();
|
||||||
|
m_snapshotOrientations.Clear();
|
||||||
|
|
||||||
|
foreach (var kvp in m_interpCurrPos)
|
||||||
|
{
|
||||||
|
m_snapshotPositions[kvp.Key] = m_interpPrevPos.TryGetValue(kvp.Key, out Vector3 prevP)
|
||||||
|
? Vector3.Lerp(prevP, kvp.Value, t)
|
||||||
|
: kvp.Value;
|
||||||
|
}
|
||||||
|
foreach (var kvp in m_interpCurrOri)
|
||||||
|
{
|
||||||
|
m_snapshotOrientations[kvp.Key] = m_interpPrevOri.TryGetValue(kvp.Key, out Quaternion prevO)
|
||||||
|
? Quaternion.Slerp(prevO, kvp.Value, t)
|
||||||
|
: kvp.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── 1€ Filter (One Euro Filter) ──────────────────────────────────────────
|
// ── 1€ Filter (One Euro Filter) ──────────────────────────────────────────
|
||||||
// 참고: Géry Casiez et al., "1€ Filter: A Simple Speed-based Low-pass Filter", CHI 2012
|
// 참고: Géry Casiez et al., "1€ Filter: A Simple Speed-based Low-pass Filter", CHI 2012
|
||||||
// 속도가 빠를수록 cutoff 상승 → 지연 감소, 속도가 느릴수록 cutoff = minCutoff → 노이즈 제거
|
// 속도가 빠를수록 cutoff 상승 → 지연 감소, 속도가 느릴수록 cutoff = minCutoff → 노이즈 제거
|
||||||
|
|||||||
@ -51,6 +51,7 @@ public class StreamDeckServerManager : MonoBehaviour
|
|||||||
private readonly List<Camera> previewCameraPool = new List<Camera>();
|
private readonly List<Camera> previewCameraPool = new List<Camera>();
|
||||||
private RenderTexture previewRT;
|
private RenderTexture previewRT;
|
||||||
private Texture2D previewReadbackTexture;
|
private Texture2D previewReadbackTexture;
|
||||||
|
private int previewTextureVersion = 0;
|
||||||
private int currentPreviewIndex = 0;
|
private int currentPreviewIndex = 0;
|
||||||
private int previewFrameCounter = 0;
|
private int previewFrameCounter = 0;
|
||||||
private readonly Dictionary<Camera, int> previewPendingCaptures = new Dictionary<Camera, int>();
|
private readonly Dictionary<Camera, int> previewPendingCaptures = new Dictionary<Camera, int>();
|
||||||
@ -66,26 +67,26 @@ public class StreamDeckServerManager : MonoBehaviour
|
|||||||
|
|
||||||
void Start()
|
void Start()
|
||||||
{
|
{
|
||||||
cameraManager = FindObjectOfType<CameraManager>();
|
cameraManager = FindAnyObjectByType<CameraManager>();
|
||||||
if (cameraManager == null)
|
if (cameraManager == null)
|
||||||
{
|
{
|
||||||
Debug.LogError("[StreamDeckServerManager] CameraManager를 찾을 수 없습니다!");
|
Debug.LogError("[StreamDeckServerManager] CameraManager를 찾을 수 없습니다!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
itemController = FindObjectOfType<ItemController>();
|
itemController = FindAnyObjectByType<ItemController>();
|
||||||
if (itemController == null)
|
if (itemController == null)
|
||||||
Debug.LogWarning("[StreamDeckServerManager] ItemController를 찾을 수 없습니다. 아이템 컨트롤 기능이 비활성화됩니다.");
|
Debug.LogWarning("[StreamDeckServerManager] ItemController를 찾을 수 없습니다. 아이템 컨트롤 기능이 비활성화됩니다.");
|
||||||
|
|
||||||
eventController = FindObjectOfType<EventController>();
|
eventController = FindAnyObjectByType<EventController>();
|
||||||
if (eventController == null)
|
if (eventController == null)
|
||||||
Debug.LogWarning("[StreamDeckServerManager] EventController를 찾을 수 없습니다. 이벤트 컨트롤 기능이 비활성화됩니다.");
|
Debug.LogWarning("[StreamDeckServerManager] EventController를 찾을 수 없습니다. 이벤트 컨트롤 기능이 비활성화됩니다.");
|
||||||
|
|
||||||
avatarOutfitController = FindObjectOfType<AvatarOutfitController>();
|
avatarOutfitController = FindAnyObjectByType<AvatarOutfitController>();
|
||||||
if (avatarOutfitController == null)
|
if (avatarOutfitController == null)
|
||||||
Debug.LogWarning("[StreamDeckServerManager] AvatarOutfitController를 찾을 수 없습니다. 아바타 의상 컨트롤 기능이 비활성화됩니다.");
|
Debug.LogWarning("[StreamDeckServerManager] AvatarOutfitController를 찾을 수 없습니다. 아바타 의상 컨트롤 기능이 비활성화됩니다.");
|
||||||
|
|
||||||
systemController = FindObjectOfType<SystemController>();
|
systemController = FindAnyObjectByType<SystemController>();
|
||||||
if (systemController == null)
|
if (systemController == null)
|
||||||
Debug.LogWarning("[StreamDeckServerManager] SystemController를 찾을 수 없습니다. 시스템 컨트롤 기능이 비활성화됩니다.");
|
Debug.LogWarning("[StreamDeckServerManager] SystemController를 찾을 수 없습니다. 시스템 컨트롤 기능이 비활성화됩니다.");
|
||||||
|
|
||||||
@ -97,11 +98,21 @@ public class StreamDeckServerManager : MonoBehaviour
|
|||||||
|
|
||||||
void Update()
|
void Update()
|
||||||
{
|
{
|
||||||
|
// 큐를 락 안에서 드레인한 뒤, 락 밖에서 실행 — WebSocket 스레드 블로킹 최소화
|
||||||
|
Action[] pendingActions = null;
|
||||||
lock (lockObject)
|
lock (lockObject)
|
||||||
{
|
{
|
||||||
while (mainThreadActions.Count > 0)
|
if (mainThreadActions.Count > 0)
|
||||||
|
{
|
||||||
|
pendingActions = mainThreadActions.ToArray();
|
||||||
|
mainThreadActions.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pendingActions != null)
|
||||||
|
{
|
||||||
|
foreach (var action in pendingActions)
|
||||||
{
|
{
|
||||||
var action = mainThreadActions.Dequeue();
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
action?.Invoke();
|
action?.Invoke();
|
||||||
@ -129,10 +140,6 @@ public class StreamDeckServerManager : MonoBehaviour
|
|||||||
void OnDestroy()
|
void OnDestroy()
|
||||||
{
|
{
|
||||||
CleanupPreview();
|
CleanupPreview();
|
||||||
}
|
|
||||||
|
|
||||||
void OnApplicationQuit()
|
|
||||||
{
|
|
||||||
StopServer();
|
StopServer();
|
||||||
StopDashboardServer();
|
StopDashboardServer();
|
||||||
}
|
}
|
||||||
@ -145,7 +152,7 @@ public class StreamDeckServerManager : MonoBehaviour
|
|||||||
// 0.0.0.0 으로 바인딩하여 LAN 내 다른 기기에서도 접속 가능
|
// 0.0.0.0 으로 바인딩하여 LAN 내 다른 기기에서도 접속 가능
|
||||||
server = new WebSocketServer(port);
|
server = new WebSocketServer(port);
|
||||||
server.KeepClean = true; // 비활성 세션 자동 정리
|
server.KeepClean = true; // 비활성 세션 자동 정리
|
||||||
server.WaitTime = TimeSpan.FromSeconds(3); // ping-pong 응답 대기 시간
|
server.WaitTime = TimeSpan.FromSeconds(10); // ping-pong 응답 대기 시간 (모바일 WiFi 대응)
|
||||||
server.AddWebSocketService<StreamDeckService>("/");
|
server.AddWebSocketService<StreamDeckService>("/");
|
||||||
server.Start();
|
server.Start();
|
||||||
Debug.Log($"[StreamDeckServerManager] WebSocket 서버 시작됨, 포트: {port} (모든 인터페이스)");
|
Debug.Log($"[StreamDeckServerManager] WebSocket 서버 시작됨, 포트: {port} (모든 인터페이스)");
|
||||||
@ -222,13 +229,20 @@ public class StreamDeckServerManager : MonoBehaviour
|
|||||||
|
|
||||||
public void OnClientDisconnected(StreamDeckService service)
|
public void OnClientDisconnected(StreamDeckService service)
|
||||||
{
|
{
|
||||||
connectedClients.Remove(service);
|
lock (lockObject)
|
||||||
PreviewUnsubscribe(service);
|
{
|
||||||
Debug.Log($"[StreamDeckServerManager] 클라이언트 연결 해제됨. 총 연결: {connectedClients.Count}");
|
mainThreadActions.Enqueue(() =>
|
||||||
|
{
|
||||||
|
connectedClients.Remove(service);
|
||||||
|
PreviewUnsubscribe(service);
|
||||||
|
Debug.Log($"[StreamDeckServerManager] 클라이언트 연결 해제됨. 총 연결: {connectedClients.Count}");
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void BroadcastMessage(string message)
|
public void BroadcastMessage(string message)
|
||||||
{
|
{
|
||||||
|
var deadClients = new List<StreamDeckService>();
|
||||||
foreach (var client in connectedClients.ToArray())
|
foreach (var client in connectedClients.ToArray())
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@ -238,9 +252,13 @@ public class StreamDeckServerManager : MonoBehaviour
|
|||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
Debug.LogError($"[StreamDeckServerManager] 메시지 전송 실패: {e.Message}");
|
Debug.LogError($"[StreamDeckServerManager] 메시지 전송 실패: {e.Message}");
|
||||||
connectedClients.Remove(client);
|
deadClients.Add(client);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
foreach (var dead in deadClients)
|
||||||
|
{
|
||||||
|
connectedClients.Remove(dead);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
@ -775,6 +793,8 @@ public class StreamDeckServerManager : MonoBehaviour
|
|||||||
|
|
||||||
private void CreatePreviewRenderTextures()
|
private void CreatePreviewRenderTextures()
|
||||||
{
|
{
|
||||||
|
previewTextureVersion++;
|
||||||
|
|
||||||
if (previewRT != null)
|
if (previewRT != null)
|
||||||
{
|
{
|
||||||
previewRT.Release();
|
previewRT.Release();
|
||||||
@ -815,8 +835,8 @@ public class StreamDeckServerManager : MonoBehaviour
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
previewFrameCounter++;
|
previewFrameCounter = (previewFrameCounter + 1) % renderInterval;
|
||||||
if (previewFrameCounter % renderInterval != 0)
|
if (previewFrameCounter != 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var presets = cameraManager.cameraPresets;
|
var presets = cameraManager.cameraPresets;
|
||||||
@ -882,10 +902,13 @@ public class StreamDeckServerManager : MonoBehaviour
|
|||||||
|
|
||||||
string presetName = presets[presetIndex].presetName;
|
string presetName = presets[presetIndex].presetName;
|
||||||
int capturedPresetIndex = presetIndex;
|
int capturedPresetIndex = presetIndex;
|
||||||
|
int capturedTextureVersion = previewTextureVersion;
|
||||||
|
|
||||||
AsyncGPUReadback.Request(camera.targetTexture, 0, TextureFormat.RGB24, (request) =>
|
AsyncGPUReadback.Request(camera.targetTexture, 0, TextureFormat.RGB24, (request) =>
|
||||||
{
|
{
|
||||||
if (request.hasError || this == null) return;
|
if (request.hasError || this == null) return;
|
||||||
|
// 텍스처가 재생성되었으면 이 콜백의 데이터는 무효
|
||||||
|
if (capturedTextureVersion != previewTextureVersion) return;
|
||||||
|
|
||||||
NativeArray<byte> data = request.GetData<byte>();
|
NativeArray<byte> data = request.GetData<byte>();
|
||||||
previewReadbackTexture.LoadRawTextureData(data);
|
previewReadbackTexture.LoadRawTextureData(data);
|
||||||
@ -1051,19 +1074,24 @@ public class StreamDeckServerManager : MonoBehaviour
|
|||||||
{
|
{
|
||||||
var statusData = new Dictionary<string, object>();
|
var statusData = new Dictionary<string, object>();
|
||||||
|
|
||||||
// OptiTrack
|
|
||||||
if (systemController != null)
|
if (systemController != null)
|
||||||
{
|
{
|
||||||
statusData["optitrack"] = new
|
if (systemController.optiTrack != null)
|
||||||
{
|
{
|
||||||
connected = systemController.optiTrack.IsOptitrackConnected(),
|
statusData["optitrack"] = new
|
||||||
status = systemController.optiTrack.GetOptitrackConnectionStatus()
|
{
|
||||||
};
|
connected = systemController.optiTrack.IsOptitrackConnected(),
|
||||||
|
status = systemController.optiTrack.GetOptitrackConnectionStatus()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
statusData["facial_motion"] = new
|
if (systemController.facialMotion != null)
|
||||||
{
|
{
|
||||||
client_count = systemController.facialMotion.facialMotionClients?.Count ?? 0
|
statusData["facial_motion"] = new
|
||||||
};
|
{
|
||||||
|
client_count = systemController.facialMotion.facialMotionClients?.Count ?? 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
statusData["recording"] = new
|
statusData["recording"] = new
|
||||||
{
|
{
|
||||||
|
|||||||
@ -159,7 +159,19 @@ public class StreamingleDashboardServer
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
HttpListenerContext context = listener.GetContext();
|
HttpListenerContext context = listener.GetContext();
|
||||||
ProcessRequest(context);
|
// 요청을 ThreadPool에서 병렬 처리 — 동시 요청 블로킹 방지
|
||||||
|
ThreadPool.QueueUserWorkItem(_ =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
ProcessRequest(context);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
if (isRunning)
|
||||||
|
Debug.LogError($"[StreamingleDashboard] 요청 처리 오류: {ex.Message}");
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
catch (HttpListenerException)
|
catch (HttpListenerException)
|
||||||
{
|
{
|
||||||
@ -169,7 +181,7 @@ public class StreamingleDashboardServer
|
|||||||
{
|
{
|
||||||
if (isRunning)
|
if (isRunning)
|
||||||
{
|
{
|
||||||
Debug.LogError($"[StreamingleDashboard] 요청 처리 오류: {ex.Message}");
|
Debug.LogError($"[StreamingleDashboard] 요청 수신 오류: {ex.Message}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user