UNPKG

@klever-one/web-sdk

Version:

Web SDK for integrating real-time room management and streaming functionality into web applications

1,521 lines 147 kB
"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