@klever-one/web-sdk
Version:
Web SDK for integrating real-time room management and streaming functionality into web applications
1,521 lines • 147 kB
JavaScript
"use client";
var Be = Object.defineProperty;
var pe = (r) => {
throw TypeError(r);
};
var Fe = (r, e, t) => e in r ? Be(r, e, { enumerable: !0, configurable: !0, writable: !0, value: t }) : r[e] = t;
var s = (r, e, t) => Fe(r, typeof e != "symbol" ? e + "" : e, t), Ee = (r, e, t) => e.has(r) || pe("Cannot " + t);
var p = (r, e, t) => (Ee(r, e, "read from private field"), t ? t.call(r) : e.get(r)), ce = (r, e, t) => e.has(r) ? pe("Cannot add the same private member more than once") : e instanceof WeakSet ? e.add(r) : e.set(r, t), V = (r, e, t, i) => (Ee(r, e, "write to private field"), i ? i.call(r, t) : e.set(r, t), t);
const ye = "initialData", Ce = "azureSttOpenai", j = "openai", He = "occupyingResource", Ve = "roomSessionTimeout";
const ve = "openai", ze = "timeout", xe = "X-Nlp-Api-Key", Ge = "Pending", $e = "Failed";
const je = {
1: "av_2",
// 차누
2: "av_5",
// 유리
3: "av_3",
// 크리스
4: "av_4",
// 마이클
5: "av_1"
// 제니
}, Ke = {
"en-US": "Introduce yourself in exactly 2–3 sentences. Do not exceed 3 sentences. Be concise and clear about your main role and how you can help.",
"ko-KR": "자신을 반드시 2~3문장으로만 간단히 소개해주세요. 3문장을 넘기지 말고, 주요 역할과 어떻게 도울 수 있는지만 간결하게 설명해주세요."
}, Je = new Proxy(Ke, {
get(r, e) {
return typeof e == "string" && e in r ? r[e] : r["ko-KR"];
}
}), Qe = 60 * 1e3;
var c = /* @__PURE__ */ ((r) => (r.IDLE = "IDLE", r.DISCONNECTED = "DISCONNECTED", r.CONNECTING = "CONNECTING", r.CONNECTED = "CONNECTED", r.RECONNECTING = "RECONNECTING", r.FAILED = "FAILED", r.ALLOCATING = "ALLOCATING", r))(c || {}), w = /* @__PURE__ */ ((r) => (r.SUCCESS = "SUCCESS", r.ERROR = "ERROR", r.IN_PROGRESS = "IN_PROGRESS", r))(w || {}), N = /* @__PURE__ */ ((r) => (r.SYSTEM = "system", r.USER = "user", r.ASSISTANT = "assistant", r.DATA = "data", r))(N || {}), h = /* @__PURE__ */ ((r) => (r.ROOM_CONNECTED = "ROOM_CONNECTED", r.ROOM_DISCONNECTED = "ROOM_DISCONNECTED", r.ROOM_CONNECTING = "ROOM_CONNECTING", r.ROOM_RECONNECTING = "ROOM_RECONNECTING", r.ROOM_CONNECTION_FAILED = "ROOM_CONNECTION_FAILED", r.ROOM_INFO_UPDATED = "ROOM_INFO_UPDATED", r.ROOM_SESSION_TIMEOUT = "ROOM_SESSION_TIMEOUT", r.STREAMING_CONNECTED = "STREAMING_CONNECTED", r.STREAMING_DISCONNECTED = "STREAMING_DISCONNECTED", r.STREAMING_CONNECTING = "STREAMING_CONNECTING", r.STREAMING_ALLOCATING = "STREAMING_ALLOCATING", r.STREAMING_RECONNECTING = "STREAMING_RECONNECTING", r.STREAMING_CONNECTION_FAILED = "STREAMING_CONNECTION_FAILED", r.IDLE_TIMEOUT_EXPIRED = "IDLE_TIMEOUT_EXPIRED", r.PAGE_UNLOAD = "PAGE_UNLOAD", r.LIP_MOTION_START = "LIP_MOTION_START", r.END_CONVERSATION = "END_CONVERSATION", r.STREAMER_LIST_UPDATED = "STREAMER_LIST_UPDATED", r.UNREAL_CONNECTED = "UNREAL_CONNECTED", r.RECORDER_ERROR = "RECORDER_ERROR", r))(h || {});
const D = class D {
constructor() {
s(this, "listeners", /* @__PURE__ */ new Map());
Object.values(h).forEach((e) => {
this.listeners.set(e, /* @__PURE__ */ new Set());
});
}
static getInstance() {
return D.instance || (D.instance = new D()), D.instance;
}
emit(e) {
const t = this.listeners.get(e.type);
t && t.forEach((i) => {
try {
i(e);
} catch {
}
});
}
subscribe(e, t) {
const i = this.listeners.get(e);
return i ? (i.add(t), () => i.delete(t)) : () => {
};
}
subscribeToMultiple(e, t) {
const i = e.map(
(n) => this.subscribe(n, t)
);
return () => i.forEach((n) => n());
}
clear() {
this.listeners.forEach((e) => e.clear());
}
};
s(D, "instance", null);
let b = D;
class qe {
constructor(e) {
// Internal state flags for initial readiness
s(this, "isDataChannelOpen", !1);
s(this, "isVideoInitialized", !1);
s(this, "isVideoStable", !1);
s(this, "videoFrameCallbackHandle", null);
// Overall state of the stream
s(this, "state", "Initializing");
// Performance and quality metrics
s(this, "metrics", {
networkQuality: -1,
bitrate: -1,
resolution: null
});
// Timeout for the initial connection
s(this, "connectionTimeout", null);
s(this, "callbacks");
// --- Public Event Handlers ---
s(this, "onDataChannelOpen", () => {
this.isDataChannelOpen = !0, this.checkReadyState();
});
s(this, "onVideoInitialized", () => {
this.isVideoInitialized = !0, this.verifyVideoStreamPlayback();
});
s(this, "onVideoStalled", () => {
this.state === "Ready" && (this.state = "Stalled", this.callbacks.onStalled());
});
// A hypothetical handler for when video resumes
s(this, "onVideoResumed", () => {
this.state === "Stalled" && (this.state = "Ready", this.callbacks.onResumed());
});
s(this, "onNetworkQualityChanged", (e) => {
this.metrics.networkQuality = e, this.callbacks.onMetricsUpdate(this.metrics);
});
s(this, "onBitrateChanged", (e) => {
this.metrics.bitrate = e, this.callbacks.onMetricsUpdate(this.metrics);
});
s(this, "onResolutionChanged", (e, t) => {
this.metrics.resolution = { width: e, height: t }, this.callbacks.onMetricsUpdate(this.metrics);
});
s(this, "onStreamerListMessage", (e) => {
const t = e.data?.messageStreamerList?.ids || [];
this.callbacks.onStreamerListUpdate(t);
});
s(this, "onWebRtcFailed", (e) => {
this.callbacks.onWebRtcFailed(e);
});
// --- State Management ---
/**
* Starts a timer that will trigger a WebRTC failure if the connection
* is not established within the given timeout period.
* @param timeout - The timeout in milliseconds. Defaults to 60000 (60 seconds).
*/
s(this, "startConnectionTimer", (e = Qe) => {
typeof window > "u" || (this.clearConnectionTimer(), this.connectionTimeout = window.setTimeout(() => {
this.state !== "Ready" && this.callbacks.onWebRtcFailed(
`스트리밍 연결에 실패했습니다. 타임아웃: ${e}ms.`
);
}, e));
});
/**
* Clears the connection timeout timer.
*/
s(this, "clearConnectionTimer", () => {
typeof window > "u" || this.connectionTimeout && (window.clearTimeout(this.connectionTimeout), this.connectionTimeout = null);
});
s(this, "reset", () => {
this.isDataChannelOpen = !1, this.isVideoInitialized = !1, this.isVideoStable = !1, this.state = "Initializing", this.metrics = { networkQuality: -1, bitrate: -1, resolution: null }, this.cancelVideoStreamVerification(), this.clearConnectionTimer();
});
this.callbacks = e;
}
// --- Private Helper Methods ---
checkReadyState() {
this.state === "Initializing" && this.isDataChannelOpen && this.isVideoInitialized && this.isVideoStable && (this.state = "Ready", this.clearConnectionTimer(), this.callbacks.onReady());
}
cancelVideoStreamVerification() {
if (!(typeof document > "u") && this.videoFrameCallbackHandle) {
const e = document.getElementById(
"streamingVideo"
);
if (e)
try {
e.cancelVideoFrameCallback(this.videoFrameCallbackHandle);
} catch {
}
this.videoFrameCallbackHandle = null;
}
}
verifyVideoStreamPlayback() {
if (typeof document > "u") return;
const e = document.getElementById(
"streamingVideo"
);
if (!e || typeof e.requestVideoFrameCallback != "function") {
this.isVideoStable = !0, this.checkReadyState();
return;
}
let t = 0;
const i = () => {
t++, t >= 5 ? (this.isVideoStable = !0, this.checkReadyState()) : this.videoFrameCallbackHandle = e.requestVideoFrameCallback(i);
};
this.videoFrameCallbackHandle = e.requestVideoFrameCallback(i);
}
}
var Xe = typeof global == "object" && global && global.Object === Object && global, Ye = typeof self == "object" && self && self.Object === Object && self, ke = Xe || Ye || Function("return this")(), F = ke.Symbol, Le = Object.prototype, Ze = Le.hasOwnProperty, et = Le.toString, z = F ? F.toStringTag : void 0;
function tt(r) {
var e = Ze.call(r, z), t = r[z];
try {
r[z] = void 0;
var i = !0;
} catch {
}
var n = et.call(r);
return i && (e ? r[z] = t : delete r[z]), n;
}
var it = Object.prototype, nt = it.toString;
function st(r) {
return nt.call(r);
}
var rt = "[object Null]", ot = "[object Undefined]", Te = F ? F.toStringTag : void 0;
function at(r) {
return r == null ? r === void 0 ? ot : rt : Te && Te in Object(r) ? tt(r) : st(r);
}
function ct(r) {
return r != null && typeof r == "object";
}
var lt = "[object Symbol]";
function Ue(r) {
return typeof r == "symbol" || ct(r) && at(r) == lt;
}
function dt(r, e) {
for (var t = -1, i = r == null ? 0 : r.length, n = Array(i); ++t < i; )
n[t] = e(r[t], t, r);
return n;
}
var ht = Array.isArray, Ie = F ? F.prototype : void 0, be = Ie ? Ie.toString : void 0;
function _e(r) {
if (typeof r == "string")
return r;
if (ht(r))
return dt(r, _e) + "";
if (Ue(r))
return be ? be.call(r) : "";
var e = r + "";
return e == "0" && 1 / r == -1 / 0 ? "-0" : e;
}
var ut = /\s/;
function mt(r) {
for (var e = r.length; e-- && ut.test(r.charAt(e)); )
;
return e;
}
var gt = /^\s+/;
function ft(r) {
return r && r.slice(0, mt(r) + 1).replace(gt, "");
}
function Y(r) {
var e = typeof r;
return r != null && (e == "object" || e == "function");
}
var Re = NaN, St = /^[-+]0x[0-9a-f]+$/i, pt = /^0b[01]+$/i, Et = /^0o[0-7]+$/i, yt = parseInt;
function Ae(r) {
if (typeof r == "number")
return r;
if (Ue(r))
return Re;
if (Y(r)) {
var e = typeof r.valueOf == "function" ? r.valueOf() : r;
r = Y(e) ? e + "" : e;
}
if (typeof r != "string")
return r === 0 ? r : +r;
r = ft(r);
var t = pt.test(r);
return t || Et.test(r) ? yt(r.slice(2), t ? 2 : 8) : St.test(r) ? Re : +r;
}
function Ct(r) {
return r == null ? "" : _e(r);
}
var le = function() {
return ke.Date.now();
}, vt = "Expected a function", Tt = Math.max, It = Math.min;
function Pe(r, e, t) {
var i, n, o, a, l, d, m = 0, u = !1, g = !1, y = !0;
if (typeof r != "function")
throw new TypeError(vt);
e = Ae(e) || 0, Y(t) && (u = !!t.leading, g = "maxWait" in t, o = g ? Tt(Ae(t.maxWait) || 0, e) : o, y = "trailing" in t ? !!t.trailing : y);
function C(S) {
var R = i, H = n;
return i = n = void 0, m = S, a = r.apply(H, R), a;
}
function se(S) {
return m = S, l = setTimeout(B, e), u ? C(S) : a;
}
function re(S) {
var R = S - d, H = S - m, Se = e - R;
return g ? It(Se, o - H) : Se;
}
function G(S) {
var R = S - d, H = S - m;
return d === void 0 || R >= e || R < 0 || g && H >= o;
}
function B() {
var S = le();
if (G(S))
return $(S);
l = setTimeout(B, re(S));
}
function $(S) {
return l = void 0, y && i ? C(S) : (i = n = void 0, a);
}
function oe() {
l !== void 0 && clearTimeout(l), m = 0, i = d = n = l = void 0;
}
function We() {
return l === void 0 ? a : $(le());
}
function ae() {
var S = le(), R = G(S);
if (i = arguments, n = this, d = S, R) {
if (l === void 0)
return se(d);
if (g)
return clearTimeout(l), l = setTimeout(B, e), C(d);
}
return l === void 0 && (l = setTimeout(B, e)), a;
}
return ae.cancel = oe, ae.flush = We, ae;
}
var bt = "Expected a function";
function Rt(r, e, t) {
var i = !0, n = !0;
if (typeof r != "function")
throw new TypeError(bt);
return Y(t) && (i = "leading" in t ? !1 : i, n = "trailing" in t ? !0 : n), Pe(r, e, {
leading: i,
maxWait: e,
trailing: n
});
}
var At = 0;
function W(r) {
var e = ++At;
return Ct(r) + e;
}
class wt {
constructor() {
s(this, "initialAudioTrack", null);
s(this, "lockedAudioElement", null);
s(this, "currentVolume", 1);
// 현재 볼륨 상태 저장
s(this, "psAudioElement", null);
}
// 픽셀 스트리밍 내부 audio element
/**
* Setup audio tracking for the given PixelStreaming instance
* Extracts and locks the initial audio track to prevent Safari audio issues
*/
setup(e) {
const t = e;
if (this.psAudioElement = t._webRtcController?.streamController?.audioElement || null, !this.psAudioElement)
return;
const i = this.psAudioElement.srcObject;
if (!i)
return;
const n = i.getAudioTracks();
n.length > 0 && !this.initialAudioTrack && (this.initialAudioTrack = n[0], this.createLockedAudio(this.initialAudioTrack));
const o = (a) => {
a.track.kind === "audio" && !this.initialAudioTrack && (this.initialAudioTrack = a.track, this.createLockedAudio(this.initialAudioTrack));
};
i.addEventListener("addtrack", o);
}
/**
* Create locked audio element with the given track
* This prevents Safari from losing audio during stream transitions
*/
createLockedAudio(e) {
this.lockedAudioElement = document.createElement("audio"), this.lockedAudioElement.autoplay = !0, this.lockedAudioElement.controls = !1, this.lockedAudioElement.muted = !1, this.lockedAudioElement.volume = this.currentVolume, this.lockedAudioElement.srcObject = new MediaStream([e]), document.body.appendChild(this.lockedAudioElement);
}
/**
* Set volume for both locked and PS audio elements
* @param volume - Volume level (0.0 to 1.0)
*/
setVolume(e) {
this.currentVolume = Math.max(0, Math.min(1, e)), this.lockedAudioElement && (this.lockedAudioElement.volume = this.currentVolume, this.lockedAudioElement.muted = this.currentVolume === 0), this.psAudioElement && (this.psAudioElement.volume = this.currentVolume, this.psAudioElement.muted = this.currentVolume === 0), typeof document < "u" && document.querySelectorAll("audio").forEach((i) => {
i.volume = this.currentVolume, i.muted = this.currentVolume === 0;
});
}
/**
* Get current volume level
* @returns Current volume (0.0 to 1.0)
*/
getVolume() {
return this.currentVolume;
}
/**
* Cleanup all audio resources
* Stops tracks and removes audio elements from DOM
*/
cleanup() {
if (this.initialAudioTrack) {
try {
this.initialAudioTrack.stop();
} catch {
}
this.initialAudioTrack = null;
}
this.lockedAudioElement && (this.lockedAudioElement.srcObject = null, this.lockedAudioElement.remove(), this.lockedAudioElement = null);
}
}
const O = class O {
constructor() {
s(this, "stream", null);
s(this, "mediaState", {
isStreamReady: !1,
isReadyToSend: !1,
keepAliveInterval: null
});
s(this, "isReadyToSendListeners", []);
s(this, "hasSentResetMessage", !1);
s(this, "eventBus");
s(this, "audioManager");
this.setupBeforeUnloadHandler(), this.eventBus = b.getInstance(), this.audioManager = new wt();
}
static getInstance() {
return O.instance || (O.instance = new O()), O.instance;
}
setStream(e) {
this.stream = e, e ? (this.setupStreamMediaHandlers(), this.startKeepAlive()) : this.stopKeepAlive();
}
setupStreamMediaHandlers() {
this.stream && this.stream.addResponseEventListener(
"handleResponseFunction",
(e) => {
this.handleResponseFunction(e);
}
);
}
setupBeforeUnloadHandler() {
typeof window > "u" || window.addEventListener("beforeunload", () => {
if (this.stream && !this.hasSentResetMessage)
try {
this.emitToDataChannel({
messageType: "reset",
timestamp: Date.now(),
sessionId: W("session_"),
payload: {}
}), this.hasSentResetMessage = !0;
} catch {
}
});
}
startKeepAlive() {
this.mediaState.keepAliveInterval && clearInterval(this.mediaState.keepAliveInterval), this.mediaState.keepAliveInterval = setInterval(() => {
this.sendKeepAlive();
}, 3e4);
}
stopKeepAlive() {
this.mediaState.keepAliveInterval && (clearInterval(this.mediaState.keepAliveInterval), this.mediaState.keepAliveInterval = null);
}
/**
* 중앙화된 데이터 채널 전송 메서드
* 모든 emitUIInteraction 호출을 이 메서드를 통해 래핑
*/
emitToDataChannel(e) {
if (this.stream)
try {
this.stream.emitUIInteraction(e);
} catch {
}
}
sendKeepAlive() {
if (this.stream)
try {
const e = {
messageType: "keepAlive",
timestamp: Date.now(),
sessionId: W("session_"),
payload: {}
};
this.emitToDataChannel(e);
} catch {
}
}
async handleResponseFunction(e) {
try {
if (e === "UnrealConnected") {
this.mediaState.isStreamReady && (this.eventBus.emit({
type: h.UNREAL_CONNECTED,
timestamp: Date.now()
}), this.sendWebConnectedInfo(), this.sendAccountType(), this.setIsReadyToSend(!0), this.stream && this.audioManager.setup(this.stream));
return;
} else if (e === "LipmotionStart") {
this.handleLipmotionStart();
return;
}
const t = JSON.parse(e);
t.messageType === "updateMessage" && this.handleUpdateMessage(t);
} catch {
e.includes("UnrealConnected") && this.setIsReadyToSend(!0);
}
}
sendWebConnectedInfo() {
this.stream && this.emitToDataChannel({
Category: "SystemSetting",
Type: "WebConnected",
Width: typeof window < "u" ? window.innerWidth : 0
});
}
handleLipmotionStart() {
this.eventBus.emit({
type: h.LIP_MOTION_START,
timestamp: Date.now()
});
}
handleUpdateMessage(e) {
if (typeof window > "u" || typeof CustomEvent > "u")
return;
const { text: t, index: i, isAllCompleted: n } = e.payload, o = new CustomEvent("updateMessage", {
detail: { source: "streaming", text: t, index: i, isAllCompleted: n }
});
window.dispatchEvent(o);
}
ttsInteractions(e, t, i, n) {
!this.stream || !e || (this.emitToDataChannel({
Category: "VoiceSetting",
Type: "TTS",
Value: n
}), this.emitToDataChannel({
Category: "VoiceSetting",
Type: "Voice",
Value: t
}), this.emitToDataChannel({
Category: "VoiceSetting",
Type: "Language",
Value: i
}), this.emitToDataChannel({
Category: "VoiceSetting",
Type: "Script",
Value: encodeURIComponent(e)
}));
}
sendStartConversation() {
this.stream && this.emitToDataChannel({
Category: "ConversationSetting",
Type: "StartConversation"
});
}
endConversation() {
if (this.stream)
try {
this.emitToDataChannel({
Category: "ConversationSetting",
Type: "EndConversation"
}), this.eventBus.emit({
type: h.END_CONVERSATION,
timestamp: Date.now()
});
} catch {
}
}
autoPlayVideo() {
if (this.stream)
try {
this.stream.play();
} catch {
}
}
play() {
if (this.stream)
try {
this.stream.play();
} catch {
}
}
// Ready state management
isReadyToSendMessages() {
return this.mediaState.isReadyToSend;
}
isStreamReady() {
return this.mediaState.isStreamReady;
}
resetIsReadyToSend() {
this.setIsReadyToSend(!1);
}
resetIsStreamReady() {
this.setIsStreamReady(!1);
}
setIsReadyToSend(e) {
this.mediaState.isReadyToSend !== e && (this.mediaState.isReadyToSend = e, this.isReadyToSendListeners.forEach((t) => {
try {
t(e);
} catch {
}
}));
}
setIsStreamReady(e) {
this.mediaState.isStreamReady !== e && (this.mediaState.isStreamReady = e);
}
sendAccountType() {
this.stream && this.emitToDataChannel({
Category: "SystemSetting",
Type: "RoleId",
Value: "ROLE_3"
});
}
addIsReadyToSendListener(e) {
this.removeIsReadyToSendListener(e), this.isReadyToSendListeners.push(e), e(this.mediaState.isReadyToSend);
}
removeIsReadyToSendListener(e) {
this.isReadyToSendListeners = this.isReadyToSendListeners.filter(
(t) => t !== e
);
}
// Audio control
setVolume(e) {
this.audioManager.setVolume(e);
}
getVolume() {
return this.audioManager.getVolume();
}
sendZoomIn() {
this.emitToDataChannel({
Category: "PageSetting",
Type: "ZoomIn"
});
}
sendZoomOut() {
this.emitToDataChannel({
Category: "PageSetting",
Type: "ZoomOut"
});
}
cleanup() {
this.stopKeepAlive(), this.setIsReadyToSend(!1), this.isReadyToSendListeners.length = 0, this.hasSentResetMessage = !1, this.audioManager.cleanup(), this.stream = null;
}
};
s(O, "instance", null);
let Z = O;
const M = class M {
constructor() {
s(this, "stream", null);
s(this, "resizeHandler", null);
s(this, "currentCamera", "cam1");
// 기본 카메라 설정
s(this, "containerElement", null);
s(this, "handleMouseDown", (e) => {
if (typeof document > "u" || e.button !== 0) return;
const t = document.getElementById("streamingVideo");
t && (t.style.cursor = "default", this.emitUnrealCoordinates(e, "MouseDown"));
});
s(this, "handleMouseUp", (e) => {
if (typeof document > "u" || e.button !== 0) return;
const t = document.getElementById("streamingVideo");
t && (t.style.cursor = "default", this.emitUnrealCoordinates(e, "MouseUp"));
});
s(this, "emitUnrealCoordinates", (e, t) => {
if (typeof document > "u") return;
const i = document.getElementById("streamingVideo");
if (!i) return;
const n = i.videoWidth, o = i.videoHeight, a = i.getBoundingClientRect(), l = a.width, d = a.height, m = Math.max(l / n, d / o), u = n * m, g = o * m, y = (u - l) / 2, C = (g - d) / 2, se = e.clientX - a.left + y, re = e.clientY - a.top + C, G = se / u, B = re / g, $ = G * n, oe = B * o;
this.emitToDataChannel({
Category: "PageSetting",
type: t,
x: $,
y: oe
});
});
}
static getInstance() {
return M.instance || (M.instance = new M()), M.instance;
}
setupResizeHandler() {
typeof window > "u" || !this.containerElement || !this.stream || (this.resizeHandler = () => {
setTimeout(() => {
const e = this.containerElement?.offsetWidth, t = this.containerElement?.offsetHeight;
this.stream && e && t && this.resize(e, t);
}, 100);
}, window.addEventListener("resize", this.resizeHandler), this.resizeHandler());
}
setStream(e) {
this.stream = e;
}
setContainer(e) {
this.containerElement = e;
}
getContainer() {
return this.containerElement;
}
getResizeHandler() {
return this.resizeHandler;
}
resetResizeHandler() {
this.resizeHandler = null;
}
// public setCurrentPageName(pageName: string | null): void {
// this.currentPageName = pageName;
// }
// Screen and video controls
resize(e, t, i) {
if (!this.stream)
return;
const n = e / 3 * 2, o = t / 3 * 2;
try {
const a = `r.setRes ${n}x${o}`;
this.emitToDataChannel({
Category: "PageSetting",
Type: "WindowSize",
Value: a,
Width: e,
Height: t,
CamNum: this.currentCamera
});
} catch {
}
}
/**
* 중앙화된 데이터 채널 전송 메서드
* 모든 emitUIInteraction 호출을 이 메서드를 통해 래핑
*/
emitToDataChannel(e) {
if (this.stream)
try {
this.stream.emitUIInteraction(e);
} catch {
}
}
enteredFullpage() {
if (typeof window > "u" || typeof document > "u" || !this.stream) return;
const e = window.innerWidth, t = window.innerHeight;
this.emitToDataChannel({
Category: "PageSetting",
Type: "EnteredFullPage"
}), this.setCameraConfig("cam2");
const i = document.getElementById("streamingVideo");
i?.addEventListener("mousedown", this.handleMouseDown), i?.addEventListener("mouseup", this.handleMouseUp), this.resize(e, t);
}
exitedFullpage(e) {
if (this.stream)
try {
if (this.emitToDataChannel({
Category: "UI",
Type: "ExitedFullpage"
}), e.current) {
const t = e.current.getBoundingClientRect();
this.resize(t.width, t.height, "exitFullpage");
}
} catch {
}
}
setCameraConfig(e) {
this.currentCamera = e;
}
// Avatar and appearance management
sendAvatarNum(e) {
if (this.stream)
try {
this.emitToDataChannel({
Category: "AvatarSetting",
Type: "AvatarNum",
Value: e
});
} catch {
}
}
sendAvatarAppearanceChange(e) {
if (this.stream)
try {
if (e.avatarType.includes("Custom") && !["1", "2", "3", "4", "5"].includes(
e.avatarAppearanceRefVal1 ?? ""
)) {
const i = {
TaskID: e.avatarAppearanceRefVal1 ?? "",
Type: "Custom",
Gender: e.gender ?? "M",
selectedAppearance: e.outfit
};
this.sendAvatarAppearanceChangeInternal({ payload: i, isCustom: !0 });
} else {
const i = {
PresetID: je[e.avatarAppearanceRefVal1 ?? ""] ?? "av_3",
Type: "Preset",
selectedAppearance: e.outfit
};
this.sendAvatarAppearanceChangeInternal({
payload: i,
isCustom: !1
});
}
} catch {
}
}
sendAvatarAppearanceChangeInternal({
payload: e
}) {
if (this.stream)
try {
const t = {
sessionId: W(`#sess_${Date.now()}_`),
messageType: "avatar.change",
timestamp: Date.now(),
payload: e
};
this.emitToDataChannel({
Category: "AvatarSetting",
Type: "AvatarAppearance",
Value: t
});
} catch {
}
}
generateDigitalHuman(e, t) {
if (this.stream)
try {
this.emitToDataChannel({
Category: "CustomSetting",
Type: "GenerateAvatarHead",
TaskId: e,
Gender: t
});
} catch {
}
}
sendFailedGenerateAvatarHead() {
if (this.stream)
try {
this.emitToDataChannel({
Category: "CustomSetting",
Type: "FailedGenerateAvatarHead"
});
} catch {
}
}
// Environment and settings
sendPlaceBackgroundChange(e) {
if (this.stream)
try {
this.emitToDataChannel({
Category: "BackgoundSetting",
Type: "BackgroundImage",
Value: e
});
} catch {
}
}
sendPageSetting(e, t) {
if (this.stream)
try {
this.emitToDataChannel({
Category: "PageSetting",
Type: e,
Value: t
});
} catch {
}
}
sendVoiceSetting(e) {
if (this.stream)
try {
this.emitToDataChannel({
Category: "VoiceSetting",
Type: "Voice",
Value: e
});
} catch {
}
}
// Generic UI interaction (used internally by other specific methods)
emitUIInteraction(e) {
if (this.stream)
try {
this.emitToDataChannel(e);
} catch {
}
}
reset() {
if (this.stream)
try {
this.emitToDataChannel({
Category: "SystemSetting",
Type: "Reset"
});
} catch {
}
}
cleanup() {
typeof window > "u" || (this.resizeHandler && (window.removeEventListener("resize", this.resizeHandler), this.resizeHandler = null), this.stream = null);
}
};
s(M, "instance", null);
let ee = M;
const k = class k {
constructor() {
s(this, "stream", null);
s(this, "application", null);
s(this, "currentStatus", c.IDLE);
s(this, "hasSentResetMessage", !1);
s(this, "onWebRtcConnectedHandler", null);
s(this, "onWebRtcDisconnectedHandler", null);
s(this, "onWebRtcPlayStreamErrorHandler", null);
s(this, "onWebRtcFailedEventHandler", null);
s(this, "beforeUnloadHandler", null);
// Reconnection logic
s(this, "reconnectTimeout", null);
s(this, "hasFailedConnection", !1);
s(this, "reconnectAttempts", 0);
s(this, "maxReconnectAttempts", 3);
s(this, "isDisconnecting", !1);
// Configuration
s(this, "signalingServerUrl", null);
s(this, "config", {});
// Event system
s(this, "eventBus");
s(this, "statusListeners", []);
s(this, "disconnectListeners", []);
s(this, "eventUnsubscribeFunctions", []);
s(this, "keepAliveInterval", null);
// UI Service
s(this, "uiService");
s(this, "mediaService");
s(this, "readyStateManager");
// Locked Audio
s(this, "initialAudioTrack", null);
s(this, "lockedAudioElement", null);
s(this, "onStreamReady", () => {
const e = this.uiService.getContainer(), t = (typeof document < "u" ? document.getElementById("streaming-container") : null) || e;
if (t) {
const { offsetWidth: i, offsetHeight: n } = t;
this.uiService.resize(i, n);
}
this.mediaService.setIsStreamReady(!0);
});
s(this, "onStreamStalled", () => {
});
s(this, "onStreamResumed", () => {
});
s(this, "onStreamMetricsUpdate", (e) => {
});
s(this, "onStreamerListUpdate", (e) => {
this.eventBus.emit({
type: h.STREAMER_LIST_UPDATED,
timestamp: Date.now(),
data: { streamerList: e }
});
});
s(this, "onWebRtcFailed", (e) => {
const t = e || "연결에 실패했습니다. 잠시 후 다시 시도해 주세요.";
this.readyStateManager.clearConnectionTimer(), this.keepAliveInterval && (clearInterval(this.keepAliveInterval), this.keepAliveInterval = null), this.hasFailedConnection = !0, this.setStatus(c.FAILED), this.eventBus.emit({
type: h.STREAMING_CONNECTION_FAILED,
timestamp: Date.now(),
data: { message: t }
});
});
this.eventBus = b.getInstance(), this.uiService = ee.getInstance(), this.mediaService = Z.getInstance();
const e = {
onReady: this.onStreamReady,
onStalled: this.onStreamStalled,
onResumed: this.onStreamResumed,
onMetricsUpdate: this.onStreamMetricsUpdate,
onStreamerListUpdate: this.onStreamerListUpdate,
onWebRtcFailed: this.onWebRtcFailed
};
this.readyStateManager = new qe(e), this.setupEventListeners();
}
static getInstance() {
return k.instance || (k.instance = new k()), k.instance;
}
/**
* Epic Games 라이브러리를 dynamic import로 로드 (SSR-safe)
*/
async loadEpicGamesLibraries() {
if (typeof window > "u")
throw new Error(
"Epic Games libraries can only be loaded in browser environment"
);
const [e, t] = await Promise.all([
import("../lib-pixelstreamingfrontend.esm-Bu1fAG_z.js"),
import("../lib-pixelstreamingfrontend-ui.esm-BbRm6bMC.js")
]);
return {
Config: e.Config,
PixelStreaming: e.PixelStreaming,
NumericParameters: e.NumericParameters,
Application: t.Application,
PixelStreamingApplicationStyle: t.PixelStreamingApplicationStyle,
UIElementCreationMode: t.UIElementCreationMode
};
}
cleanupEventListeners() {
this.eventUnsubscribeFunctions.forEach((e) => e()), this.eventUnsubscribeFunctions.length = 0, this.beforeUnloadHandler && typeof window < "u" && (window.removeEventListener("beforeunload", this.beforeUnloadHandler), this.beforeUnloadHandler = null);
}
setupEventListeners() {
this.cleanupEventListeners();
const e = this.eventBus.subscribe(
h.ROOM_INFO_UPDATED,
(n) => {
const o = n.data?.roomInfo;
o?.resourceInfo?.url && this.connect(o.resourceInfo.url).catch((a) => {
this.setStatus(c.FAILED);
});
}
), t = this.eventBus.subscribe(
h.ROOM_DISCONNECTED,
() => {
this.disconnect();
}
), i = this.eventBus.subscribe(
h.UNREAL_CONNECTED,
() => {
this.stream && this.setupLockedAudio(this.stream);
}
);
this.eventUnsubscribeFunctions.push(
e,
t,
i
), typeof window < "u" && (this.beforeUnloadHandler = () => {
if (this.stream && !this.hasSentResetMessage)
try {
this.stream.webSocketController?.webSocket?.readyState === WebSocket.OPEN && (this.reset(), this.hasSentResetMessage = !0);
} catch {
}
}, window.addEventListener("beforeunload", this.beforeUnloadHandler));
}
reset() {
this.stream && this.emitToDataChannel({
Category: "SystemSetting",
Type: "Reset"
});
}
/**
* 중앙화된 데이터 채널 전송 메서드
* 모든 emitUIInteraction 호출을 이 메서드를 통해 래핑
*/
emitToDataChannel(e) {
if (this.stream)
try {
this.stream.emitUIInteraction(e);
} catch {
}
}
setContainer(e) {
this.uiService.setContainer(e), this.application?.rootElement && e && (e.appendChild(this.application.rootElement), this.uiService.setupResizeHandler());
}
forceResetContainer(e) {
const t = this.uiService.getContainer();
if (this.application?.rootElement)
try {
t && t.contains(this.application.rootElement) && t.removeChild(this.application.rootElement), this.application.rootElement.parentElement && this.application.rootElement.parentElement.removeChild(
this.application.rootElement
);
} catch {
}
this.uiService.setContainer(e);
}
async connect(e, t = {}) {
if (!(this.currentStatus === c.CONNECTING || this.currentStatus === c.CONNECTED)) {
this.signalingServerUrl = e, this.config = t, this.setStatus(c.CONNECTING);
try {
await this.createStreamingConnection();
} catch (i) {
throw this.setStatus(c.FAILED), i;
}
}
}
async createStreamingConnection() {
if (!this.signalingServerUrl)
throw new Error("Signaling server URL not provided");
const e = await this.loadEpicGamesLibraries(), t = new e.Config({
useUrlParams: !0
});
t.setTextSetting("ss", this.signalingServerUrl), t.setNumericSetting(e.NumericParameters.MaxReconnectAttempts, 0), t.setFlagEnabled(
"KeyboardInput",
this.config.setKeyboardEnabled ?? !1
), t.setFlagEnabled("HoveringMouse", !1), t.setFlagEnabled("MouseInput", !1);
const i = new e.PixelStreamingApplicationStyle();
i.applyStyleSheet(), this.stream = new e.PixelStreaming(t), this.uiService.setStream(this.stream), this.application = new e.Application({
stream: this.stream,
onColorModeChanged: (o) => i.setColorMode(o),
fullScreenControlsConfig: {
creationMode: e.UIElementCreationMode.Disable
},
settingsPanelConfig: {
isEnabled: !1,
visibilityButtonConfig: {
creationMode: e.UIElementCreationMode.Disable
}
},
statsPanelConfig: {
isEnabled: !1,
visibilityButtonConfig: {
creationMode: e.UIElementCreationMode.Disable
}
},
videoQpIndicatorConfig: { disableIndicator: !0 }
});
const n = this.uiService.getContainer();
if (n && this.application.rootElement && (n.innerHTML = "", n.appendChild(this.application.rootElement), typeof document < "u")) {
const o = document.getElementById("streamingVideo");
o && (o.style.objectFit = "cover", document.addEventListener("pointerlockchange", () => {
document.exitPointerLock();
}));
}
this.stream._onDisconnect = () => {
this.setStatus(c.DISCONNECTED), this.notifyDisconnectListeners(), !this.isDisconnecting && this.hasFailedConnection && this.scheduleReconnect();
}, this.registerEventHandlers();
try {
this.readyStateManager.startConnectionTimer(), this.stream.connect();
} catch (o) {
throw this.hasFailedConnection = !0, this.setStatus(c.FAILED), this.readyStateManager.clearConnectionTimer(), o;
}
}
registerEventHandlers() {
this.stream && (this.onWebRtcConnectedHandler = () => {
this.hasFailedConnection = !1, this.reconnectAttempts = 0, this.setStatus(c.CONNECTED), this.keepAliveInterval && clearInterval(this.keepAliveInterval), this.keepAliveInterval = setInterval(() => {
try {
this.stream?.webSocketController?.webSocket?.readyState === WebSocket.OPEN && this.stream.webSocketController.webSocket.send(
JSON.stringify({ type: "ping" })
);
} catch {
}
}, 3e4);
}, this.stream.addEventListener(
"webRtcConnected",
this.onWebRtcConnectedHandler
), this.onWebRtcDisconnectedHandler = () => {
this.isDisconnecting || this.setStatus(c.DISCONNECTED);
}, this.stream.addEventListener(
"webRtcDisconnected",
this.onWebRtcDisconnectedHandler
), this.onWebRtcPlayStreamErrorHandler = () => {
}, this.stream.addEventListener(
"playStreamError",
this.onWebRtcPlayStreamErrorHandler
), this.onWebRtcFailedEventHandler = () => {
this.readyStateManager.onWebRtcFailed(
"Failure reported by Streaming library."
);
}, this.stream.addEventListener(
"webRtcFailed",
this.onWebRtcFailedEventHandler
), this.stream.addEventListener(
"videoInitialized",
this.readyStateManager.onVideoInitialized
), this.stream.addEventListener(
"dataChannelOpen",
this.readyStateManager.onDataChannelOpen
), this.stream.addEventListener(
"streamerListMessage",
this.readyStateManager.onStreamerListMessage
));
}
unregisterEventHandlers() {
this.stream && (this.onWebRtcConnectedHandler && (this.stream.removeEventListener(
"webRtcConnected",
this.onWebRtcConnectedHandler
), this.onWebRtcConnectedHandler = null), this.onWebRtcDisconnectedHandler && (this.stream.removeEventListener(
"webRtcDisconnected",
this.onWebRtcDisconnectedHandler
), this.onWebRtcDisconnectedHandler = null), this.onWebRtcPlayStreamErrorHandler && (this.stream.removeEventListener(
"playStreamError",
this.onWebRtcPlayStreamErrorHandler
), this.onWebRtcPlayStreamErrorHandler = null), this.onWebRtcFailedEventHandler && (this.stream.removeEventListener(
"webRtcFailed",
this.onWebRtcFailedEventHandler
), this.onWebRtcFailedEventHandler = null), this.stream.removeEventListener(
"videoInitialized",
this.readyStateManager.onVideoInitialized
), this.stream.removeEventListener(
"dataChannelOpen",
this.readyStateManager.onDataChannelOpen
), this.stream.removeEventListener(
"streamerListMessage",
this.readyStateManager.onStreamerListMessage
));
}
setupLockedAudio(e) {
const i = e._webRtcController?.streamController?.audioElement;
if (!i)
return;
const n = i.srcObject;
if (!n)
return;
const o = n.getAudioTracks();
o.length > 0 && !this.initialAudioTrack && (this.initialAudioTrack = o[0], this.createLockedAudio(this.initialAudioTrack));
const a = (l) => {
l.track.kind === "audio" && !this.initialAudioTrack && (this.initialAudioTrack = l.track, this.createLockedAudio(this.initialAudioTrack));
};
n.addEventListener("addtrack", a);
}
createLockedAudio(e) {
typeof document > "u" || (this.lockedAudioElement = document.createElement("audio"), this.lockedAudioElement.autoplay = !0, this.lockedAudioElement.controls = !1, this.lockedAudioElement.muted = !1, this.lockedAudioElement.srcObject = new MediaStream([e]), document.body.appendChild(this.lockedAudioElement));
}
disconnect() {
if (!this.isDisconnecting)
if (this.isDisconnecting = !0, this.stream)
try {
this.reset(), setTimeout(() => {
this.performDisconnect();
}, 100);
} catch {
this.performDisconnect();
}
else
this.performDisconnect();
}
performDisconnect() {
if (this.readyStateManager.reset(), this.reconnectTimeout && (clearTimeout(this.reconnectTimeout), this.reconnectTimeout = null), this.initialAudioTrack) {
try {
this.initialAudioTrack.stop();
} catch {
}
this.initialAudioTrack = null;
}
if (this.lockedAudioElement && (this.lockedAudioElement.srcObject = null, this.lockedAudioElement.remove(), this.lockedAudioElement = null), this.keepAliveInterval && (clearInterval(this.keepAliveInterval), this.keepAliveInterval = null), this.uiService.getResizeHandler() && typeof window < "u" && (window.removeEventListener("resize", this.uiService.getResizeHandler()), this.uiService.resetResizeHandler()), this.stream && (this.unregisterEventHandlers(), this.stream.disconnect(), this.stream.webSocketController?.webSocket && this.stream.webSocketController.webSocket.close(), this.stream = null), this.application)
try {
const e = this.uiService.getContainer();
e && this.application.rootElement && e.removeChild(this.application.rootElement), this.application = null;
} catch {
}
this.uiService.setContainer(null), this.setStatus(c.DISCONNECTED), this.notifyDisconnectListeners(), this.cleanupEventListeners(), this.isDisconnecting = !1, this.reconnectAttempts = 0, this.hasFailedConnection = !1, this.hasSentResetMessage = !1;
}
async reconnect() {
this.eventBus.emit({
type: h.STREAMING_RECONNECTING,
timestamp: Date.now()
});
}
scheduleReconnect() {
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
this.setStatus(c.FAILED);
return;
}
const e = Math.pow(2, this.reconnectAttempts) * 1e3;
this.reconnectTimeout = setTimeout(async () => {
this.reconnectAttempts++, this.setStatus(c.CONNECTING);
try {
this.signalingServerUrl && await this.connect(this.signalingServerUrl, this.config);
} catch {
this.scheduleReconnect();
}
}, e);
}
setStatus(e) {
if (this.currentStatus === e)
return;
this.currentStatus = e;
const t = this.getConnectionEventType(e);
this.eventBus.emit({
type: t,
timestamp: Date.now(),
data: { status: e }
}), this.statusListeners.forEach((i, n) => {
try {
`${n}`, i(e);
} catch {
}
});
}
getConnectionEventType(e) {
switch (e) {
case c.CONNECTED:
return h.STREAMING_CONNECTED;
case c.DISCONNECTED:
return h.STREAMING_DISCONNECTED;
case c.CONNECTING:
return h.STREAMING_CONNECTING;
case c.ALLOCATING:
return h.STREAMING_ALLOCATING;
case c.FAILED:
return h.STREAMING_CONNECTION_FAILED;
default:
return h.STREAMING_DISCONNECTED;
}
}
notifyDisconnectListeners() {
this.disconnectListeners.forEach((e) => {
try {
e();
} catch {
}
});
}
updateInputOptions(e, t) {
this.stream && (this.stream.config.setFlagEnabled("MouseInput", e), this.stream.config.setFlagEnabled("KeyboardInput", t)), this.config.setMouseEnabled = e, this.config.setKeyboardEnabled = t;
}
// Status and listener management
getStatus() {
return this.currentStatus;
}
getStream() {
return this.stream;
}
getUIService() {
return this.uiService;
}
addStatusListener(e) {
this.statusListeners.push(e), e(this.currentStatus);
}
removeStatusListener(e) {
this.statusListeners = this.statusListeners.filter((t) => t !== e);
}
addDisconnectListener(e) {
this.disconnectListeners.push(e);
}
removeDisconnectListener(e) {
this.disconnectListeners = this.disconnectListeners.filter(
(t) => t !== e
);
}
addResponseListener(e) {
this.stream && this.stream.addResponseEventListener("handle_responses", e);
}
removeResponseListener(e) {
this.stream && this.stream.removeResponseEventListener("handle_responses");
}
};
s(k, "instance", null);
let de = k;
const T = class T {
constructor() {
s(this, "connectionService");
s(this, "mediaService");
s(this, "uiService");
this.connectionService = de.getInstance(), this.mediaService = Z.getInstance(), this.uiService = ee.getInstance(), this.setupServiceCoordination();
}
static getInstance() {
return T.instance || (T.instance = new T()), T.instance;
}
setupServiceCoordination() {
this.connectionService.addStatusListener((e) => {
const t = this.connectionService.getStream();
e === c.CONNECTED && t ? (this.mediaService.setStream(t), this.uiService.setStream(t)) : e === c.DISCONNECTED && (this.mediaService.setStream(null), this.uiService.setStream(null));
}), this.connectionService.addDisconnectListener(() => {
this.mediaService.cleanup();
});
}
// Connection Management (delegated to connection service)
setContainer(e) {
this.connectionService.setContainer(e);
}
forceResetContainer(e) {
this.connectionService.forceResetContainer(e);
}
async connect(e, t = {}) {
const i = {
setKeyboardEnabled: t.setKeyboardEnabled,
setMouseEnabled: t.setMouseEnabled,
initStreamingConfig: t.initStreamingConfig
};
return this.connectionService.connect(e, i);
}
disconnect() {
this.connectionService.disconnect(), this.mediaService.cleanup(), this.uiService.cleanup();
}
async reconnect() {
return this.connectionService.reconnect();
}
updateInputOptions(e, t) {
this.connectionService.updateInputOptions(e, t);
}
// Status and Stream Access (delegated to connection service)
getStatus() {
return this.connectionService.getStatus();
}
getStream() {
return this.connectionService.getStream();
}
getApplication() {
return this.connectionService.application;
}
getConnectionService() {
return this.connectionService;
}
getContainer() {
return this.uiService.getContainer();
}
addStatusListener(e) {
this.connectionService.addStatusListener(e);
}
removeStatusListener(e) {
this.connectionService.removeStatusListener(e);
}
addDisconnectListener(e) {
this.connectionService.addDisconnectListener(e);
}
removeDisconnectListener(e) {
this.connectionService.removeDisconnectListener(e);
}
// Media Operations (delegated to media service)
ttsInteractions(e, t, i, n) {
this.mediaService.ttsInteractions(
e,
t,
i,
n
);
}
sendStartConversation() {
this.mediaService.sendStartConversation();
}
endConversation() {
this.mediaService.endConversation();
}
autoPlayVideo() {
this.mediaService.autoPlayVideo();
}
play() {
this.mediaService.play();
}
// Ready State Management (delegated to media service)
isReadyToSendMessages() {
return this.mediaService.isReadyToSendMessages();
}
resetIsReadyToSend() {
this.mediaService.resetIsReadyToSend();
}
resetIsStreamReady() {
this.mediaService.resetIsStreamReady();
}
addIsReadyToSendListener(e) {
this.mediaService.addIsReadyToSendListener(e);
}
removeIsReadyToSendListener(e) {
this.mediaService.removeIsReadyToSendListener(e);
}
// UI Operations (delegated to UI service)
// public setCurrentPageName(pageName: string | null): void {
// this.uiService.setCurrentPageName(pageName);
// }
resize(e, t, i) {
this.uiService.resize(e, t, i);
}
enteredFullpage() {
this.uiService.enteredFullpage();
}
exitedFullpage(e) {
this.uiService.exitedFullpage(e);
}
sendAvatarNum(e) {
this.uiService.sendAvatarNum(e);
}
sendAvatarAppearanceChange(e) {
this.uiService.sendAvatarAppearanceChange(e);
}
generateDigitalHuman(e, t) {
this.uiService.generateDigitalHuman(e, t);
}
sendFailedGenerateAvatarHead() {
this.uiService.sendFailedGenerateAvatarHead();
}
sendPlaceBackgroundChange(e) {
this.uiService.sendPlaceBackgroundChange(e);
}
sendPageSetting(e, t) {
this.uiService.sendPageSetting(e, t);
}
sendVoiceSetting(e) {
this.uiService.sendVoiceSetting(e);
}
// Audio Control (delegated to media service)
setVolume(e) {
this.mediaService.setVolume(e);
}
getVolume() {
return this.mediaService.getVolume();
}
sendZoomIn() {
this.mediaService.sendZoomIn();
}
sendZoomOut() {
this.mediaService.sendZoomOut();
}
reset() {
this.uiService.reset();
}
// High-level convenience methods
async handleResponseFunction(e) {
return this.mediaService.handleResponseFunction(e);
}
// Legacy compatibility method (marked as private in UI service)
// Legacy compatibility method - delegates to UI service
emitUIInteraction(e) {
this.uiService.emitUIInteraction(e);
}
// Cleanup and shutdown
cleanup() {
this.disconnect();
}
// Static cleanup for singleton
static destroyInstance() {
T.instance && (T.instance.cleanup(), T.instance = null);
}
};
s(T, "instance", null);
let x = T;
const L = class L {
constructor() {
s(this, "autoDisconnectTimer", null);
s(this, "idleTimer", null);
s(this, "IDLE_TIMEOUT", 300 * 1e3);
// 5분 무활동 감지
// private readonly AUTO_DISCONNECT_INTERVAL = 10 * 60 * 1000; // 10분 자리 비움 후 자동 종료
// private readonly IDLE_TIMEOUT = 3 * 1000; // 3초 무활동 감지
s(this, "AUTO_DISCONNECT_INTERVAL", 5 * 1e3);
// 5초 자리 비움 후 자동 종료
s(this, "userActivityEventsRegistered", !1);
s(this, "eventBus");
s(this, "userEvents", [
// 기본 마우스/키보드 이벤트
"mousemove",
"keydown",
"click",
"scroll",
"wheel",
"contextmenu",
"focus",
// 터치 이벤트
"touchstart",
"touchmove",
"touchend",
"touchcancel",
// 통합 포인터 이벤트 (터치 + 마우스)
"pointerdown",
"pointermove",
"pointerup",
"pointercancel",
// 드래그 & 드롭
"dragstart",
"dragend",
"drop",
// 폼 상호작용
"input",
"change",
"submit",
// 클립보드/텍스트 선택
"paste",
"copy",
"cut",
"select",
"selectstart",
"selectionchange",
// 모바일 특화 이벤트
"gesturestart",
"gesturechange",
"gestureend",
"orientationchange"
]);
// throttle을 적용한 사용자 활동 핸들러 (200ms 간격으로 최대 한 번씩만 실행)
s(this, "userEventHandler");
// 바인딩된 메서드 참조들 (메모리 누수 방지를 위해 생성자에서 한 번만 바인딩)
s(this, "boundHandleVisibilityChange");
s(this, "boundHandleBeforeUnload");
this.eventBus = b.getInstance(), this.userEventHand