778 lines
29 KiB
JavaScript

/**
* Streamingle Camera Controller - Property Inspector
* 엘가토 공식 구조 기반 단순화 버전
*/
// Global variables
let websocket = null;
let uuid = null;
let actionContext = null; // 현재 액션의 컨텍스트
let settings = {};
// Context별 설정 관리
const contextSettings = new Map();
let currentActionContext = null;
// Unity 연결 상태 (Plugin Main에서 받아옴)
let isUnityConnected = false;
let cameraData = [];
let currentCamera = 0;
// DOM elements
let statusDot = null;
let connectionStatus = null;
let cameraSelect = null;
let currentCameraDisplay = null;
let refreshButton = null;
// 화면에 로그를 표시하는 함수
function logToScreen(msg, color = "#fff") {
let logDiv = document.getElementById('logArea');
if (!logDiv) {
logDiv = document.createElement('div');
logDiv.id = 'logArea';
logDiv.style.background = '#111';
logDiv.style.color = '#fff';
logDiv.style.fontSize = '11px';
logDiv.style.padding = '8px';
logDiv.style.marginTop = '16px';
logDiv.style.height = '120px';
logDiv.style.overflowY = 'auto';
document.body.appendChild(logDiv);
}
const line = document.createElement('div');
line.style.color = color;
line.textContent = `[${new Date().toLocaleTimeString()}] ${msg}`;
logDiv.appendChild(line);
logDiv.scrollTop = logDiv.scrollHeight;
}
// 기존 console.log/console.error를 화면에도 출력
const origLog = console.log;
console.log = function(...args) {
origLog.apply(console, args);
logToScreen(args.map(a => (typeof a === 'object' ? JSON.stringify(a) : a)).join(' '), '#0f0');
};
const origErr = console.error;
console.error = function(...args) {
origErr.apply(console, args);
logToScreen(args.map(a => (typeof a === 'object' ? JSON.stringify(a) : a)).join(' '), '#f55');
};
console.log('🔧 Property Inspector script loaded');
// Initialize when DOM is loaded
document.addEventListener('DOMContentLoaded', function() {
console.log('📋 Property Inspector 초기화');
initializePropertyInspector();
});
// Initialize Property Inspector
function initializePropertyInspector() {
// Get DOM elements
statusDot = document.getElementById('statusDot');
connectionStatus = document.getElementById('connection-status');
cameraSelect = document.getElementById('camera-select');
currentCameraDisplay = document.getElementById('current-camera');
refreshButton = document.getElementById('refresh-button');
// Setup event listeners
if (cameraSelect) {
cameraSelect.addEventListener('change', onCameraSelectionChanged);
}
if (refreshButton) {
refreshButton.addEventListener('click', onRefreshClicked);
}
console.log('✅ Property Inspector 준비 완료');
}
// Send message to plugin
function sendToPlugin(command, data = {}) {
if (!websocket) {
console.error('❌ WebSocket not available');
return;
}
try {
const message = {
command: command,
context: uuid,
...data
};
// StreamDeck SDK 표준 방식 - sendToPlugin 이벤트 사용
const payload = {
event: 'sendToPlugin',
context: uuid,
payload: message
};
websocket.send(JSON.stringify(payload));
console.log('📤 Message sent to plugin:', command, data);
} catch (error) {
console.error('❌ Failed to send message to plugin:', error);
}
}
// Update connection status display
function updateConnectionStatus(isConnected) {
console.log('🔄 Connection status update:', isConnected);
// 전역 변수도 업데이트
isUnityConnected = isConnected;
if (statusDot) {
statusDot.className = `dot ${isConnected ? 'green' : 'red'}`;
}
if (connectionStatus) {
connectionStatus.textContent = isConnected ? 'Unity 연결됨' : 'Unity 연결 안됨';
connectionStatus.className = isConnected ? 'connected' : 'disconnected';
}
if (cameraSelect) {
cameraSelect.disabled = !isConnected;
}
if (refreshButton) {
refreshButton.disabled = !isConnected;
}
}
// Update camera data display
function updateCameraData(cameraDataParam, currentCamera) {
console.log('📹 Camera data update:', cameraDataParam, currentCamera);
if (cameraSelect && cameraDataParam) {
// Clear existing options
cameraSelect.innerHTML = '';
// cameraDataParam이 직접 배열인지 확인
let cameras = cameraDataParam;
if (cameraDataParam.cameras) {
cameras = cameraDataParam.cameras;
} else if (Array.isArray(cameraDataParam)) {
cameras = cameraDataParam;
}
console.log('📹 처리할 카메라 배열:', cameras);
if (cameras && cameras.length > 0) {
// 전역 변수에 카메라 데이터 저장
cameraData = cameras;
console.log('💾 전역 cameraData 저장됨:', cameraData.length + '개');
// Add camera options
cameras.forEach((camera, index) => {
const option = document.createElement('option');
option.value = index;
option.textContent = `카메라 ${index + 1}`;
if (camera.name) {
option.textContent += ` (${camera.name})`;
}
cameraSelect.appendChild(option);
});
// Set current selection
if (typeof currentCamera === 'number') {
cameraSelect.value = currentCamera;
}
cameraSelect.disabled = false;
console.log('✅ 카메라 목록 업데이트 완료:', cameras.length + '개');
} else {
console.log('⚠️ 카메라 데이터가 없거나 빈 배열');
cameraSelect.disabled = true;
}
}
// Update current camera display
updateCurrentCameraDisplay(currentCamera);
}
// Update current camera display
function updateCurrentCameraDisplay(currentCamera) {
if (currentCameraDisplay) {
if (typeof currentCamera === 'number') {
currentCameraDisplay.textContent = `현재 카메라: ${currentCamera + 1}`;
} else {
currentCameraDisplay.textContent = '현재 카메라: -';
}
}
}
// Handle camera selection change
function onCameraSelectionChanged() {
if (!cameraSelect || !currentActionContext) return;
const selectedIndex = parseInt(cameraSelect.value, 10);
if (isNaN(selectedIndex)) return;
console.log('🎯 카메라 선택 변경:', selectedIndex);
console.log('📋 현재 cameraData:', cameraData);
console.log('📋 cameraData 길이:', cameraData ? cameraData.length : 'undefined');
// StreamDeck에 설정 저장 (Plugin Main에서 didReceiveSettings 이벤트 발생)
if (websocket) {
const setSettingsMessage = {
event: 'setSettings',
context: currentActionContext,
payload: {
cameraIndex: selectedIndex,
cameraList: cameraData // 현재 카메라 목록 포함
}
};
console.log('📤 Plugin Main으로 전송할 데이터:', setSettingsMessage.payload);
websocket.send(JSON.stringify(setSettingsMessage));
console.log('💾 설정 저장됨 - Plugin Main에서 버튼 제목 업데이트됨');
}
// UI 업데이트
updateCurrentCameraDisplay(selectedIndex);
}
// Handle refresh button click
function onRefreshClicked() {
console.log('🔄 새로고침 버튼 클릭 - Plugin Main에 카메라 목록 요청');
// Plugin Main에 카메라 목록 요청
sendToPlugin('requestCameraList');
}
// Unity 연결은 Plugin Main에서만 처리
function startUnityAutoReconnect() {
console.log('🩺 Property Inspector에서는 Unity 연결을 직접 관리하지 않음 - Plugin Main에서 처리됨');
}
// Unity 재연결 시도 (제거)
function attemptUnityReconnect() {
if (isShuttingDown || isConnecting || unityReconnectInterval) return;
unityConnectionAttempts++;
// 재연결 간격 조정
let delay;
if (unityConnectionAttempts <= 3) {
delay = 2000; // 처음 3번은 2초 간격
} else if (unityConnectionAttempts <= 10) {
delay = 5000; // 4-10번은 5초 간격
} else {
delay = 30000; // 그 이후는 30초 간격
}
console.log(`🔄 [Property Inspector] ${delay/1000}초 후 Unity 재연결 시도... (${unityConnectionAttempts}번째 시도)`);
unityReconnectInterval = setTimeout(() => {
unityReconnectInterval = null;
connectToUnity().catch(error => {
console.error(`❌ [Property Inspector] Unity 재연결 실패:`, error);
// 실패해도 계속 시도
if (!isShuttingDown) {
attemptUnityReconnect();
}
});
}, delay);
}
// Unity WebSocket 연결 (개선된 버전)
function connectToUnity() {
return new Promise((resolve, reject) => {
// 글로벌 상태 확인
if (window.sharedUnityConnected && window.sharedUnitySocket) {
console.log('✅ [Property Inspector] 기존 Unity 연결 재사용');
isUnityConnected = true;
unitySocket = window.sharedUnitySocket;
updateConnectionStatus(true);
resolve();
return;
}
if (isUnityConnected) {
console.log('✅ [Property Inspector] Unity 이미 연결됨');
resolve();
return;
}
if (isConnecting) {
console.log('⏳ [Property Inspector] Unity 연결 중... 대기');
reject(new Error('이미 연결 중'));
return;
}
isConnecting = true;
window.sharedIsConnecting = true;
console.log(`🔌 [Property Inspector] Unity 연결 시도... (시도 ${unityConnectionAttempts + 1}회)`);
try {
unitySocket = new WebSocket('ws://localhost:10701');
const connectionTimeout = setTimeout(() => {
isConnecting = false;
window.sharedIsConnecting = false;
console.log('⏰ [Property Inspector] Unity 연결 타임아웃');
if (unitySocket) {
unitySocket.close();
}
reject(new Error('연결 타임아웃'));
}, 5000);
unitySocket.onopen = function() {
clearTimeout(connectionTimeout);
isConnecting = false;
isUnityConnected = true;
unityConnectionAttempts = 0; // 성공 시 재시도 카운터 리셋
// 재연결 타이머 정리
if (unityReconnectInterval) {
clearTimeout(unityReconnectInterval);
unityReconnectInterval = null;
}
// 글로벌 상태 저장
window.sharedUnitySocket = unitySocket;
window.sharedUnityConnected = true;
window.sharedIsConnecting = false;
console.log('✅ [Property Inspector] Unity 연결 성공!');
updateConnectionStatus(true);
resolve();
};
unitySocket.onmessage = function(event) {
try {
const message = JSON.parse(event.data);
handleUnityMessage(message);
} catch (error) {
console.error('❌ [Property Inspector] Unity 메시지 파싱 오류:', error);
}
};
unitySocket.onclose = function(event) {
clearTimeout(connectionTimeout);
const wasConnected = isUnityConnected;
isConnecting = false;
isUnityConnected = false;
// 글로벌 상태 정리
window.sharedUnitySocket = null;
window.sharedUnityConnected = false;
window.sharedIsConnecting = false;
if (wasConnected) {
console.log(`❌ [Property Inspector] Unity 연결 끊어짐 (코드: ${event.code}, 이유: ${event.reason || '알 수 없음'})`);
}
updateConnectionStatus(false);
unitySocket = null;
if (!event.wasClean) {
reject(new Error('연결 실패'));
}
// 자동 재연결 시도
if (!isShuttingDown) {
attemptUnityReconnect();
}
};
unitySocket.onerror = function(error) {
clearTimeout(connectionTimeout);
isConnecting = false;
window.sharedIsConnecting = false;
console.error('❌ [Property Inspector] Unity 연결 오류:', error);
isUnityConnected = false;
updateConnectionStatus(false);
reject(error);
};
} catch (error) {
isConnecting = false;
window.sharedIsConnecting = false;
console.error('❌ [Property Inspector] Unity WebSocket 생성 실패:', error);
reject(error);
}
});
}
// Unity 메시지 처리
function handleUnityMessage(message) {
const messageType = message.type;
if (messageType === 'connection_established') {
console.log('🎉 Unity 연결 확인됨');
if (message.data && message.data.camera_data) {
console.log('📹 연결 시 카메라 데이터 수신 (초기 로드)');
updateCameraUI(message.data.camera_data.presets, message.data.camera_data.current_index);
// 이미 카메라 데이터를 받았으므로 추가 요청하지 않음
cameraData = message.data.camera_data.presets; // 글로벌 변수에 저장
window.sharedCameraData = cameraData; // 브라우저 세션에서 공유
}
} else if (messageType === 'camera_list_response') {
console.log('📹 카메라 목록 응답 수신 (요청에 대한 응답)');
if (message.data && message.data.camera_data) {
updateCameraUI(message.data.camera_data.presets, message.data.camera_data.current_index);
cameraData = message.data.camera_data.presets; // 글로벌 변수에 저장
window.sharedCameraData = cameraData; // 브라우저 세션에서 공유
}
} else if (messageType === 'camera_changed') {
console.log('📹 카메라 변경 알림 수신');
if (message.data && typeof message.data.camera_index === 'number') {
updateCurrentCamera(message.data.camera_index);
}
}
}
function updateCameraUI(cameras, currentIndex) {
if (!cameras || !Array.isArray(cameras)) {
console.error('❌ 잘못된 카메라 데이터');
return;
}
console.log('📹 카메라 UI 업데이트:', cameras.length + '개');
const cameraSelect = document.getElementById('camera-select');
const currentCameraDisplay = document.getElementById('current-camera');
if (!cameraSelect) {
console.error('❌ camera-select 요소를 찾을 수 없음');
return;
}
// 카메라 목록 업데이트
cameraSelect.innerHTML = '<option value="">카메라 선택...</option>';
cameras.forEach((camera, index) => {
const option = document.createElement('option');
option.value = index;
option.textContent = `${index + 1}. ${camera.name}`;
cameraSelect.appendChild(option);
});
// 현재 컨텍스트의 설정 가져오기
const currentSettings = getContextSettings(currentActionContext);
// 카메라 목록 저장
const newSettings = {
...currentSettings,
cameraList: cameras
};
saveContextSettings(currentActionContext, newSettings);
// 이미 설정된 카메라가 있으면 선택
if (currentSettings && typeof currentSettings.cameraIndex === 'number') {
cameraSelect.value = currentSettings.cameraIndex;
// 설정된 카메라의 이름으로 현재 표시만 업데이트 (버튼 제목은 Plugin Main에서 처리)
const selectedCamera = cameras[currentSettings.cameraIndex];
if (selectedCamera) {
currentCameraDisplay.textContent = `현재: ${selectedCamera.name}`;
console.log('📋 기존 카메라 설정 복원:', selectedCamera.name);
}
} else {
// 설정이 없으면 Unity의 현재 카메라 사용
if (typeof currentIndex === 'number' && currentIndex >= 0 && cameras[currentIndex]) {
cameraSelect.value = currentIndex;
currentCameraDisplay.textContent = `현재: ${cameras[currentIndex].name}`;
} else {
currentCameraDisplay.textContent = '현재: 없음';
}
}
console.log('✅ 카메라 UI 업데이트 완료');
}
// Unity에서 카메라 목록 요청
function requestCameraListFromUnity() {
if (isUnityConnected && unitySocket) {
const message = JSON.stringify({ type: 'get_camera_list' });
unitySocket.send(message);
console.log('📤 Unity에 카메라 목록 요청:', message);
}
}
// Unity에서 카메라 전환
function switchCameraInUnity(cameraIndex) {
if (isUnityConnected && unitySocket) {
const message = JSON.stringify({
type: 'switch_camera',
data: {
camera_index: cameraIndex
}
});
unitySocket.send(message);
console.log('📤 Unity에 카메라 전환 요청:', message);
}
}
// Unity에서 받은 카메라 데이터로 UI 업데이트
function updateCameraDataFromUnity() {
console.log('📹 Unity 카메라 데이터로 UI 업데이트:', cameraData);
if (cameraSelect && cameraData && cameraData.length > 0) {
// Clear existing options
cameraSelect.innerHTML = '';
// Add camera options
cameraData.forEach((camera, index) => {
const option = document.createElement('option');
option.value = index;
option.textContent = camera.name || `카메라 ${index + 1}`;
cameraSelect.appendChild(option);
});
// Set current selection
cameraSelect.value = currentCamera;
cameraSelect.disabled = false;
console.log('✅ 카메라 선택 목록 업데이트 완료');
}
// Update current camera display
updateCurrentCameraDisplay(currentCamera);
}
// Handle messages from plugin
function handleMessage(jsonObj) {
console.log('📨 Message received:', jsonObj);
try {
if (jsonObj.event === 'sendToPropertyInspector') {
// payload는 직접 객체로 전달됨
const payload = jsonObj.payload;
console.log('📋 Parsed payload:', payload);
switch (payload.type || payload.command) {
case 'connection_status':
console.log('📡 연결 상태 업데이트:', payload.connected);
updateConnectionStatus(payload.connected);
break;
case 'camera_data':
console.log('📹 카메라 데이터 업데이트 수신:', payload);
console.log('📹 카메라 데이터 상세:', payload.camera_data);
updateCameraData(payload.camera_data, payload.current_camera);
break;
case 'current_settings':
console.log('⚙️ 현재 설정 수신:', payload);
// 현재 설정을 컨텍스트에 저장
if (currentActionContext) {
const newSettings = {
cameraIndex: payload.cameraIndex || 0,
cameraList: payload.cameraList || [],
isUnityConnected: payload.isUnityConnected || false
};
saveContextSettings(currentActionContext, newSettings);
// UI 업데이트
updateConnectionStatus(payload.isUnityConnected);
if (payload.cameraList && payload.cameraList.length > 0) {
updateCameraData({ cameras: payload.cameraList }, payload.cameraIndex);
}
}
break;
case 'camera_changed':
console.log('📹 카메라 변경:', payload.current_camera);
updateCurrentCameraDisplay(payload.current_camera);
break;
default:
console.log('❓ Unknown message type:', payload.type || payload.command);
}
} else if (jsonObj.event === 'didReceiveSettings') {
// didReceiveSettings 이벤트 처리 추가
console.log('⚙️ didReceiveSettings 이벤트 수신:', jsonObj);
const settings = jsonObj.payload.settings || {};
console.log('📋 설정 데이터:', settings);
// 컨텍스트에 설정 저장
if (currentActionContext) {
saveContextSettings(currentActionContext, settings);
// UI 업데이트
if (settings.cameraList && settings.cameraList.length > 0) {
console.log('📹 카메라 목록으로 UI 업데이트:', settings.cameraList.length + '개');
updateCameraData(settings.cameraList, settings.cameraIndex || 0);
// 카메라 목록이 있다면 Unity가 연결되어 있다고 판단
console.log('🔍 카메라 목록 존재 - Unity 연결됨으로 판단');
updateConnectionStatus(true);
}
// 연결 상태 업데이트 (카메라 목록이 없을 때만 설정값 사용)
if (typeof settings.isUnityConnected === 'boolean' && (!settings.cameraList || settings.cameraList.length === 0)) {
updateConnectionStatus(settings.isUnityConnected);
}
}
}
} catch (error) {
console.error('❌ Failed to handle message:', error);
}
}
// StreamDeck SDK connection
function connectElgatoStreamDeckSocket(inPort, inPropertyInspectorUUID, inRegisterEvent, inInfo, inActionInfo) {
uuid = inPropertyInspectorUUID;
console.log('🔌 StreamDeck 연결 중...');
// Parse info
try {
const info = JSON.parse(inInfo);
const actionInfo = JSON.parse(inActionInfo);
actionContext = actionInfo.context; // 액션 컨텍스트 저장
currentActionContext = actionInfo.context; // 현재 액션 컨텍스트 설정
settings = actionInfo.payload.settings || {};
// 컨텍스트별 설정 초기화
saveContextSettings(currentActionContext, settings);
console.log('📋 컨텍스트 설정 완료:', currentActionContext);
} catch (error) {
console.error('❌ 정보 파싱 실패:', error);
}
// Connect to StreamDock
websocket = new WebSocket('ws://127.0.0.1:' + inPort);
websocket.onopen = function() {
console.log('✅ StreamDeck 연결됨');
// Register
const json = {
event: inRegisterEvent,
uuid: uuid
};
websocket.send(JSON.stringify(json));
// Plugin Main에 초기 설정 요청
console.log('📤 Plugin Main에 초기 설정 요청');
sendToPlugin('getInitialSettings');
};
websocket.onmessage = function(evt) {
try {
const jsonObj = JSON.parse(evt.data);
handleMessage(jsonObj);
} catch (error) {
console.error('❌ Failed to parse message:', error);
}
};
websocket.onclose = function() {
console.log('❌ StreamDeck WebSocket closed');
isShuttingDown = true; // 종료 플래그 설정
// Unity 자동 재연결 정리
if (unityReconnectInterval) {
clearTimeout(unityReconnectInterval);
unityReconnectInterval = null;
}
if (unityHealthCheckInterval) {
clearInterval(unityHealthCheckInterval);
unityHealthCheckInterval = null;
}
websocket = null;
};
websocket.onerror = function(error) {
console.error('❌ StreamDeck WebSocket error:', error);
};
}
// 컨텍스트별 설정 관리 함수들
function getContextSettings(context) {
if (!context) return {};
return contextSettings.get(context) || {};
}
function saveContextSettings(context, newSettings) {
if (!context) return;
contextSettings.set(context, { ...newSettings });
console.log('💾 컨텍스트 설정 저장:', context, newSettings);
}
// 현재 컨텍스트의 설정에서 카메라 이름을 가져오는 공통 함수
function getCurrentCameraName(context, cameraIndex = null) {
if (!context) return '카메라\n선택';
const settings = getContextSettings(context);
if (!settings || !settings.cameraList) return '카메라\n선택';
// cameraIndex가 제공되면 그것을 사용, 아니면 설정에서 가져옴
const index = cameraIndex !== null ? cameraIndex : settings.cameraIndex;
if (typeof index !== 'number' || !settings.cameraList[index]) return '카메라\n선택';
return settings.cameraList[index].name || '카메라\n선택';
}
// 버튼 제목 업데이트 공통 함수 (Plugin Main과 동일한 로직)
function updateButtonTitle(context, cameraName = null, cameraIndex = null) {
if (!websocket || !context) return;
// cameraName이 제공되지 않으면 현재 설정에서 가져옴
if (!cameraName) {
cameraName = getCurrentCameraName(context, cameraIndex);
console.log(`🔍 [Property Inspector] getCurrentCameraName 결과: "${cameraName}"`);
// 디버깅을 위해 현재 설정 상태도 출력
const settings = getContextSettings(context);
console.log(`🔍 [Property Inspector] 컨텍스트 설정:`, settings);
console.log(`🔍 [Property Inspector] 카메라 인덱스: ${cameraIndex !== null ? cameraIndex : settings.cameraIndex}, 목록 길이: ${settings.cameraList ? settings.cameraList.length : 0}`);
} else {
console.log(`🔍 [Property Inspector] 직접 제공된 카메라 이름: "${cameraName}"`);
}
// 기본값 설정
let title = cameraName || '카메라\n선택';
console.log(`🔍 [Property Inspector] 최종 사용할 제목 (가공 전): "${title}"`);
// 긴 텍스트를 두 줄로 나누기 (Plugin Main과 동일한 로직)
if (title.length > 8) {
const underscoreIndex = title.indexOf('_');
if (underscoreIndex !== -1 && underscoreIndex > 0) {
// 언더스코어가 있으면 그 위치에서 분할하고 언더스코어는 제거
const firstLine = title.substring(0, underscoreIndex);
const secondLine = title.substring(underscoreIndex + 1); // +1로 언더스코어 제거
// 각 줄이 너무 길면 적절히 자르기
const maxLineLength = 8;
let line1 = firstLine.length > maxLineLength ? firstLine.substring(0, maxLineLength - 1) + '.' : firstLine;
let line2 = secondLine.length > maxLineLength ? secondLine.substring(0, maxLineLength - 1) + '.' : secondLine;
title = line1 + '\n' + line2;
} else {
// 언더스코어가 없으면 중간 지점에서 분할
const midPoint = Math.ceil(title.length / 2);
const firstLine = title.substring(0, midPoint);
const secondLine = title.substring(midPoint);
title = firstLine + '\n' + secondLine;
}
}
// 버튼 제목 설정 (Plugin Main과 완전히 동일한 매개변수)
const message = {
event: 'setTitle',
context: context,
payload: {
title: title,
target: 0, // 하드웨어와 소프트웨어 모두
titleParameters: {
fontSize: 18,
showTitle: true,
titleAlignment: "middle"
}
}
};
websocket.send(JSON.stringify(message));
console.log('🏷️ [Property Inspector] 버튼 제목 업데이트:', title.replace('\n', '\\n'));
}