@fuzionx/player 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
- var u=[{urls:"stun:stun.l.google.com:19302"},{urls:"stun:stun1.l.google.com:19302"}],r={JOIN:"join",OFFER:"offer",ANSWER:"answer",CANDIDATE:"candidate",PLI:"pli",LEAVE:"leave",SLOT_INFO:"slot_info",CHAT:"chat",ERROR:"error"},l={BROADCAST:"broadcast",VIDEOCHAT:"videochat"},f={[l.BROADCAST]:1,[l.VIDEOCHAT]:9},_={MAX_RETRIES:5,BASE_DELAY_MS:1e3,MAX_DELAY_MS:3e4},C={VIDEO_MIME:"video/H264",AUDIO_MIME:"audio/opus",VIDEO_CLOCK:9e4,AUDIO_CLOCK:48e3};var d=class{constructor(e){this.url=e.url,this.onMessage=e.onMessage||(()=>{}),this.onOpen=e.onOpen||(()=>{}),this.onClose=e.onClose||(()=>{}),this.onError=e.onError||(()=>{}),this.autoReconnect=e.autoReconnect!==!1,this._ws=null,this._retryCount=0,this._reconnectTimer=null,this._intentionalClose=!1}connect(){this._intentionalClose=!1,this._doConnect()}_doConnect(){try{this._ws=new WebSocket(this.url)}catch(e){this.onError(e),this._scheduleReconnect();return}this._ws.onopen=()=>{this._retryCount=0,this.onOpen()},this._ws.onmessage=e=>{try{let t=JSON.parse(e.data);this.onMessage(t)}catch{console.warn("[FuzionX] Invalid JSON:",e.data)}},this._ws.onclose=e=>{this.onClose(e),!this._intentionalClose&&this.autoReconnect&&this._scheduleReconnect()},this._ws.onerror=e=>{this.onError(e)}}send(e){return this._ws&&this._ws.readyState===WebSocket.OPEN?(this._ws.send(JSON.stringify(e)),!0):!1}sendJoin(e,t,c={}){return this.send({type:r.JOIN,peer_id:e,channel_id:t,nickname:c.nickname||null,token:c.token||null,mode:c.mode||null})}sendOffer(e){return this.send({type:r.OFFER,sdp:e})}sendAnswer(e){return this.send({type:r.ANSWER,sdp:e})}sendCandidate(e){return this.send({type:r.CANDIDATE,candidate:e.candidate,sdp_mid:e.sdpMid,sdp_m_line_index:e.sdpMLineIndex})}sendChat(e,t){return this.send({type:r.CHAT,text:e,nickname:t||null,peer_id:null})}sendPLI(){return this.send({type:r.PLI})}sendLeave(){return this.send({type:r.LEAVE})}disconnect(){this._intentionalClose=!0,clearTimeout(this._reconnectTimer),this._ws&&(this._ws.close(),this._ws=null)}get connected(){return this._ws&&this._ws.readyState===WebSocket.OPEN}_scheduleReconnect(){if(this._retryCount>=_.MAX_RETRIES){console.error("[FuzionX] Max reconnect retries reached."),this.onError(new Error("Max reconnect retries"));return}let e=Math.min(_.BASE_DELAY_MS*Math.pow(2,this._retryCount),_.MAX_DELAY_MS);this._retryCount++,console.log(`[FuzionX] Reconnecting in ${e}ms (${this._retryCount}/${_.MAX_RETRIES})`),this._reconnectTimer=setTimeout(()=>this._doConnect(),e)}};var g=class p{constructor(e){this.url=e.url,this.channelId=e.channelId,this.mode=e.mode||l.BROADCAST,this.nickname=e.nickname||null,this.token=e.token||null,this.peerId=e.peerId||`viewer-${Math.random().toString(36).slice(2,10)}`,this.autoReconnect=e.autoReconnect!==!1,this.rtcConfig=e.rtcConfig||{iceServers:u,bundlePolicy:"max-bundle",rtcpMuxPolicy:"require"},this._signaling=null,this._pc=null,this._listeners={},this._slots=new Map,this._maxSlots=f[this.mode]||1,this._candidateQueue=[],this._connected=!1}on(e,t){return this._listeners[e]||(this._listeners[e]=[]),this._listeners[e].push(t),this}_emit(e,...t){(this._listeners[e]||[]).forEach(c=>c(...t))}connect(){this._signaling=new d({url:this.url,autoReconnect:this.autoReconnect,onOpen:()=>this._onSignalingOpen(),onMessage:e=>this._onSignalingMessage(e),onClose:e=>this._onSignalingClose(e),onError:e=>this._emit("error",e)}),this._signaling.connect()}disconnect(){this._signaling&&(this._signaling.sendLeave(),this._signaling.disconnect()),this._closePeerConnection(),this._slots.clear(),this._connected=!1}chat(e){this._signaling&&this._signaling.sendChat(e,this.nickname)}requestKeyframe(){this._signaling&&this._signaling.sendPLI()}get slots(){return this._slots}_onSignalingOpen(){this._signaling.sendJoin(this.peerId,this.channelId,{nickname:this.nickname,token:this.token,mode:this.mode}),this._createPeerConnection().catch(e=>this._emit("error",e))}_onSignalingMessage(e){switch(e.type){case r.ANSWER:this._handleAnswer(e);break;case r.CANDIDATE:this._handleCandidate(e);break;case r.SLOT_INFO:this._handleSlotInfo(e);break;case r.CHAT:this._emit("chat",{peerId:e.peer_id,nickname:e.nickname,text:e.text});break;case r.ERROR:this._emit("error",new Error(e.message));break}}_onSignalingClose(e){this._closePeerConnection(),this._connected=!1,this._emit("close",e)}async _handleAnswer(e){if(this._pc)try{await this._pc.setRemoteDescription(new RTCSessionDescription({type:"answer",sdp:e.sdp}));for(let t of this._candidateQueue)await this._pc.addIceCandidate(t);this._candidateQueue=[]}catch(t){this._emit("error",t)}}async _handleCandidate(e){let t=new RTCIceCandidate({candidate:e.candidate,sdpMid:e.sdp_mid,sdpMLineIndex:e.sdp_m_line_index});if(this._pc&&this._pc.remoteDescription)try{await this._pc.addIceCandidate(t)}catch(c){console.warn("[FuzionX] ICE candidate error:",c)}else this._candidateQueue.push(t)}_handleSlotInfo(e){let t=parseInt(e.stream_id.replace("stream_",""),10);if(!e.nickname||e.nickname===""){this._slots.delete(t),this._emit("slot_remove",{slotIndex:t,senderId:e.sender_id});return}let c=this._slots.get(t)||{};this._slots.set(t,{...c,slotIndex:t,streamId:e.stream_id,nickname:e.nickname,senderId:e.sender_id}),this._emit("slot",this._slots.get(t))}async _createPeerConnection(){this._pc=new RTCPeerConnection(this.rtcConfig);let e=0;this._pc.ontrack=a=>{let n=a.track;if(n.kind==="video"){let i=e++,s=a.streams[0];s||(s=new MediaStream,s.addTrack(n));let o=this._slots.get(i)||{slotIndex:i};o.stream=s,this._slots.set(i,o),this._emit("stream",s,i),this._connected||(this._connected=!0,this._emit("connected"))}},this._pc.onconnectionstatechange=()=>{let a=this._pc?.connectionState;(a==="failed"||a==="disconnected")&&this._emit("error",new Error(`PeerConnection ${a}`))};for(let a=0;a<this._maxSlots;a++)this._pc.addTransceiver("video",{direction:"recvonly"}),this._pc.addTransceiver("audio",{direction:"recvonly"});let t=await this._pc.createOffer();t.sdp=p._forceCodecs(t.sdp),await this._pc.setLocalDescription(t),await this._waitForIceGathering();let c=this._pc.localDescription?.sdp;c&&this._signaling.sendOffer(c)}_waitForIceGathering(){return new Promise(e=>{if(this._pc.iceGatheringState==="complete")return e();let t=()=>{this._pc?.iceGatheringState==="complete"&&(this._pc.removeEventListener("icegatheringstatechange",t),e())};this._pc.addEventListener("icegatheringstatechange",t),setTimeout(()=>{this._pc&&this._pc.removeEventListener("icegatheringstatechange",t),e()},3e3)})}static _forceCodecs(e){let t=e.split(`\r
2
- `),c=t.findIndex(n=>n.startsWith("m=video"));if(c!==-1){let n=[],i=new Map;if(t.forEach(s=>{let o=s.match(/a=rtpmap:(\d+) H264\/90000/);o&&(n.push(o[1]),i.set(o[1],0))}),t.forEach(s=>{if(s.startsWith("a=fmtp:")){let o=s.split(" ")[0].split(":")[1];i.has(o)&&(s.includes("profile-level-id=42e01f")?i.set(o,100):s.includes("profile-level-id=42001f")&&i.set(o,80),s.includes("packetization-mode=1")&&i.set(o,(i.get(o)||0)+10))}}),n.length>0){n.sort((h,m)=>i.get(m)-i.get(h));let s=t[c].split(" "),o=s.slice(3).filter(h=>!n.includes(h));t[c]=[...s.slice(0,3),...n,...o].join(" ")}}let a=t.findIndex(n=>n.startsWith("m=audio"));if(a!==-1){let n=[];if(t.forEach(i=>{let s=i.match(/a=rtpmap:(\d+) opus\/48000/);s&&n.push(s[1])}),n.length>0){let i=t[a].split(" "),s=i.slice(3).filter(o=>!n.includes(o));t[a]=[...i.slice(0,3),...n,...s].join(" ")}}return t.join(`\r
3
- `)}_closePeerConnection(){this._pc&&(this._pc.close(),this._pc=null),this._candidateQueue=[]}};var S=class p{constructor(e){this.whipUrl=e.whipUrl||null,this.url=e.url||null,this.channelId=e.channelId||null,this.mode=e.mode||l.BROADCAST,this.nickname=e.nickname||null,this.token=e.token||null,this.peerId=e.peerId||`pub-${Math.random().toString(36).slice(2,10)}`,this.autoReconnect=e.autoReconnect!==!1,this.mediaConstraints=e.media||{video:!0,audio:!0},this._externalStream=e.stream||null,this.rtcConfig=e.rtcConfig||{iceServers:u,bundlePolicy:"max-bundle",rtcpMuxPolicy:"require"},this._signaling=null,this._pc=null,this._localStream=null,this._listeners={},this._candidateQueue=[],this._whipResourceUrl=null,this._maxSlots=f[this.mode]||1,this._slots=new Map,this._connected=!1}on(e,t){return this._listeners[e]||(this._listeners[e]=[]),this._listeners[e].push(t),this}_emit(e,...t){(this._listeners[e]||[]).forEach(c=>c(...t))}async connect(){try{if(await this._acquireMedia(),this.whipUrl)await this._connectWhip();else if(this.url)this._connectWebSocket();else throw new Error("url \uB610\uB294 whipUrl \uC911 \uD558\uB098\uB97C \uC9C0\uC815\uD574\uC57C \uD569\uB2C8\uB2E4.")}catch(e){this._emit("error",e)}}async disconnect(){if(this.whipUrl&&this._whipResourceUrl){try{let e=new URL(this.whipUrl).origin;await fetch(`${e}${this._whipResourceUrl}`,{method:"DELETE"})}catch(e){console.warn("[FuzionX] WHIP DELETE error:",e)}this._whipResourceUrl=null}this._signaling&&(this._signaling.sendLeave(),this._signaling.disconnect()),this._closePeerConnection(),this._stopMedia(),this._connected=!1}chat(e){this._signaling&&this._signaling.sendChat(e,this.nickname)}get localStream(){return this._localStream}get slots(){return this._slots}async _acquireMedia(){this._externalStream?this._localStream=this._externalStream:this._localStream=await navigator.mediaDevices.getUserMedia(this.mediaConstraints),this._emit("media",this._localStream)}_stopMedia(){this._localStream&&!this._externalStream&&this._localStream.getTracks().forEach(e=>e.stop()),this._localStream=null}async _connectWhip(){this._pc=new RTCPeerConnection(this.rtcConfig),this._localStream.getTracks().forEach(i=>{this._pc.addTrack(i,this._localStream)});let e=await this._pc.createOffer();e.sdp=p._forceCodecs(e.sdp),await this._pc.setLocalDescription(e),await new Promise(i=>{this._pc.iceGatheringState==="complete"?i():(this._pc.onicegatheringstatechange=()=>{this._pc.iceGatheringState==="complete"&&i()},setTimeout(i,3e3))});let t=this._pc.localDescription,c=this.whipUrl;this.token&&!c.includes("token=")&&(c+=(c.includes("?")?"&":"?")+`token=${this.token}`);let a=await fetch(c,{method:"POST",headers:{"Content-Type":"application/sdp"},body:t.sdp});if(a.status!==201)throw new Error(`WHIP failed: ${a.status} ${await a.text()}`);let n=await a.text();this._whipResourceUrl=a.headers.get("location"),await this._pc.setRemoteDescription(new RTCSessionDescription({type:"answer",sdp:n})),this._pc.onconnectionstatechange=()=>{let i=this._pc?.connectionState;i==="connected"?(this._connected=!0,this._emit("ready")):(i==="failed"||i==="disconnected")&&this._emit("error",new Error(`WHIP PeerConnection ${i}`))},this._emit("ready")}_connectWebSocket(){this._signaling=new d({url:this.url,autoReconnect:this.autoReconnect,onOpen:()=>this._onSignalingOpen(),onMessage:e=>this._onSignalingMessage(e),onClose:e=>{this._closePeerConnection(),this._connected=!1,this._emit("close",e)},onError:e=>this._emit("error",e)}),this._signaling.connect()}async _onSignalingOpen(){this._signaling.sendJoin(this.peerId,this.channelId,{nickname:this.nickname,token:this.token,mode:this.mode}),await this._createPeerConnection()}async _createPeerConnection(){if(this._pc=new RTCPeerConnection(this.rtcConfig),this._localStream.getTracks().forEach(c=>{this._pc.addTrack(c,this._localStream)}),this.mode===l.VIDEOCHAT){for(let n=0;n<this._maxSlots;n++)this._pc.addTransceiver("video",{direction:"recvonly"}),this._pc.addTransceiver("audio",{direction:"recvonly"});this._pc.getTransceivers().forEach(n=>{n.sender.track&&n.direction==="recvonly"&&(n.direction="sendrecv")});let a=0;this._pc.ontrack=n=>{let i=n.track;if(i.kind==="video"){let s=a++,o=n.streams[0];o||(o=new MediaStream,o.addTrack(i));let h=this._slots.get(s)||{slotIndex:s};h.stream=o,this._slots.set(s,h),this._emit("stream",o,s)}}}this._pc.onconnectionstatechange=()=>{let c=this._pc?.connectionState;c==="connected"&&!this._connected?(this._connected=!0,this._emit("ready")):(c==="failed"||c==="disconnected")&&this._emit("error",new Error(`PeerConnection ${c}`))};let e=await this._pc.createOffer();e.sdp=p._forceCodecs(e.sdp),await this._pc.setLocalDescription(e),await this._waitForIceGathering();let t=this._pc.localDescription?.sdp;t&&this._signaling.sendOffer(t)}_waitForIceGathering(){return new Promise(e=>{if(this._pc.iceGatheringState==="complete")return e();let t=()=>{this._pc?.iceGatheringState==="complete"&&(this._pc.removeEventListener("icegatheringstatechange",t),e())};this._pc.addEventListener("icegatheringstatechange",t),setTimeout(()=>{this._pc&&this._pc.removeEventListener("icegatheringstatechange",t),e()},3e3)})}static _forceCodecs(e){let t=e.split(`\r
4
- `),c=t.findIndex(n=>n.startsWith("m=video"));if(c!==-1){let n=[],i=new Map;if(t.forEach(s=>{let o=s.match(/a=rtpmap:(\d+) H264\/90000/);o&&(n.push(o[1]),i.set(o[1],0))}),t.forEach(s=>{if(s.startsWith("a=fmtp:")){let o=s.split(" ")[0].split(":")[1];i.has(o)&&(s.includes("profile-level-id=42e01f")?i.set(o,100):s.includes("profile-level-id=42001f")&&i.set(o,80),s.includes("packetization-mode=1")&&i.set(o,(i.get(o)||0)+10))}}),n.length>0){n.sort((h,m)=>i.get(m)-i.get(h));let s=t[c].split(" "),o=s.slice(3).filter(h=>!n.includes(h));t[c]=[...s.slice(0,3),...n,...o].join(" ")}}let a=t.findIndex(n=>n.startsWith("m=audio"));if(a!==-1){let n=[];if(t.forEach(i=>{let s=i.match(/a=rtpmap:(\d+) opus\/48000/);s&&n.push(s[1])}),n.length>0){let i=t[a].split(" "),s=i.slice(3).filter(o=>!n.includes(o));t[a]=[...i.slice(0,3),...n,...s].join(" ")}}return t.join(`\r
5
- `)}_onSignalingMessage(e){switch(e.type){case r.ANSWER:this._handleAnswer(e);break;case r.CANDIDATE:this._handleCandidate(e);break;case r.SLOT_INFO:this._handleSlotInfo(e);break;case r.CHAT:this._emit("chat",{peerId:e.peer_id,nickname:e.nickname,text:e.text});break;case r.ERROR:this._emit("error",new Error(e.message));break}}async _handleAnswer(e){if(this._pc)try{await this._pc.setRemoteDescription(new RTCSessionDescription({type:"answer",sdp:e.sdp}));for(let t of this._candidateQueue)await this._pc.addIceCandidate(t);this._candidateQueue=[]}catch(t){this._emit("error",t)}}async _handleCandidate(e){let t=new RTCIceCandidate({candidate:e.candidate,sdpMid:e.sdp_mid,sdpMLineIndex:e.sdp_m_line_index});if(this._pc&&this._pc.remoteDescription)try{await this._pc.addIceCandidate(t)}catch{}else this._candidateQueue.push(t)}_handleSlotInfo(e){let t=parseInt(e.stream_id.replace("stream_",""),10);if(!e.nickname||e.nickname===""){this._slots.delete(t),this._emit("slot_remove",{slotIndex:t,senderId:e.sender_id});return}let c=this._slots.get(t)||{};this._slots.set(t,{...c,slotIndex:t,streamId:e.stream_id,nickname:e.nickname,senderId:e.sender_id}),this._emit("slot",this._slots.get(t))}_closePeerConnection(){this._pc&&(this._pc.close(),this._pc=null),this._candidateQueue=[]}};export{C as CODEC,u as DEFAULT_ICE_SERVERS,S as FuzionXPublisher,d as FuzionXSignaling,g as FuzionXViewer,f as MAX_SLOTS,_ as RECONNECT,l as SessionMode,r as SignalType};
1
+ var u=[{urls:"stun:stun.l.google.com:19302"},{urls:"stun:stun1.l.google.com:19302"}],a={JOIN:"join",OFFER:"offer",ANSWER:"answer",CANDIDATE:"candidate",PLI:"pli",LEAVE:"leave",SLOT_INFO:"slot_info",CHAT:"chat",ERROR:"error"},l={BROADCAST:"broadcast",VIDEOCHAT:"videochat"},f={[l.BROADCAST]:1,[l.VIDEOCHAT]:9},_={MAX_RETRIES:5,BASE_DELAY_MS:1e3,MAX_DELAY_MS:3e4},S={VIDEO_MIME:"video/H264",AUDIO_MIME:"audio/opus",VIDEO_CLOCK:9e4,AUDIO_CLOCK:48e3};var d=class{constructor(e){this.url=e.url,this.onMessage=e.onMessage||(()=>{}),this.onOpen=e.onOpen||(()=>{}),this.onClose=e.onClose||(()=>{}),this.onError=e.onError||(()=>{}),this.autoReconnect=e.autoReconnect!==!1,this._ws=null,this._retryCount=0,this._reconnectTimer=null,this._intentionalClose=!1}connect(){this._intentionalClose=!1,this._doConnect()}_doConnect(){try{this._ws=new WebSocket(this.url)}catch(e){this.onError(e),this._scheduleReconnect();return}this._ws.onopen=()=>{this._retryCount=0,this.onOpen()},this._ws.onmessage=e=>{try{let t=JSON.parse(e.data);this.onMessage(t)}catch{console.warn("[FuzionX] Invalid JSON:",e.data)}},this._ws.onclose=e=>{this.onClose(e),!this._intentionalClose&&this.autoReconnect&&this._scheduleReconnect()},this._ws.onerror=e=>{this.onError(e)}}send(e){return this._ws&&this._ws.readyState===WebSocket.OPEN?(this._ws.send(JSON.stringify(e)),!0):!1}sendJoin(e,t,s={}){return this.send({type:a.JOIN,peer_id:e,channel_id:t,nickname:s.nickname||null,token:s.token||null,mode:s.mode||null})}sendOffer(e){return this.send({type:a.OFFER,sdp:e})}sendAnswer(e){return this.send({type:a.ANSWER,sdp:e})}sendCandidate(e){return this.send({type:a.CANDIDATE,candidate:e.candidate,sdp_mid:e.sdpMid,sdp_m_line_index:e.sdpMLineIndex})}sendChat(e,t){return this.send({type:a.CHAT,text:e,nickname:t||null,peer_id:null})}sendPLI(){return this.send({type:a.PLI})}sendLeave(){return this.send({type:a.LEAVE})}disconnect(){this._intentionalClose=!0,clearTimeout(this._reconnectTimer),this._ws&&(this._ws.close(),this._ws=null)}get connected(){return this._ws&&this._ws.readyState===WebSocket.OPEN}_scheduleReconnect(){if(this._retryCount>=_.MAX_RETRIES){console.error("[FuzionX] Max reconnect retries reached."),this.onError(new Error("Max reconnect retries"));return}let e=Math.min(_.BASE_DELAY_MS*Math.pow(2,this._retryCount),_.MAX_DELAY_MS);this._retryCount++,console.log(`[FuzionX] Reconnecting in ${e}ms (${this._retryCount}/${_.MAX_RETRIES})`),this._reconnectTimer=setTimeout(()=>this._doConnect(),e)}};var g=class p{constructor(e){this.url=e.url||null,this.hubUrl=e.hubUrl||null,this.channelId=e.channelId,this.mode=e.mode||l.BROADCAST,this.nickname=e.nickname||null,this.token=e.token||null,this.peerId=e.peerId||`viewer-${Math.random().toString(36).slice(2,10)}`,this.autoReconnect=e.autoReconnect!==!1,this.rtcConfig=e.rtcConfig||{iceServers:u,bundlePolicy:"max-bundle",rtcpMuxPolicy:"require"},this._signaling=null,this._pc=null,this._listeners={},this._slots=new Map,this._maxSlots=f[this.mode]||1,this._candidateQueue=[],this._connected=!1}on(e,t){return this._listeners[e]||(this._listeners[e]=[]),this._listeners[e].push(t),this}_emit(e,...t){(this._listeners[e]||[]).forEach(s=>s(...t))}async connect(){if(!this.url&&this.hubUrl)try{let e=await fetch(`${this.hubUrl}/api/channels/${this.channelId}`);if(!e.ok)throw new Error(`Channel not found: ${this.channelId}`);let t=await e.json();if(t.ws_url){let s=this.hubUrl.startsWith("https");this.url=t.ws_url.replace(/^ws(s?):/,s?"wss:":"ws:")}else{let o=this.hubUrl.startsWith("https")?"wss":"ws";this.url=`${o}://${t.media_ip}:${t.webrtc_port}`}}catch(e){this._emit("error",e);return}if(!this.url){this._emit("error",new Error("url \uB610\uB294 hubUrl\uC744 \uC9C0\uC815\uD574\uC57C \uD569\uB2C8\uB2E4."));return}this._signaling=new d({url:this.url,autoReconnect:this.autoReconnect,onOpen:()=>this._onSignalingOpen(),onMessage:e=>this._onSignalingMessage(e),onClose:e=>this._onSignalingClose(e),onError:e=>this._emit("error",e)}),this._signaling.connect()}disconnect(){this._signaling&&(this._signaling.sendLeave(),this._signaling.disconnect()),this._closePeerConnection(),this._slots.clear(),this._connected=!1}chat(e){this._signaling&&this._signaling.sendChat(e,this.nickname)}requestKeyframe(){this._signaling&&this._signaling.sendPLI()}get slots(){return this._slots}_onSignalingOpen(){this._signaling.sendJoin(this.peerId,this.channelId,{nickname:this.nickname,token:this.token,mode:this.mode}),this._createPeerConnection().catch(e=>this._emit("error",e))}_onSignalingMessage(e){switch(e.type){case a.ANSWER:this._handleAnswer(e);break;case a.CANDIDATE:this._handleCandidate(e);break;case a.SLOT_INFO:this._handleSlotInfo(e);break;case a.CHAT:this._emit("chat",{peerId:e.peer_id,nickname:e.nickname,text:e.text});break;case a.ERROR:this._emit("error",new Error(e.message));break}}_onSignalingClose(e){this._closePeerConnection(),this._connected=!1,this._emit("close",e)}async _handleAnswer(e){if(this._pc)try{await this._pc.setRemoteDescription(new RTCSessionDescription({type:"answer",sdp:e.sdp}));for(let t of this._candidateQueue)await this._pc.addIceCandidate(t);this._candidateQueue=[]}catch(t){this._emit("error",t)}}async _handleCandidate(e){let t=new RTCIceCandidate({candidate:e.candidate,sdpMid:e.sdp_mid,sdpMLineIndex:e.sdp_m_line_index});if(this._pc&&this._pc.remoteDescription)try{await this._pc.addIceCandidate(t)}catch(s){console.warn("[FuzionX] ICE candidate error:",s)}else this._candidateQueue.push(t)}_handleSlotInfo(e){let t=parseInt(e.stream_id.replace("stream_",""),10);if(!e.nickname||e.nickname===""){this._slots.delete(t),this._emit("slot_remove",{slotIndex:t,senderId:e.sender_id});return}let s=this._slots.get(t)||{};this._slots.set(t,{...s,slotIndex:t,streamId:e.stream_id,nickname:e.nickname,senderId:e.sender_id}),this._emit("slot",this._slots.get(t))}async _createPeerConnection(){this._pc=new RTCPeerConnection(this.rtcConfig);let e=0;this._pc.ontrack=o=>{let n=o.track;if(n.kind==="video"){let i=e++,r=o.streams[0];r||(r=new MediaStream,r.addTrack(n));let c=this._slots.get(i)||{slotIndex:i};c.stream=r,this._slots.set(i,c),this._emit("stream",r,i),this._connected||(this._connected=!0,this._emit("connected"),this._signaling&&this._signaling.sendPLI())}},this._pc.onconnectionstatechange=()=>{let o=this._pc?.connectionState;(o==="failed"||o==="disconnected")&&this._emit("error",new Error(`PeerConnection ${o}`))};for(let o=0;o<this._maxSlots;o++)this._pc.addTransceiver("video",{direction:"recvonly"}),this._pc.addTransceiver("audio",{direction:"recvonly"});let t=await this._pc.createOffer();t.sdp=p._forceCodecs(t.sdp),await this._pc.setLocalDescription(t),await this._waitForIceGathering();let s=this._pc.localDescription?.sdp;s&&this._signaling.sendOffer(s)}_waitForIceGathering(){return new Promise(e=>{if(this._pc.iceGatheringState==="complete")return e();let t=()=>{this._pc?.iceGatheringState==="complete"&&(this._pc.removeEventListener("icegatheringstatechange",t),e())};this._pc.addEventListener("icegatheringstatechange",t),setTimeout(()=>{this._pc&&this._pc.removeEventListener("icegatheringstatechange",t),e()},150)})}static _forceCodecs(e){let t=e.split(`\r
2
+ `),s=t.findIndex(n=>n.startsWith("m=video"));if(s!==-1){let n=[],i=new Map;if(t.forEach(r=>{let c=r.match(/a=rtpmap:(\d+) H264\/90000/);c&&(n.push(c[1]),i.set(c[1],0))}),t.forEach(r=>{if(r.startsWith("a=fmtp:")){let c=r.split(" ")[0].split(":")[1];i.has(c)&&(r.includes("profile-level-id=42e01f")?i.set(c,100):r.includes("profile-level-id=42001f")&&i.set(c,80),r.includes("packetization-mode=1")&&i.set(c,(i.get(c)||0)+10))}}),n.length>0){n.sort((h,m)=>i.get(m)-i.get(h));let r=t[s].split(" "),c=r.slice(3).filter(h=>!n.includes(h));t[s]=[...r.slice(0,3),...n,...c].join(" ")}}let o=t.findIndex(n=>n.startsWith("m=audio"));if(o!==-1){let n=[];if(t.forEach(i=>{let r=i.match(/a=rtpmap:(\d+) opus\/48000/);r&&n.push(r[1])}),n.length>0){let i=t[o].split(" "),r=i.slice(3).filter(c=>!n.includes(c));t[o]=[...i.slice(0,3),...n,...r].join(" ")}}return t.join(`\r
3
+ `)}_closePeerConnection(){this._pc&&(this._pc.close(),this._pc=null),this._candidateQueue=[]}};var w=class p{constructor(e){this.whipUrl=e.whipUrl||null,this.url=e.url||null,this.hubUrl=e.hubUrl||null,this.channelId=e.channelId||null,this.mode=e.mode||l.BROADCAST,this.nickname=e.nickname||null,this.token=e.token||null,this.peerId=e.peerId||`pub-${Math.random().toString(36).slice(2,10)}`,this.autoReconnect=e.autoReconnect!==!1,this.mediaConstraints=e.media||{video:!0,audio:!0},this._externalStream=e.stream||null,this.rtcConfig=e.rtcConfig||{iceServers:u,bundlePolicy:"max-bundle",rtcpMuxPolicy:"require"},this._signaling=null,this._pc=null,this._localStream=null,this._listeners={},this._candidateQueue=[],this._whipResourceUrl=null,this._maxSlots=f[this.mode]||1,this._slots=new Map,this._connected=!1}on(e,t){return this._listeners[e]||(this._listeners[e]=[]),this._listeners[e].push(t),this}_emit(e,...t){(this._listeners[e]||[]).forEach(s=>s(...t))}async connect(){try{if(!this.url&&!this.whipUrl&&this.hubUrl&&this.channelId){let e=await fetch(`${this.hubUrl}/api/channels`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({channel_id:this.channelId,source_type:"webrtc"})});if(e.status===409)throw new Error(`Channel already exists: ${this.channelId}`);if(!e.ok)throw new Error(`Failed to create channel: ${this.channelId}`);let t=await e.json();if(t.ws_url){let s=this.hubUrl.startsWith("https");this.url=t.ws_url.replace(/^ws(s?):/,s?"wss:":"ws:")}else{let o=this.hubUrl.startsWith("https")?"wss":"ws";this.url=`${o}://${t.media_ip}:${t.webrtc_port}`}}if(await this._acquireMedia(),this.whipUrl)await this._connectWhip();else if(this.url)this._connectWebSocket();else throw new Error("url, hubUrl, \uB610\uB294 whipUrl \uC911 \uD558\uB098\uB97C \uC9C0\uC815\uD574\uC57C \uD569\uB2C8\uB2E4.")}catch(e){this._emit("error",e)}}async disconnect(){if(this.whipUrl&&this._whipResourceUrl){try{let e=new URL(this.whipUrl).origin;await fetch(`${e}${this._whipResourceUrl}`,{method:"DELETE"})}catch(e){console.warn("[FuzionX] WHIP DELETE error:",e)}this._whipResourceUrl=null}this._signaling&&(this._signaling.sendLeave(),this._signaling.disconnect()),this._closePeerConnection(),this._stopMedia(),this._connected=!1}chat(e){this._signaling&&this._signaling.sendChat(e,this.nickname)}get localStream(){return this._localStream}get slots(){return this._slots}async _acquireMedia(){this._externalStream?this._localStream=this._externalStream:this._localStream=await navigator.mediaDevices.getUserMedia(this.mediaConstraints),this._emit("media",this._localStream)}_stopMedia(){this._localStream&&!this._externalStream&&this._localStream.getTracks().forEach(e=>e.stop()),this._localStream=null}async _connectWhip(){this._pc=new RTCPeerConnection(this.rtcConfig),this._localStream.getTracks().forEach(i=>{this._pc.addTrack(i,this._localStream)});let e=await this._pc.createOffer();e.sdp=p._forceCodecs(e.sdp),await this._pc.setLocalDescription(e),await new Promise(i=>{this._pc.iceGatheringState==="complete"?i():(this._pc.onicegatheringstatechange=()=>{this._pc.iceGatheringState==="complete"&&i()},setTimeout(i,150))});let t=this._pc.localDescription,s=this.whipUrl;this.token&&!s.includes("token=")&&(s+=(s.includes("?")?"&":"?")+`token=${this.token}`);let o=await fetch(s,{method:"POST",headers:{"Content-Type":"application/sdp"},body:t.sdp});if(o.status!==201)throw new Error(`WHIP failed: ${o.status} ${await o.text()}`);let n=await o.text();this._whipResourceUrl=o.headers.get("location"),await this._pc.setRemoteDescription(new RTCSessionDescription({type:"answer",sdp:n})),this._pc.onconnectionstatechange=()=>{let i=this._pc?.connectionState;i==="connected"?(this._connected=!0,this._emit("ready")):(i==="failed"||i==="disconnected")&&this._emit("error",new Error(`WHIP PeerConnection ${i}`))},this._emit("ready")}_connectWebSocket(){this._signaling=new d({url:this.url,autoReconnect:this.autoReconnect,onOpen:()=>this._onSignalingOpen(),onMessage:e=>this._onSignalingMessage(e),onClose:e=>{this._closePeerConnection(),this._connected=!1,this._emit("close",e)},onError:e=>this._emit("error",e)}),this._signaling.connect()}async _onSignalingOpen(){this._signaling.sendJoin(this.peerId,this.channelId,{nickname:this.nickname,token:this.token,mode:this.mode}),await this._createPeerConnection()}async _createPeerConnection(){if(this._pc=new RTCPeerConnection(this.rtcConfig),this._localStream.getTracks().forEach(s=>{this._pc.addTrack(s,this._localStream)}),this.mode===l.VIDEOCHAT){for(let n=0;n<this._maxSlots;n++)this._pc.addTransceiver("video",{direction:"recvonly"}),this._pc.addTransceiver("audio",{direction:"recvonly"});this._pc.getTransceivers().forEach(n=>{n.sender.track&&n.direction==="recvonly"&&(n.direction="sendrecv")});let o=0;this._pc.ontrack=n=>{let i=n.track;if(i.kind==="video"){let r=o++,c=n.streams[0];c||(c=new MediaStream,c.addTrack(i));let h=this._slots.get(r)||{slotIndex:r};h.stream=c,this._slots.set(r,h),this._emit("stream",c,r)}}}this._pc.onconnectionstatechange=()=>{let s=this._pc?.connectionState;s==="connected"&&!this._connected?(this._connected=!0,this._emit("ready")):(s==="failed"||s==="disconnected")&&this._emit("error",new Error(`PeerConnection ${s}`))};let e=await this._pc.createOffer();e.sdp=p._forceCodecs(e.sdp),await this._pc.setLocalDescription(e),await this._waitForIceGathering();let t=this._pc.localDescription?.sdp;t&&this._signaling.sendOffer(t)}_waitForIceGathering(){return new Promise(e=>{if(this._pc.iceGatheringState==="complete")return e();let t=()=>{this._pc?.iceGatheringState==="complete"&&(this._pc.removeEventListener("icegatheringstatechange",t),e())};this._pc.addEventListener("icegatheringstatechange",t),setTimeout(()=>{this._pc&&this._pc.removeEventListener("icegatheringstatechange",t),e()},150)})}static _forceCodecs(e){let t=e.split(`\r
4
+ `),s=t.findIndex(n=>n.startsWith("m=video"));if(s!==-1){let n=[],i=new Map;if(t.forEach(r=>{let c=r.match(/a=rtpmap:(\d+) H264\/90000/);c&&(n.push(c[1]),i.set(c[1],0))}),t.forEach(r=>{if(r.startsWith("a=fmtp:")){let c=r.split(" ")[0].split(":")[1];i.has(c)&&(r.includes("profile-level-id=42e01f")?i.set(c,100):r.includes("profile-level-id=42001f")&&i.set(c,80),r.includes("packetization-mode=1")&&i.set(c,(i.get(c)||0)+10))}}),n.length>0){n.sort((h,m)=>i.get(m)-i.get(h));let r=t[s].split(" "),c=r.slice(3).filter(h=>!n.includes(h));t[s]=[...r.slice(0,3),...n,...c].join(" ")}}let o=t.findIndex(n=>n.startsWith("m=audio"));if(o!==-1){let n=[];if(t.forEach(i=>{let r=i.match(/a=rtpmap:(\d+) opus\/48000/);r&&n.push(r[1])}),n.length>0){let i=t[o].split(" "),r=i.slice(3).filter(c=>!n.includes(c));t[o]=[...i.slice(0,3),...n,...r].join(" ")}}return t.join(`\r
5
+ `)}_onSignalingMessage(e){switch(e.type){case a.ANSWER:this._handleAnswer(e);break;case a.CANDIDATE:this._handleCandidate(e);break;case a.SLOT_INFO:this._handleSlotInfo(e);break;case a.CHAT:this._emit("chat",{peerId:e.peer_id,nickname:e.nickname,text:e.text});break;case a.ERROR:this._emit("error",new Error(e.message));break}}async _handleAnswer(e){if(this._pc)try{await this._pc.setRemoteDescription(new RTCSessionDescription({type:"answer",sdp:e.sdp}));for(let t of this._candidateQueue)await this._pc.addIceCandidate(t);this._candidateQueue=[]}catch(t){this._emit("error",t)}}async _handleCandidate(e){let t=new RTCIceCandidate({candidate:e.candidate,sdpMid:e.sdp_mid,sdpMLineIndex:e.sdp_m_line_index});if(this._pc&&this._pc.remoteDescription)try{await this._pc.addIceCandidate(t)}catch{}else this._candidateQueue.push(t)}_handleSlotInfo(e){let t=parseInt(e.stream_id.replace("stream_",""),10);if(!e.nickname||e.nickname===""){this._slots.delete(t),this._emit("slot_remove",{slotIndex:t,senderId:e.sender_id});return}let s=this._slots.get(t)||{};this._slots.set(t,{...s,slotIndex:t,streamId:e.stream_id,nickname:e.nickname,senderId:e.sender_id}),this._emit("slot",this._slots.get(t))}_closePeerConnection(){this._pc&&(this._pc.close(),this._pc=null),this._candidateQueue=[]}};export{S as CODEC,u as DEFAULT_ICE_SERVERS,w as FuzionXPublisher,d as FuzionXSignaling,g as FuzionXViewer,f as MAX_SLOTS,_ as RECONNECT,l as SessionMode,a as SignalType};
6
6
  //# sourceMappingURL=fuzionx-player.esm.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/constants.js", "../src/FuzionXSignaling.js", "../src/FuzionXViewer.js", "../src/FuzionXPublisher.js"],
4
- "sourcesContent": ["/**\n * @fuzionx/player \u2014 Constants & Default Configuration\n */\n\n/** Default ICE Servers */\nexport const DEFAULT_ICE_SERVERS = [\n { urls: 'stun:stun.l.google.com:19302' },\n { urls: 'stun:stun1.l.google.com:19302' },\n];\n\n/** Signaling message types (maps to server SignalMessage enum) */\nexport const SignalType = {\n JOIN: 'join',\n OFFER: 'offer',\n ANSWER: 'answer',\n CANDIDATE: 'candidate',\n PLI: 'pli',\n LEAVE: 'leave',\n SLOT_INFO: 'slot_info',\n CHAT: 'chat',\n ERROR: 'error',\n};\n\n/** Session modes */\nexport const SessionMode = {\n BROADCAST: 'broadcast',\n VIDEOCHAT: 'videochat',\n};\n\n/** Max slots per mode */\nexport const MAX_SLOTS = {\n [SessionMode.BROADCAST]: 1,\n [SessionMode.VIDEOCHAT]: 9,\n};\n\n/** Default reconnect settings */\nexport const RECONNECT = {\n MAX_RETRIES: 5,\n BASE_DELAY_MS: 1000,\n MAX_DELAY_MS: 30000,\n};\n\n/** Codec preferences */\nexport const CODEC = {\n VIDEO_MIME: 'video/H264',\n AUDIO_MIME: 'audio/opus',\n VIDEO_CLOCK: 90000,\n AUDIO_CLOCK: 48000,\n};\n", "/**\n * @fuzionx/player \u2014 WebSocket Signaling Layer\n *\n * FuzionX Media Server\uC758 JSON \uC2DC\uADF8\uB110\uB9C1 \uD504\uB85C\uD1A0\uCF5C \uCEA1\uC290\uD654.\n * \uC790\uB3D9 \uC7AC\uC5F0\uACB0 + \uC774\uBCA4\uD2B8 \uC2DC\uC2A4\uD15C.\n */\n\nimport { SignalType, RECONNECT } from './constants.js';\n\nexport class FuzionXSignaling {\n /**\n * @param {Object} opts\n * @param {string} opts.url - WebSocket URL (ws:// or wss://)\n * @param {Function} [opts.onMessage] - \uBA54\uC2DC\uC9C0 \uC218\uC2E0 \uCF5C\uBC31\n * @param {Function} [opts.onOpen] - \uC5F0\uACB0 \uC131\uACF5 \uCF5C\uBC31\n * @param {Function} [opts.onClose] - \uC5F0\uACB0 \uC885\uB8CC \uCF5C\uBC31\n * @param {Function} [opts.onError] - \uC5D0\uB7EC \uCF5C\uBC31\n * @param {boolean} [opts.autoReconnect=true]\n */\n constructor(opts) {\n this.url = opts.url;\n this.onMessage = opts.onMessage || (() => {});\n this.onOpen = opts.onOpen || (() => {});\n this.onClose = opts.onClose || (() => {});\n this.onError = opts.onError || (() => {});\n this.autoReconnect = opts.autoReconnect !== false;\n\n /** @private */\n this._ws = null;\n this._retryCount = 0;\n this._reconnectTimer = null;\n this._intentionalClose = false;\n }\n\n /** WebSocket \uC5F0\uACB0. */\n connect() {\n this._intentionalClose = false;\n this._doConnect();\n }\n\n /** @private */\n _doConnect() {\n try {\n this._ws = new WebSocket(this.url);\n } catch (e) {\n this.onError(e);\n this._scheduleReconnect();\n return;\n }\n\n this._ws.onopen = () => {\n this._retryCount = 0;\n this.onOpen();\n };\n\n this._ws.onmessage = (event) => {\n try {\n const msg = JSON.parse(event.data);\n this.onMessage(msg);\n } catch (e) {\n console.warn('[FuzionX] Invalid JSON:', event.data);\n }\n };\n\n this._ws.onclose = (event) => {\n this.onClose(event);\n if (!this._intentionalClose && this.autoReconnect) {\n this._scheduleReconnect();\n }\n };\n\n this._ws.onerror = (event) => {\n this.onError(event);\n };\n }\n\n /** JSON \uBA54\uC2DC\uC9C0 \uC804\uC1A1. */\n send(msg) {\n if (this._ws && this._ws.readyState === WebSocket.OPEN) {\n this._ws.send(JSON.stringify(msg));\n return true;\n }\n return false;\n }\n\n // \u2500\u2500 \uC2DC\uADF8\uB110\uB9C1 \uD5EC\uD37C \u2500\u2500\n\n /** Join \uC804\uC1A1 */\n sendJoin(peerId, channelId, opts = {}) {\n return this.send({\n type: SignalType.JOIN,\n peer_id: peerId,\n channel_id: channelId,\n nickname: opts.nickname || null,\n token: opts.token || null,\n mode: opts.mode || null,\n });\n }\n\n /** SDP Offer \uC804\uC1A1 */\n sendOffer(sdp) {\n return this.send({ type: SignalType.OFFER, sdp });\n }\n\n /** SDP Answer \uC804\uC1A1 */\n sendAnswer(sdp) {\n return this.send({ type: SignalType.ANSWER, sdp });\n }\n\n /** ICE Candidate \uC804\uC1A1 */\n sendCandidate(candidate) {\n return this.send({\n type: SignalType.CANDIDATE,\n candidate: candidate.candidate,\n sdp_mid: candidate.sdpMid,\n sdp_m_line_index: candidate.sdpMLineIndex,\n });\n }\n\n /** Chat \uC804\uC1A1 */\n sendChat(text, nickname) {\n return this.send({\n type: SignalType.CHAT,\n text,\n nickname: nickname || null,\n peer_id: null,\n });\n }\n\n /** PLI \uC694\uCCAD */\n sendPLI() {\n return this.send({ type: SignalType.PLI });\n }\n\n /** Leave \uC804\uC1A1 */\n sendLeave() {\n return this.send({ type: SignalType.LEAVE });\n }\n\n /** \uC5F0\uACB0 \uC885\uB8CC (\uC7AC\uC5F0\uACB0 \uC548 \uD568). */\n disconnect() {\n this._intentionalClose = true;\n clearTimeout(this._reconnectTimer);\n if (this._ws) {\n this._ws.close();\n this._ws = null;\n }\n }\n\n /** @returns {boolean} \uC5F0\uACB0 \uC0C1\uD0DC */\n get connected() {\n return this._ws && this._ws.readyState === WebSocket.OPEN;\n }\n\n /** @private \uC7AC\uC5F0\uACB0 \uC2A4\uCF00\uC904 (Exponential Backoff). */\n _scheduleReconnect() {\n if (this._retryCount >= RECONNECT.MAX_RETRIES) {\n console.error('[FuzionX] Max reconnect retries reached.');\n this.onError(new Error('Max reconnect retries'));\n return;\n }\n const delay = Math.min(\n RECONNECT.BASE_DELAY_MS * Math.pow(2, this._retryCount),\n RECONNECT.MAX_DELAY_MS\n );\n this._retryCount++;\n console.log(`[FuzionX] Reconnecting in ${delay}ms (${this._retryCount}/${RECONNECT.MAX_RETRIES})`);\n this._reconnectTimer = setTimeout(() => this._doConnect(), delay);\n }\n}\n", "/**\n * @fuzionx/player \u2014 FuzionXViewer (\uC218\uC2E0\uC790 \uBAA8\uB4DC)\n *\n * \uC11C\uBC84\uC5D0\uC11C MediaStream\uC744 \uC218\uC2E0\uD558\uC5EC <video> \uC5D8\uB9AC\uBA3C\uD2B8\uC5D0 \uB80C\uB354\uB9C1.\n * broadcast(1 stream) / videochat(\uCD5C\uB300 9 streams) \uC9C0\uC6D0.\n *\n * @example\n * const viewer = new FuzionXViewer({\n * url: 'wss://media:50002',\n * channelId: 'my-live',\n * mode: 'broadcast',\n * });\n * viewer.on('stream', (stream, slotIndex) => {\n * videoEl.srcObject = stream;\n * });\n * viewer.connect();\n */\n\nimport { FuzionXSignaling } from './FuzionXSignaling.js';\nimport { DEFAULT_ICE_SERVERS, SignalType, SessionMode, MAX_SLOTS } from './constants.js';\n\nexport class FuzionXViewer {\n /**\n * @param {Object} opts\n * @param {string} opts.url - WebSocket URL\n * @param {string} opts.channelId - \uCC44\uB110 ID\n * @param {string} [opts.mode='broadcast'] - 'broadcast' | 'videochat'\n * @param {string} [opts.nickname] - \uB2C9\uB124\uC784\n * @param {string} [opts.token] - \uC778\uC99D \uD1A0\uD070\n * @param {string} [opts.peerId] - \uD53C\uC5B4 ID (\uC790\uB3D9 \uC0DD\uC131)\n * @param {RTCConfiguration} [opts.rtcConfig] - WebRTC \uC124\uC815 \uC624\uBC84\uB77C\uC774\uB4DC\n * @param {boolean} [opts.autoReconnect=true]\n */\n constructor(opts) {\n this.url = opts.url;\n this.channelId = opts.channelId;\n this.mode = opts.mode || SessionMode.BROADCAST;\n this.nickname = opts.nickname || null;\n this.token = opts.token || null;\n this.peerId = opts.peerId || `viewer-${Math.random().toString(36).slice(2, 10)}`;\n this.autoReconnect = opts.autoReconnect !== false;\n\n this.rtcConfig = opts.rtcConfig || {\n iceServers: DEFAULT_ICE_SERVERS,\n bundlePolicy: 'max-bundle',\n rtcpMuxPolicy: 'require',\n };\n\n /** @private */\n this._signaling = null;\n this._pc = null;\n this._listeners = {};\n this._slots = new Map(); // slotIndex \u2192 { streamId, nickname, senderId, stream }\n this._maxSlots = MAX_SLOTS[this.mode] || 1;\n this._candidateQueue = [];\n this._connected = false;\n }\n\n // \u2500\u2500 Event System \u2500\u2500\n\n /**\n * \uC774\uBCA4\uD2B8 \uB9AC\uC2A4\uB108 \uB4F1\uB85D.\n * @param {'stream'|'slot'|'slot_remove'|'chat'|'error'|'close'|'connected'} event\n * @param {Function} handler\n */\n on(event, handler) {\n if (!this._listeners[event]) this._listeners[event] = [];\n this._listeners[event].push(handler);\n return this;\n }\n\n /** @private */\n _emit(event, ...args) {\n (this._listeners[event] || []).forEach((fn) => fn(...args));\n }\n\n // \u2500\u2500 Lifecycle \u2500\u2500\n\n /** \uC11C\uBC84 \uC5F0\uACB0 + WebRTC \uC138\uC158 \uC2DC\uC791. */\n connect() {\n this._signaling = new FuzionXSignaling({\n url: this.url,\n autoReconnect: this.autoReconnect,\n onOpen: () => this._onSignalingOpen(),\n onMessage: (msg) => this._onSignalingMessage(msg),\n onClose: (evt) => this._onSignalingClose(evt),\n onError: (err) => this._emit('error', err),\n });\n this._signaling.connect();\n }\n\n /** \uC5F0\uACB0 \uC885\uB8CC. */\n disconnect() {\n if (this._signaling) {\n this._signaling.sendLeave();\n this._signaling.disconnect();\n }\n this._closePeerConnection();\n this._slots.clear();\n this._connected = false;\n }\n\n /** \uCC44\uD305 \uC804\uC1A1. */\n chat(text) {\n if (this._signaling) {\n this._signaling.sendChat(text, this.nickname);\n }\n }\n\n /** \uD0A4\uD504\uB808\uC784 \uC694\uCCAD. */\n requestKeyframe() {\n if (this._signaling) {\n this._signaling.sendPLI();\n }\n }\n\n /** @returns {Map} \uD604\uC7AC \uC2AC\uB86F \uC815\uBCF4 */\n get slots() {\n return this._slots;\n }\n\n // \u2500\u2500 Internal \u2500\u2500\n\n /** @private */\n _onSignalingOpen() {\n this._signaling.sendJoin(this.peerId, this.channelId, {\n nickname: this.nickname,\n token: this.token,\n mode: this.mode,\n });\n // Join \uC804\uC1A1 \uD6C4 \uBC14\uB85C PeerConnection \uC0DD\uC131\n this._createPeerConnection().catch((e) => this._emit('error', e));\n }\n\n /** @private */\n _onSignalingMessage(msg) {\n switch (msg.type) {\n case SignalType.ANSWER:\n this._handleAnswer(msg);\n break;\n case SignalType.CANDIDATE:\n this._handleCandidate(msg);\n break;\n case SignalType.SLOT_INFO:\n this._handleSlotInfo(msg);\n break;\n case SignalType.CHAT:\n this._emit('chat', {\n peerId: msg.peer_id,\n nickname: msg.nickname,\n text: msg.text,\n });\n break;\n case SignalType.ERROR:\n this._emit('error', new Error(msg.message));\n break;\n }\n }\n\n /** @private */\n _onSignalingClose(evt) {\n this._closePeerConnection();\n this._connected = false;\n this._emit('close', evt);\n }\n\n /** @private */\n async _handleAnswer(msg) {\n if (!this._pc) return;\n try {\n await this._pc.setRemoteDescription(\n new RTCSessionDescription({ type: 'answer', sdp: msg.sdp })\n );\n // Flush queued ICE candidates\n for (const c of this._candidateQueue) {\n await this._pc.addIceCandidate(c);\n }\n this._candidateQueue = [];\n } catch (e) {\n this._emit('error', e);\n }\n }\n\n /** @private */\n async _handleCandidate(msg) {\n const candidate = new RTCIceCandidate({\n candidate: msg.candidate,\n sdpMid: msg.sdp_mid,\n sdpMLineIndex: msg.sdp_m_line_index,\n });\n if (this._pc && this._pc.remoteDescription) {\n try {\n await this._pc.addIceCandidate(candidate);\n } catch (e) {\n console.warn('[FuzionX] ICE candidate error:', e);\n }\n } else {\n this._candidateQueue.push(candidate);\n }\n }\n\n /** @private */\n _handleSlotInfo(msg) {\n const slotIndex = parseInt(msg.stream_id.replace('stream_', ''), 10);\n\n // nickname\uC774 \uBE48 \uBB38\uC790\uC5F4\uC774\uBA74 \uC2AC\uB86F \uD574\uC81C\n if (!msg.nickname || msg.nickname === '') {\n this._slots.delete(slotIndex);\n this._emit('slot_remove', { slotIndex, senderId: msg.sender_id });\n return;\n }\n\n const existing = this._slots.get(slotIndex) || {};\n this._slots.set(slotIndex, {\n ...existing,\n slotIndex,\n streamId: msg.stream_id,\n nickname: msg.nickname,\n senderId: msg.sender_id,\n });\n this._emit('slot', this._slots.get(slotIndex));\n }\n\n /**\n * PeerConnection \uC0DD\uC131 + Non-Trickle ICE Offer.\n * old alloy-player \uAC80\uC99D \uD328\uD134:\n * 1. addTransceiver(recvonly) \u00D7 maxSlots \u2014 \uC11C\uBC84 m-line \uB9E4\uD551\n * 2. createOffer \u2192 H264 SDP \uAC15\uC81C\n * 3. ICE gathering \uC644\uB8CC \uB300\uAE30 \uD6C4 SDP \uC804\uCCB4 \uC804\uC1A1 (Non-Trickle)\n * @private\n */\n async _createPeerConnection() {\n this._pc = new RTCPeerConnection(this.rtcConfig);\n\n // Non-Trickle: ICE candidate\uB294 SDP\uC5D0 \uD3EC\uD568\uB418\uC5B4 \uC804\uC1A1\uB428\n // (onicecandidate\uB294 gathering \uCD94\uC801\uC6A9\uC73C\uB85C\uB9CC \uC720\uC9C0)\n\n // \uC218\uC2E0 \uD2B8\uB799 \uB9E4\uD551\n let videoTrackCount = 0;\n this._pc.ontrack = (event) => {\n const track = event.track;\n\n if (track.kind === 'video') {\n const slotIndex = videoTrackCount++;\n\n let stream = event.streams[0];\n if (!stream) {\n stream = new MediaStream();\n stream.addTrack(track);\n }\n\n const slot = this._slots.get(slotIndex) || { slotIndex };\n slot.stream = stream;\n this._slots.set(slotIndex, slot);\n\n this._emit('stream', stream, slotIndex);\n\n if (!this._connected) {\n this._connected = true;\n this._emit('connected');\n }\n }\n };\n\n this._pc.onconnectionstatechange = () => {\n const state = this._pc?.connectionState;\n if (state === 'failed' || state === 'disconnected') {\n this._emit('error', new Error(`PeerConnection ${state}`));\n }\n };\n\n // recvonly Transceiver \uCD94\uAC00 (\uC11C\uBC84 \uC2AC\uB86F \uC218\uC5D0 \uB9DE\uCDA4)\n for (let i = 0; i < this._maxSlots; i++) {\n this._pc.addTransceiver('video', { direction: 'recvonly' });\n this._pc.addTransceiver('audio', { direction: 'recvonly' });\n }\n\n // Offer \uC0DD\uC131 + H264/Opus \uCF54\uB371 \uAC15\uC81C\n const offer = await this._pc.createOffer();\n offer.sdp = FuzionXViewer._forceCodecs(offer.sdp);\n await this._pc.setLocalDescription(offer);\n\n // Non-Trickle ICE: gathering \uC644\uB8CC \uB300\uAE30 \uD6C4 \uC804\uCCB4 SDP \uC804\uC1A1\n await this._waitForIceGathering();\n\n // gathering \uC644\uB8CC \uD6C4 \uCD5C\uC885 SDP (ICE candidates \uD3EC\uD568) \uC804\uC1A1\n const finalSdp = this._pc.localDescription?.sdp;\n if (finalSdp) {\n this._signaling.sendOffer(finalSdp);\n }\n }\n\n /**\n * ICE gathering \uC644\uB8CC \uB300\uAE30 (Non-Trickle)\n * @private\n */\n _waitForIceGathering() {\n return new Promise((resolve) => {\n if (this._pc.iceGatheringState === 'complete') {\n return resolve();\n }\n const check = () => {\n if (this._pc?.iceGatheringState === 'complete') {\n this._pc.removeEventListener('icegatheringstatechange', check);\n resolve();\n }\n };\n this._pc.addEventListener('icegatheringstatechange', check);\n // Timeout 3s (Safari \uD638\uD658)\n setTimeout(() => {\n if (this._pc) {\n this._pc.removeEventListener('icegatheringstatechange', check);\n }\n resolve();\n }, 3000);\n });\n }\n\n /**\n * SDP\uC5D0\uC11C H264 + Opus \uCF54\uB371 \uC6B0\uC120 \uAC15\uC81C.\n * @private\n */\n static _forceCodecs(sdp) {\n let lines = sdp.split('\\r\\n');\n\n // Video: H264 \uC6B0\uC120\n const videoIdx = lines.findIndex((l) => l.startsWith('m=video'));\n if (videoIdx !== -1) {\n const h264Pts = [];\n const h264Scores = new Map();\n lines.forEach((l) => {\n const m = l.match(/a=rtpmap:(\\d+) H264\\/90000/);\n if (m) {\n h264Pts.push(m[1]);\n h264Scores.set(m[1], 0);\n }\n });\n lines.forEach((l) => {\n if (l.startsWith('a=fmtp:')) {\n const pt = l.split(' ')[0].split(':')[1];\n if (h264Scores.has(pt)) {\n if (l.includes('profile-level-id=42e01f')) h264Scores.set(pt, 100);\n else if (l.includes('profile-level-id=42001f')) h264Scores.set(pt, 80);\n if (l.includes('packetization-mode=1')) h264Scores.set(pt, (h264Scores.get(pt) || 0) + 10);\n }\n }\n });\n if (h264Pts.length > 0) {\n h264Pts.sort((a, b) => h264Scores.get(b) - h264Scores.get(a));\n const parts = lines[videoIdx].split(' ');\n const otherPts = parts.slice(3).filter((pt) => !h264Pts.includes(pt));\n lines[videoIdx] = [...parts.slice(0, 3), ...h264Pts, ...otherPts].join(' ');\n }\n }\n\n // Audio: Opus \uC6B0\uC120\n const audioIdx = lines.findIndex((l) => l.startsWith('m=audio'));\n if (audioIdx !== -1) {\n const opusPts = [];\n lines.forEach((l) => {\n const m = l.match(/a=rtpmap:(\\d+) opus\\/48000/);\n if (m) opusPts.push(m[1]);\n });\n if (opusPts.length > 0) {\n const parts = lines[audioIdx].split(' ');\n const otherPts = parts.slice(3).filter((pt) => !opusPts.includes(pt));\n lines[audioIdx] = [...parts.slice(0, 3), ...opusPts, ...otherPts].join(' ');\n }\n }\n\n return lines.join('\\r\\n');\n }\n\n /** @private */\n _closePeerConnection() {\n if (this._pc) {\n this._pc.close();\n this._pc = null;\n }\n this._candidateQueue = [];\n }\n\n}\n", "/**\n * @fuzionx/player \u2014 FuzionXPublisher (\uC1A1\uCD9C\uC790 \uBAA8\uB4DC)\n *\n * \uB85C\uCEEC \uCE74\uBA54\uB77C/\uB9C8\uC774\uD06C \u2192 \uC11C\uBC84 \uC804\uC1A1.\n * 2\uAC00\uC9C0 \uBC29\uC2DD \uC9C0\uC6D0:\n * A. WebSocket \uBC29\uC2DD (videochat \uC591\uBC29\uD5A5)\n * B. WHIP \uBC29\uC2DD (OBS/\uB2E8\uBC29\uD5A5 \uBC29\uC1A1)\n *\n * @example WebSocket\n * const pub = new FuzionXPublisher({\n * url: 'wss://media:50002',\n * channelId: 'my-live',\n * mode: 'videochat',\n * nickname: '\uBC1C\uD45C\uC790',\n * });\n * pub.on('ready', () => console.log('Publishing!'));\n * pub.connect();\n *\n * @example WHIP\n * const pub = new FuzionXPublisher({\n * whipUrl: 'https://media:7777/whip/my-live',\n * });\n * pub.connect();\n */\n\nimport { FuzionXSignaling } from './FuzionXSignaling.js';\nimport { DEFAULT_ICE_SERVERS, SignalType, SessionMode, MAX_SLOTS } from './constants.js';\n\nexport class FuzionXPublisher {\n /**\n * @param {Object} opts\n * @param {string} [opts.url] - WebSocket URL (WS \uBC29\uC2DD)\n * @param {string} [opts.whipUrl] - WHIP URL (WHIP \uBC29\uC2DD)\n * @param {string} [opts.channelId] - \uCC44\uB110 ID (WS \uBC29\uC2DD \uD544\uC218)\n * @param {string} [opts.mode='broadcast'] - 'broadcast' | 'videochat'\n * @param {string} [opts.nickname] - \uB2C9\uB124\uC784\n * @param {string} [opts.token] - \uC778\uC99D \uD1A0\uD070\n * @param {string} [opts.peerId] - \uD53C\uC5B4 ID\n * @param {MediaStreamConstraints} [opts.media] - getUserMedia \uC81C\uC57D\n * @param {MediaStream} [opts.stream] - \uC774\uBBF8 \uD68D\uB4DD\uD55C MediaStream\n * @param {RTCConfiguration} [opts.rtcConfig] - WebRTC \uC124\uC815\n * @param {boolean} [opts.autoReconnect=true]\n */\n constructor(opts) {\n // WHIP or WebSocket\n this.whipUrl = opts.whipUrl || null;\n this.url = opts.url || null;\n this.channelId = opts.channelId || null;\n this.mode = opts.mode || SessionMode.BROADCAST;\n this.nickname = opts.nickname || null;\n this.token = opts.token || null;\n this.peerId = opts.peerId || `pub-${Math.random().toString(36).slice(2, 10)}`;\n this.autoReconnect = opts.autoReconnect !== false;\n\n this.mediaConstraints = opts.media || { video: true, audio: true };\n this._externalStream = opts.stream || null;\n\n this.rtcConfig = opts.rtcConfig || {\n iceServers: DEFAULT_ICE_SERVERS,\n bundlePolicy: 'max-bundle',\n rtcpMuxPolicy: 'require',\n };\n\n /** @private */\n this._signaling = null;\n this._pc = null;\n this._localStream = null;\n this._listeners = {};\n this._candidateQueue = [];\n this._whipResourceUrl = null;\n this._maxSlots = MAX_SLOTS[this.mode] || 1;\n this._slots = new Map();\n this._connected = false;\n }\n\n // \u2500\u2500 Event System \u2500\u2500\n\n on(event, handler) {\n if (!this._listeners[event]) this._listeners[event] = [];\n this._listeners[event].push(handler);\n return this;\n }\n\n /** @private */\n _emit(event, ...args) {\n (this._listeners[event] || []).forEach((fn) => fn(...args));\n }\n\n // \u2500\u2500 Lifecycle \u2500\u2500\n\n /** \uC5F0\uACB0 \uC2DC\uC791 (\uBBF8\uB514\uC5B4 \uD68D\uB4DD \u2192 WebSocket \uB610\uB294 WHIP). */\n async connect() {\n try {\n await this._acquireMedia();\n\n if (this.whipUrl) {\n await this._connectWhip();\n } else if (this.url) {\n this._connectWebSocket();\n } else {\n throw new Error('url \uB610\uB294 whipUrl \uC911 \uD558\uB098\uB97C \uC9C0\uC815\uD574\uC57C \uD569\uB2C8\uB2E4.');\n }\n } catch (e) {\n this._emit('error', e);\n }\n }\n\n /** \uC5F0\uACB0 \uC885\uB8CC. */\n async disconnect() {\n if (this.whipUrl && this._whipResourceUrl) {\n // WHIP DELETE\n try {\n const baseUrl = new URL(this.whipUrl).origin;\n await fetch(`${baseUrl}${this._whipResourceUrl}`, { method: 'DELETE' });\n } catch (e) {\n console.warn('[FuzionX] WHIP DELETE error:', e);\n }\n this._whipResourceUrl = null;\n }\n\n if (this._signaling) {\n this._signaling.sendLeave();\n this._signaling.disconnect();\n }\n\n this._closePeerConnection();\n this._stopMedia();\n this._connected = false;\n }\n\n /** \uCC44\uD305 \uC804\uC1A1. */\n chat(text) {\n if (this._signaling) {\n this._signaling.sendChat(text, this.nickname);\n }\n }\n\n /** @returns {MediaStream|null} \uB85C\uCEEC \uC2A4\uD2B8\uB9BC */\n get localStream() {\n return this._localStream;\n }\n\n /** @returns {Map} \uC2AC\uB86F \uC815\uBCF4 (videochat: \uB2E4\uB978 \uCC38\uAC00\uC790) */\n get slots() {\n return this._slots;\n }\n\n // \u2500\u2500 Media \u2500\u2500\n\n /** @private \uBBF8\uB514\uC5B4 \uD68D\uB4DD */\n async _acquireMedia() {\n if (this._externalStream) {\n this._localStream = this._externalStream;\n } else {\n this._localStream = await navigator.mediaDevices.getUserMedia(this.mediaConstraints);\n }\n this._emit('media', this._localStream);\n }\n\n /** @private */\n _stopMedia() {\n if (this._localStream && !this._externalStream) {\n this._localStream.getTracks().forEach((t) => t.stop());\n }\n this._localStream = null;\n }\n\n // \u2500\u2500 WHIP \u2500\u2500\n\n /** @private WHIP \uC5F0\uACB0 */\n async _connectWhip() {\n this._pc = new RTCPeerConnection(this.rtcConfig);\n\n // \uD2B8\uB799 \uCD94\uAC00\n this._localStream.getTracks().forEach((track) => {\n this._pc.addTrack(track, this._localStream);\n });\n\n // ICE Gathering \uC644\uB8CC \uB300\uAE30\n const offer = await this._pc.createOffer();\n offer.sdp = FuzionXPublisher._forceCodecs(offer.sdp);\n await this._pc.setLocalDescription(offer);\n\n // Gathering \uC644\uB8CC \uB300\uAE30\n await new Promise((resolve) => {\n if (this._pc.iceGatheringState === 'complete') {\n resolve();\n } else {\n this._pc.onicegatheringstatechange = () => {\n if (this._pc.iceGatheringState === 'complete') resolve();\n };\n // \uC548\uC804 \uD0C0\uC784\uC544\uC6C3\n setTimeout(resolve, 3000);\n }\n });\n\n const localDesc = this._pc.localDescription;\n // Hub API\uC758 whip_url\uC740 \uC774\uBBF8 ?token=xxx \uD3EC\uD568 \uAC00\uB2A5\n let url = this.whipUrl;\n if (this.token && !url.includes('token=')) {\n url += (url.includes('?') ? '&' : '?') + `token=${this.token}`;\n }\n\n const response = await fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/sdp' },\n body: localDesc.sdp,\n });\n\n if (response.status !== 201) {\n throw new Error(`WHIP failed: ${response.status} ${await response.text()}`);\n }\n\n const answerSdp = await response.text();\n this._whipResourceUrl = response.headers.get('location');\n\n await this._pc.setRemoteDescription(\n new RTCSessionDescription({ type: 'answer', sdp: answerSdp })\n );\n\n this._pc.onconnectionstatechange = () => {\n const state = this._pc?.connectionState;\n if (state === 'connected') {\n this._connected = true;\n this._emit('ready');\n } else if (state === 'failed' || state === 'disconnected') {\n this._emit('error', new Error(`WHIP PeerConnection ${state}`));\n }\n };\n\n this._emit('ready');\n }\n\n // \u2500\u2500 WebSocket \u2500\u2500\n\n /** @private WS \uC5F0\uACB0 */\n _connectWebSocket() {\n this._signaling = new FuzionXSignaling({\n url: this.url,\n autoReconnect: this.autoReconnect,\n onOpen: () => this._onSignalingOpen(),\n onMessage: (msg) => this._onSignalingMessage(msg),\n onClose: (evt) => {\n this._closePeerConnection();\n this._connected = false;\n this._emit('close', evt);\n },\n onError: (err) => this._emit('error', err),\n });\n this._signaling.connect();\n }\n\n /** @private */\n async _onSignalingOpen() {\n this._signaling.sendJoin(this.peerId, this.channelId, {\n nickname: this.nickname,\n token: this.token,\n mode: this.mode,\n });\n await this._createPeerConnection();\n }\n\n /** @private */\n async _createPeerConnection() {\n this._pc = new RTCPeerConnection(this.rtcConfig);\n\n // Non-Trickle: ICE candidate\uB294 SDP\uC5D0 \uD3EC\uD568\n\n // \uB85C\uCEEC \uD2B8\uB799 \uCD94\uAC00 (\uC1A1\uCD9C)\n this._localStream.getTracks().forEach((track) => {\n this._pc.addTrack(track, this._localStream);\n });\n\n // videochat \uBAA8\uB4DC: \uC218\uC2E0 \uD2B8\uB799\uB3C4 \uC900\uBE44 (\uC591\uBC29\uD5A5)\n if (this.mode === SessionMode.VIDEOCHAT) {\n for (let i = 0; i < this._maxSlots; i++) {\n this._pc.addTransceiver('video', { direction: 'recvonly' });\n this._pc.addTransceiver('audio', { direction: 'recvonly' });\n }\n\n // \uC1A1\uCD9C \uD2B8\uB799\uC740 addTrack\uC73C\uB85C \uCD94\uAC00\uB428 \u2192 transceiver\uBC29\uD5A5\uC744 sendrecv\uB85C \uC5C5\uADF8\uB808\uC774\uB4DC\n const transceivers = this._pc.getTransceivers();\n transceivers.forEach((t) => {\n if (t.sender.track && t.direction === 'recvonly') {\n t.direction = 'sendrecv';\n }\n });\n\n let videoTrackCount = 0;\n this._pc.ontrack = (event) => {\n const track = event.track;\n if (track.kind === 'video') {\n const slotIndex = videoTrackCount++;\n let stream = event.streams[0];\n if (!stream) {\n stream = new MediaStream();\n stream.addTrack(track);\n }\n const slot = this._slots.get(slotIndex) || { slotIndex };\n slot.stream = stream;\n this._slots.set(slotIndex, slot);\n this._emit('stream', stream, slotIndex);\n }\n };\n }\n\n this._pc.onconnectionstatechange = () => {\n const state = this._pc?.connectionState;\n if (state === 'connected' && !this._connected) {\n this._connected = true;\n this._emit('ready');\n } else if (state === 'failed' || state === 'disconnected') {\n this._emit('error', new Error(`PeerConnection ${state}`));\n }\n };\n\n // Offer + H264/Opus SDP \uAC15\uC81C\n const offer = await this._pc.createOffer();\n offer.sdp = FuzionXPublisher._forceCodecs(offer.sdp);\n await this._pc.setLocalDescription(offer);\n\n // Non-Trickle: ICE gathering \uC644\uB8CC \uB300\uAE30\n await this._waitForIceGathering();\n\n const finalSdp = this._pc.localDescription?.sdp;\n if (finalSdp) {\n this._signaling.sendOffer(finalSdp);\n }\n }\n\n /** @private */\n _waitForIceGathering() {\n return new Promise((resolve) => {\n if (this._pc.iceGatheringState === 'complete') {\n return resolve();\n }\n const check = () => {\n if (this._pc?.iceGatheringState === 'complete') {\n this._pc.removeEventListener('icegatheringstatechange', check);\n resolve();\n }\n };\n this._pc.addEventListener('icegatheringstatechange', check);\n setTimeout(() => {\n if (this._pc) {\n this._pc.removeEventListener('icegatheringstatechange', check);\n }\n resolve();\n }, 3000);\n });\n }\n\n /** @private */\n static _forceCodecs(sdp) {\n let lines = sdp.split('\\r\\n');\n const videoIdx = lines.findIndex((l) => l.startsWith('m=video'));\n if (videoIdx !== -1) {\n const h264Pts = [];\n const h264Scores = new Map();\n lines.forEach((l) => {\n const m = l.match(/a=rtpmap:(\\d+) H264\\/90000/);\n if (m) { h264Pts.push(m[1]); h264Scores.set(m[1], 0); }\n });\n lines.forEach((l) => {\n if (l.startsWith('a=fmtp:')) {\n const pt = l.split(' ')[0].split(':')[1];\n if (h264Scores.has(pt)) {\n if (l.includes('profile-level-id=42e01f')) h264Scores.set(pt, 100);\n else if (l.includes('profile-level-id=42001f')) h264Scores.set(pt, 80);\n if (l.includes('packetization-mode=1')) h264Scores.set(pt, (h264Scores.get(pt) || 0) + 10);\n }\n }\n });\n if (h264Pts.length > 0) {\n h264Pts.sort((a, b) => h264Scores.get(b) - h264Scores.get(a));\n const parts = lines[videoIdx].split(' ');\n const otherPts = parts.slice(3).filter((pt) => !h264Pts.includes(pt));\n lines[videoIdx] = [...parts.slice(0, 3), ...h264Pts, ...otherPts].join(' ');\n }\n }\n const audioIdx = lines.findIndex((l) => l.startsWith('m=audio'));\n if (audioIdx !== -1) {\n const opusPts = [];\n lines.forEach((l) => {\n const m = l.match(/a=rtpmap:(\\d+) opus\\/48000/);\n if (m) opusPts.push(m[1]);\n });\n if (opusPts.length > 0) {\n const parts = lines[audioIdx].split(' ');\n const otherPts = parts.slice(3).filter((pt) => !opusPts.includes(pt));\n lines[audioIdx] = [...parts.slice(0, 3), ...opusPts, ...otherPts].join(' ');\n }\n }\n return lines.join('\\r\\n');\n }\n\n /** @private */\n _onSignalingMessage(msg) {\n switch (msg.type) {\n case SignalType.ANSWER:\n this._handleAnswer(msg);\n break;\n case SignalType.CANDIDATE:\n this._handleCandidate(msg);\n break;\n case SignalType.SLOT_INFO:\n this._handleSlotInfo(msg);\n break;\n case SignalType.CHAT:\n this._emit('chat', {\n peerId: msg.peer_id,\n nickname: msg.nickname,\n text: msg.text,\n });\n break;\n case SignalType.ERROR:\n this._emit('error', new Error(msg.message));\n break;\n }\n }\n\n /** @private */\n async _handleAnswer(msg) {\n if (!this._pc) return;\n try {\n await this._pc.setRemoteDescription(\n new RTCSessionDescription({ type: 'answer', sdp: msg.sdp })\n );\n for (const c of this._candidateQueue) {\n await this._pc.addIceCandidate(c);\n }\n this._candidateQueue = [];\n } catch (e) {\n this._emit('error', e);\n }\n }\n\n /** @private */\n async _handleCandidate(msg) {\n const candidate = new RTCIceCandidate({\n candidate: msg.candidate,\n sdpMid: msg.sdp_mid,\n sdpMLineIndex: msg.sdp_m_line_index,\n });\n if (this._pc && this._pc.remoteDescription) {\n try { await this._pc.addIceCandidate(candidate); } catch (e) { /* ignore */ }\n } else {\n this._candidateQueue.push(candidate);\n }\n }\n\n /** @private */\n _handleSlotInfo(msg) {\n const slotIndex = parseInt(msg.stream_id.replace('stream_', ''), 10);\n if (!msg.nickname || msg.nickname === '') {\n this._slots.delete(slotIndex);\n this._emit('slot_remove', { slotIndex, senderId: msg.sender_id });\n return;\n }\n const existing = this._slots.get(slotIndex) || {};\n this._slots.set(slotIndex, {\n ...existing,\n slotIndex,\n streamId: msg.stream_id,\n nickname: msg.nickname,\n senderId: msg.sender_id,\n });\n this._emit('slot', this._slots.get(slotIndex));\n }\n\n /** @private */\n _closePeerConnection() {\n if (this._pc) {\n this._pc.close();\n this._pc = null;\n }\n this._candidateQueue = [];\n }\n}\n"],
5
- "mappings": "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",
6
- "names": ["DEFAULT_ICE_SERVERS", "SignalType", "SessionMode", "MAX_SLOTS", "RECONNECT", "CODEC", "FuzionXSignaling", "opts", "event", "msg", "peerId", "channelId", "SignalType", "sdp", "candidate", "text", "nickname", "RECONNECT", "delay", "FuzionXViewer", "_FuzionXViewer", "opts", "SessionMode", "DEFAULT_ICE_SERVERS", "MAX_SLOTS", "event", "handler", "args", "fn", "FuzionXSignaling", "msg", "evt", "err", "text", "SignalType", "c", "e", "candidate", "slotIndex", "existing", "videoTrackCount", "track", "stream", "slot", "state", "i", "offer", "finalSdp", "resolve", "check", "sdp", "lines", "videoIdx", "l", "h264Pts", "h264Scores", "m", "pt", "a", "b", "parts", "otherPts", "audioIdx", "opusPts", "FuzionXPublisher", "_FuzionXPublisher", "opts", "SessionMode", "DEFAULT_ICE_SERVERS", "MAX_SLOTS", "event", "handler", "args", "fn", "baseUrl", "text", "t", "track", "offer", "resolve", "localDesc", "url", "response", "answerSdp", "state", "FuzionXSignaling", "msg", "evt", "err", "i", "videoTrackCount", "slotIndex", "stream", "slot", "finalSdp", "check", "sdp", "lines", "videoIdx", "l", "h264Pts", "h264Scores", "m", "pt", "a", "b", "parts", "otherPts", "audioIdx", "opusPts", "SignalType", "c", "e", "candidate", "existing"]
4
+ "sourcesContent": ["/**\n * @fuzionx/player \u2014 Constants & Default Configuration\n */\n\n/** Default ICE Servers */\nexport const DEFAULT_ICE_SERVERS = [\n { urls: 'stun:stun.l.google.com:19302' },\n { urls: 'stun:stun1.l.google.com:19302' },\n];\n\n/** Signaling message types (maps to server SignalMessage enum) */\nexport const SignalType = {\n JOIN: 'join',\n OFFER: 'offer',\n ANSWER: 'answer',\n CANDIDATE: 'candidate',\n PLI: 'pli',\n LEAVE: 'leave',\n SLOT_INFO: 'slot_info',\n CHAT: 'chat',\n ERROR: 'error',\n};\n\n/** Session modes */\nexport const SessionMode = {\n BROADCAST: 'broadcast',\n VIDEOCHAT: 'videochat',\n};\n\n/** Max slots per mode */\nexport const MAX_SLOTS = {\n [SessionMode.BROADCAST]: 1,\n [SessionMode.VIDEOCHAT]: 9,\n};\n\n/** Default reconnect settings */\nexport const RECONNECT = {\n MAX_RETRIES: 5,\n BASE_DELAY_MS: 1000,\n MAX_DELAY_MS: 30000,\n};\n\n/** Codec preferences */\nexport const CODEC = {\n VIDEO_MIME: 'video/H264',\n AUDIO_MIME: 'audio/opus',\n VIDEO_CLOCK: 90000,\n AUDIO_CLOCK: 48000,\n};\n", "/**\n * @fuzionx/player \u2014 WebSocket Signaling Layer\n *\n * FuzionX Media Server\uC758 JSON \uC2DC\uADF8\uB110\uB9C1 \uD504\uB85C\uD1A0\uCF5C \uCEA1\uC290\uD654.\n * \uC790\uB3D9 \uC7AC\uC5F0\uACB0 + \uC774\uBCA4\uD2B8 \uC2DC\uC2A4\uD15C.\n */\n\nimport { SignalType, RECONNECT } from './constants.js';\n\nexport class FuzionXSignaling {\n /**\n * @param {Object} opts\n * @param {string} opts.url - WebSocket URL (ws:// or wss://)\n * @param {Function} [opts.onMessage] - \uBA54\uC2DC\uC9C0 \uC218\uC2E0 \uCF5C\uBC31\n * @param {Function} [opts.onOpen] - \uC5F0\uACB0 \uC131\uACF5 \uCF5C\uBC31\n * @param {Function} [opts.onClose] - \uC5F0\uACB0 \uC885\uB8CC \uCF5C\uBC31\n * @param {Function} [opts.onError] - \uC5D0\uB7EC \uCF5C\uBC31\n * @param {boolean} [opts.autoReconnect=true]\n */\n constructor(opts) {\n this.url = opts.url;\n this.onMessage = opts.onMessage || (() => {});\n this.onOpen = opts.onOpen || (() => {});\n this.onClose = opts.onClose || (() => {});\n this.onError = opts.onError || (() => {});\n this.autoReconnect = opts.autoReconnect !== false;\n\n /** @private */\n this._ws = null;\n this._retryCount = 0;\n this._reconnectTimer = null;\n this._intentionalClose = false;\n }\n\n /** WebSocket \uC5F0\uACB0. */\n connect() {\n this._intentionalClose = false;\n this._doConnect();\n }\n\n /** @private */\n _doConnect() {\n try {\n this._ws = new WebSocket(this.url);\n } catch (e) {\n this.onError(e);\n this._scheduleReconnect();\n return;\n }\n\n this._ws.onopen = () => {\n this._retryCount = 0;\n this.onOpen();\n };\n\n this._ws.onmessage = (event) => {\n try {\n const msg = JSON.parse(event.data);\n this.onMessage(msg);\n } catch (e) {\n console.warn('[FuzionX] Invalid JSON:', event.data);\n }\n };\n\n this._ws.onclose = (event) => {\n this.onClose(event);\n if (!this._intentionalClose && this.autoReconnect) {\n this._scheduleReconnect();\n }\n };\n\n this._ws.onerror = (event) => {\n this.onError(event);\n };\n }\n\n /** JSON \uBA54\uC2DC\uC9C0 \uC804\uC1A1. */\n send(msg) {\n if (this._ws && this._ws.readyState === WebSocket.OPEN) {\n this._ws.send(JSON.stringify(msg));\n return true;\n }\n return false;\n }\n\n // \u2500\u2500 \uC2DC\uADF8\uB110\uB9C1 \uD5EC\uD37C \u2500\u2500\n\n /** Join \uC804\uC1A1 */\n sendJoin(peerId, channelId, opts = {}) {\n return this.send({\n type: SignalType.JOIN,\n peer_id: peerId,\n channel_id: channelId,\n nickname: opts.nickname || null,\n token: opts.token || null,\n mode: opts.mode || null,\n });\n }\n\n /** SDP Offer \uC804\uC1A1 */\n sendOffer(sdp) {\n return this.send({ type: SignalType.OFFER, sdp });\n }\n\n /** SDP Answer \uC804\uC1A1 */\n sendAnswer(sdp) {\n return this.send({ type: SignalType.ANSWER, sdp });\n }\n\n /** ICE Candidate \uC804\uC1A1 */\n sendCandidate(candidate) {\n return this.send({\n type: SignalType.CANDIDATE,\n candidate: candidate.candidate,\n sdp_mid: candidate.sdpMid,\n sdp_m_line_index: candidate.sdpMLineIndex,\n });\n }\n\n /** Chat \uC804\uC1A1 */\n sendChat(text, nickname) {\n return this.send({\n type: SignalType.CHAT,\n text,\n nickname: nickname || null,\n peer_id: null,\n });\n }\n\n /** PLI \uC694\uCCAD */\n sendPLI() {\n return this.send({ type: SignalType.PLI });\n }\n\n /** Leave \uC804\uC1A1 */\n sendLeave() {\n return this.send({ type: SignalType.LEAVE });\n }\n\n /** \uC5F0\uACB0 \uC885\uB8CC (\uC7AC\uC5F0\uACB0 \uC548 \uD568). */\n disconnect() {\n this._intentionalClose = true;\n clearTimeout(this._reconnectTimer);\n if (this._ws) {\n this._ws.close();\n this._ws = null;\n }\n }\n\n /** @returns {boolean} \uC5F0\uACB0 \uC0C1\uD0DC */\n get connected() {\n return this._ws && this._ws.readyState === WebSocket.OPEN;\n }\n\n /** @private \uC7AC\uC5F0\uACB0 \uC2A4\uCF00\uC904 (Exponential Backoff). */\n _scheduleReconnect() {\n if (this._retryCount >= RECONNECT.MAX_RETRIES) {\n console.error('[FuzionX] Max reconnect retries reached.');\n this.onError(new Error('Max reconnect retries'));\n return;\n }\n const delay = Math.min(\n RECONNECT.BASE_DELAY_MS * Math.pow(2, this._retryCount),\n RECONNECT.MAX_DELAY_MS\n );\n this._retryCount++;\n console.log(`[FuzionX] Reconnecting in ${delay}ms (${this._retryCount}/${RECONNECT.MAX_RETRIES})`);\n this._reconnectTimer = setTimeout(() => this._doConnect(), delay);\n }\n}\n", "/**\n * @fuzionx/player \u2014 FuzionXViewer (\uC218\uC2E0\uC790 \uBAA8\uB4DC)\n *\n * \uC11C\uBC84\uC5D0\uC11C MediaStream\uC744 \uC218\uC2E0\uD558\uC5EC <video> \uC5D8\uB9AC\uBA3C\uD2B8\uC5D0 \uB80C\uB354\uB9C1.\n * broadcast(1 stream) / videochat(\uCD5C\uB300 9 streams) \uC9C0\uC6D0.\n *\n * @example\n * const viewer = new FuzionXViewer({\n * url: 'wss://media:50002',\n * channelId: 'my-live',\n * mode: 'broadcast',\n * });\n * viewer.on('stream', (stream, slotIndex) => {\n * videoEl.srcObject = stream;\n * });\n * viewer.connect();\n */\n\nimport { FuzionXSignaling } from './FuzionXSignaling.js';\nimport { DEFAULT_ICE_SERVERS, SignalType, SessionMode, MAX_SLOTS } from './constants.js';\n\nexport class FuzionXViewer {\n /**\n * @param {Object} opts\n * @param {string} [opts.url] - WebSocket URL (\uC9C1\uC811 \uC9C0\uC815)\n * @param {string} [opts.hubUrl] - Hub API URL (\uC790\uB3D9 \uB77C\uC6B0\uD305: Hub\uC5D0\uC11C \uBBF8\uB514\uC5B4 \uC11C\uBC84 \uC870\uD68C)\n * @param {string} opts.channelId - \uCC44\uB110 ID\n * @param {string} [opts.mode='broadcast'] - 'broadcast' | 'videochat'\n * @param {string} [opts.nickname] - \uB2C9\uB124\uC784\n * @param {string} [opts.token] - \uC778\uC99D \uD1A0\uD070\n * @param {string} [opts.peerId] - \uD53C\uC5B4 ID (\uC790\uB3D9 \uC0DD\uC131)\n * @param {RTCConfiguration} [opts.rtcConfig] - WebRTC \uC124\uC815 \uC624\uBC84\uB77C\uC774\uB4DC\n * @param {boolean} [opts.autoReconnect=true]\n */\n constructor(opts) {\n this.url = opts.url || null;\n this.hubUrl = opts.hubUrl || null;\n this.channelId = opts.channelId;\n this.mode = opts.mode || SessionMode.BROADCAST;\n this.nickname = opts.nickname || null;\n this.token = opts.token || null;\n this.peerId = opts.peerId || `viewer-${Math.random().toString(36).slice(2, 10)}`;\n this.autoReconnect = opts.autoReconnect !== false;\n\n this.rtcConfig = opts.rtcConfig || {\n iceServers: DEFAULT_ICE_SERVERS,\n bundlePolicy: 'max-bundle',\n rtcpMuxPolicy: 'require',\n };\n\n /** @private */\n this._signaling = null;\n this._pc = null;\n this._listeners = {};\n this._slots = new Map(); // slotIndex \u2192 { streamId, nickname, senderId, stream }\n this._maxSlots = MAX_SLOTS[this.mode] || 1;\n this._candidateQueue = [];\n this._connected = false;\n }\n\n // \u2500\u2500 Event System \u2500\u2500\n\n /**\n * \uC774\uBCA4\uD2B8 \uB9AC\uC2A4\uB108 \uB4F1\uB85D.\n * @param {'stream'|'slot'|'slot_remove'|'chat'|'error'|'close'|'connected'} event\n * @param {Function} handler\n */\n on(event, handler) {\n if (!this._listeners[event]) this._listeners[event] = [];\n this._listeners[event].push(handler);\n return this;\n }\n\n /** @private */\n _emit(event, ...args) {\n (this._listeners[event] || []).forEach((fn) => fn(...args));\n }\n\n // \u2500\u2500 Lifecycle \u2500\u2500\n\n /** \uC11C\uBC84 \uC5F0\uACB0 + WebRTC \uC138\uC158 \uC2DC\uC791. */\n async connect() {\n // Hub \uB77C\uC6B0\uD305: hubUrl\uC774 \uC788\uC73C\uBA74 Hub\uC5D0\uC11C \uBBF8\uB514\uC5B4 \uC11C\uBC84 \uC870\uD68C\n if (!this.url && this.hubUrl) {\n try {\n const res = await fetch(`${this.hubUrl}/api/channels/${this.channelId}`);\n if (!res.ok) throw new Error(`Channel not found: ${this.channelId}`);\n const data = await res.json();\n // ws_url \uC0AC\uC6A9 \uB610\uB294 ip:port\uC5D0\uC11C \uC0DD\uC131\n if (data.ws_url) {\n // Hub\uC640 \uAC19\uC740 \uD504\uB85C\uD1A0\uCF5C \uC0AC\uC6A9 (https\u2192wss, http\u2192ws)\n const isSecure = this.hubUrl.startsWith('https');\n this.url = data.ws_url.replace(/^ws(s?):/, isSecure ? 'wss:' : 'ws:');\n } else {\n const isSecure = this.hubUrl.startsWith('https');\n const wsProto = isSecure ? 'wss' : 'ws';\n this.url = `${wsProto}://${data.media_ip}:${data.webrtc_port}`;\n }\n } catch (e) {\n this._emit('error', e);\n return;\n }\n }\n\n if (!this.url) {\n this._emit('error', new Error('url \uB610\uB294 hubUrl\uC744 \uC9C0\uC815\uD574\uC57C \uD569\uB2C8\uB2E4.'));\n return;\n }\n\n this._signaling = new FuzionXSignaling({\n url: this.url,\n autoReconnect: this.autoReconnect,\n onOpen: () => this._onSignalingOpen(),\n onMessage: (msg) => this._onSignalingMessage(msg),\n onClose: (evt) => this._onSignalingClose(evt),\n onError: (err) => this._emit('error', err),\n });\n this._signaling.connect();\n }\n\n /** \uC5F0\uACB0 \uC885\uB8CC. */\n disconnect() {\n if (this._signaling) {\n this._signaling.sendLeave();\n this._signaling.disconnect();\n }\n this._closePeerConnection();\n this._slots.clear();\n this._connected = false;\n }\n\n /** \uCC44\uD305 \uC804\uC1A1. */\n chat(text) {\n if (this._signaling) {\n this._signaling.sendChat(text, this.nickname);\n }\n }\n\n /** \uD0A4\uD504\uB808\uC784 \uC694\uCCAD. */\n requestKeyframe() {\n if (this._signaling) {\n this._signaling.sendPLI();\n }\n }\n\n /** @returns {Map} \uD604\uC7AC \uC2AC\uB86F \uC815\uBCF4 */\n get slots() {\n return this._slots;\n }\n\n // \u2500\u2500 Internal \u2500\u2500\n\n /** @private */\n _onSignalingOpen() {\n this._signaling.sendJoin(this.peerId, this.channelId, {\n nickname: this.nickname,\n token: this.token,\n mode: this.mode,\n });\n // Join \uC804\uC1A1 \uD6C4 \uBC14\uB85C PeerConnection \uC0DD\uC131\n this._createPeerConnection().catch((e) => this._emit('error', e));\n }\n\n /** @private */\n _onSignalingMessage(msg) {\n switch (msg.type) {\n case SignalType.ANSWER:\n this._handleAnswer(msg);\n break;\n case SignalType.CANDIDATE:\n this._handleCandidate(msg);\n break;\n case SignalType.SLOT_INFO:\n this._handleSlotInfo(msg);\n break;\n case SignalType.CHAT:\n this._emit('chat', {\n peerId: msg.peer_id,\n nickname: msg.nickname,\n text: msg.text,\n });\n break;\n case SignalType.ERROR:\n this._emit('error', new Error(msg.message));\n break;\n }\n }\n\n /** @private */\n _onSignalingClose(evt) {\n this._closePeerConnection();\n this._connected = false;\n this._emit('close', evt);\n }\n\n /** @private */\n async _handleAnswer(msg) {\n if (!this._pc) return;\n try {\n await this._pc.setRemoteDescription(\n new RTCSessionDescription({ type: 'answer', sdp: msg.sdp })\n );\n // Flush queued ICE candidates\n for (const c of this._candidateQueue) {\n await this._pc.addIceCandidate(c);\n }\n this._candidateQueue = [];\n } catch (e) {\n this._emit('error', e);\n }\n }\n\n /** @private */\n async _handleCandidate(msg) {\n const candidate = new RTCIceCandidate({\n candidate: msg.candidate,\n sdpMid: msg.sdp_mid,\n sdpMLineIndex: msg.sdp_m_line_index,\n });\n if (this._pc && this._pc.remoteDescription) {\n try {\n await this._pc.addIceCandidate(candidate);\n } catch (e) {\n console.warn('[FuzionX] ICE candidate error:', e);\n }\n } else {\n this._candidateQueue.push(candidate);\n }\n }\n\n /** @private */\n _handleSlotInfo(msg) {\n const slotIndex = parseInt(msg.stream_id.replace('stream_', ''), 10);\n\n // nickname\uC774 \uBE48 \uBB38\uC790\uC5F4\uC774\uBA74 \uC2AC\uB86F \uD574\uC81C\n if (!msg.nickname || msg.nickname === '') {\n this._slots.delete(slotIndex);\n this._emit('slot_remove', { slotIndex, senderId: msg.sender_id });\n return;\n }\n\n const existing = this._slots.get(slotIndex) || {};\n this._slots.set(slotIndex, {\n ...existing,\n slotIndex,\n streamId: msg.stream_id,\n nickname: msg.nickname,\n senderId: msg.sender_id,\n });\n this._emit('slot', this._slots.get(slotIndex));\n }\n\n /**\n * PeerConnection \uC0DD\uC131 + Non-Trickle ICE Offer.\n * old alloy-player \uAC80\uC99D \uD328\uD134:\n * 1. addTransceiver(recvonly) \u00D7 maxSlots \u2014 \uC11C\uBC84 m-line \uB9E4\uD551\n * 2. createOffer \u2192 H264 SDP \uAC15\uC81C\n * 3. ICE gathering \uC644\uB8CC \uB300\uAE30 \uD6C4 SDP \uC804\uCCB4 \uC804\uC1A1 (Non-Trickle)\n * @private\n */\n async _createPeerConnection() {\n this._pc = new RTCPeerConnection(this.rtcConfig);\n\n // Non-Trickle: ICE candidate\uB294 SDP\uC5D0 \uD3EC\uD568\uB418\uC5B4 \uC804\uC1A1\uB428\n // (onicecandidate\uB294 gathering \uCD94\uC801\uC6A9\uC73C\uB85C\uB9CC \uC720\uC9C0)\n\n // \uC218\uC2E0 \uD2B8\uB799 \uB9E4\uD551\n let videoTrackCount = 0;\n this._pc.ontrack = (event) => {\n const track = event.track;\n\n if (track.kind === 'video') {\n const slotIndex = videoTrackCount++;\n\n let stream = event.streams[0];\n if (!stream) {\n stream = new MediaStream();\n stream.addTrack(track);\n }\n\n const slot = this._slots.get(slotIndex) || { slotIndex };\n slot.stream = stream;\n this._slots.set(slotIndex, slot);\n\n this._emit('stream', stream, slotIndex);\n\n if (!this._connected) {\n this._connected = true;\n this._emit('connected');\n\n // \uC989\uC2DC \uD0A4\uD504\uB808\uC784 \uC694\uCCAD (\uCCAB \uD504\uB808\uC784 \uBE60\uB974\uAC8C)\n if (this._signaling) {\n this._signaling.sendPLI();\n }\n }\n }\n };\n\n this._pc.onconnectionstatechange = () => {\n const state = this._pc?.connectionState;\n if (state === 'failed' || state === 'disconnected') {\n this._emit('error', new Error(`PeerConnection ${state}`));\n }\n };\n\n // recvonly Transceiver \uCD94\uAC00 (\uC11C\uBC84 \uC2AC\uB86F \uC218\uC5D0 \uB9DE\uCDA4)\n for (let i = 0; i < this._maxSlots; i++) {\n this._pc.addTransceiver('video', { direction: 'recvonly' });\n this._pc.addTransceiver('audio', { direction: 'recvonly' });\n }\n\n // Offer \uC0DD\uC131 + H264/Opus \uCF54\uB371 \uAC15\uC81C\n const offer = await this._pc.createOffer();\n offer.sdp = FuzionXViewer._forceCodecs(offer.sdp);\n await this._pc.setLocalDescription(offer);\n\n // Non-Trickle ICE: gathering \uC644\uB8CC \uB300\uAE30 \uD6C4 \uC804\uCCB4 SDP \uC804\uC1A1\n await this._waitForIceGathering();\n\n // gathering \uC644\uB8CC \uD6C4 \uCD5C\uC885 SDP (ICE candidates \uD3EC\uD568) \uC804\uC1A1\n const finalSdp = this._pc.localDescription?.sdp;\n if (finalSdp) {\n this._signaling.sendOffer(finalSdp);\n }\n }\n\n /**\n * ICE gathering \uC644\uB8CC \uB300\uAE30 (Non-Trickle)\n * @private\n */\n _waitForIceGathering() {\n return new Promise((resolve) => {\n if (this._pc.iceGatheringState === 'complete') {\n return resolve();\n }\n const check = () => {\n if (this._pc?.iceGatheringState === 'complete') {\n this._pc.removeEventListener('icegatheringstatechange', check);\n resolve();\n }\n };\n this._pc.addEventListener('icegatheringstatechange', check);\n // 500ms \uD6C4 gathering \uC548 \uB05D\uB098\uBA74 \uD604\uC7AC SDP\uB85C \uC9C4\uD589\n setTimeout(() => {\n if (this._pc) {\n this._pc.removeEventListener('icegatheringstatechange', check);\n }\n resolve();\n }, 150);\n });\n }\n\n /**\n * SDP\uC5D0\uC11C H264 + Opus \uCF54\uB371 \uC6B0\uC120 \uAC15\uC81C.\n * @private\n */\n static _forceCodecs(sdp) {\n let lines = sdp.split('\\r\\n');\n\n // Video: H264 \uC6B0\uC120\n const videoIdx = lines.findIndex((l) => l.startsWith('m=video'));\n if (videoIdx !== -1) {\n const h264Pts = [];\n const h264Scores = new Map();\n lines.forEach((l) => {\n const m = l.match(/a=rtpmap:(\\d+) H264\\/90000/);\n if (m) {\n h264Pts.push(m[1]);\n h264Scores.set(m[1], 0);\n }\n });\n lines.forEach((l) => {\n if (l.startsWith('a=fmtp:')) {\n const pt = l.split(' ')[0].split(':')[1];\n if (h264Scores.has(pt)) {\n if (l.includes('profile-level-id=42e01f')) h264Scores.set(pt, 100);\n else if (l.includes('profile-level-id=42001f')) h264Scores.set(pt, 80);\n if (l.includes('packetization-mode=1')) h264Scores.set(pt, (h264Scores.get(pt) || 0) + 10);\n }\n }\n });\n if (h264Pts.length > 0) {\n h264Pts.sort((a, b) => h264Scores.get(b) - h264Scores.get(a));\n const parts = lines[videoIdx].split(' ');\n const otherPts = parts.slice(3).filter((pt) => !h264Pts.includes(pt));\n lines[videoIdx] = [...parts.slice(0, 3), ...h264Pts, ...otherPts].join(' ');\n }\n }\n\n // Audio: Opus \uC6B0\uC120\n const audioIdx = lines.findIndex((l) => l.startsWith('m=audio'));\n if (audioIdx !== -1) {\n const opusPts = [];\n lines.forEach((l) => {\n const m = l.match(/a=rtpmap:(\\d+) opus\\/48000/);\n if (m) opusPts.push(m[1]);\n });\n if (opusPts.length > 0) {\n const parts = lines[audioIdx].split(' ');\n const otherPts = parts.slice(3).filter((pt) => !opusPts.includes(pt));\n lines[audioIdx] = [...parts.slice(0, 3), ...opusPts, ...otherPts].join(' ');\n }\n }\n\n return lines.join('\\r\\n');\n }\n\n /** @private */\n _closePeerConnection() {\n if (this._pc) {\n this._pc.close();\n this._pc = null;\n }\n this._candidateQueue = [];\n }\n\n}\n", "/**\n * @fuzionx/player \u2014 FuzionXPublisher (\uC1A1\uCD9C\uC790 \uBAA8\uB4DC)\n *\n * \uB85C\uCEEC \uCE74\uBA54\uB77C/\uB9C8\uC774\uD06C \u2192 \uC11C\uBC84 \uC804\uC1A1.\n * 2\uAC00\uC9C0 \uBC29\uC2DD \uC9C0\uC6D0:\n * A. WebSocket \uBC29\uC2DD (videochat \uC591\uBC29\uD5A5)\n * B. WHIP \uBC29\uC2DD (OBS/\uB2E8\uBC29\uD5A5 \uBC29\uC1A1)\n *\n * @example WebSocket\n * const pub = new FuzionXPublisher({\n * url: 'wss://media:50002',\n * channelId: 'my-live',\n * mode: 'videochat',\n * nickname: '\uBC1C\uD45C\uC790',\n * });\n * pub.on('ready', () => console.log('Publishing!'));\n * pub.connect();\n *\n * @example WHIP\n * const pub = new FuzionXPublisher({\n * whipUrl: 'https://media:7777/whip/my-live',\n * });\n * pub.connect();\n */\n\nimport { FuzionXSignaling } from './FuzionXSignaling.js';\nimport { DEFAULT_ICE_SERVERS, SignalType, SessionMode, MAX_SLOTS } from './constants.js';\n\nexport class FuzionXPublisher {\n /**\n * @param {Object} opts\n * @param {string} [opts.url] - WebSocket URL (WS \uBC29\uC2DD)\n * @param {string} [opts.whipUrl] - WHIP URL (WHIP \uBC29\uC2DD)\n * @param {string} [opts.channelId] - \uCC44\uB110 ID (WS \uBC29\uC2DD \uD544\uC218)\n * @param {string} [opts.mode='broadcast'] - 'broadcast' | 'videochat'\n * @param {string} [opts.nickname] - \uB2C9\uB124\uC784\n * @param {string} [opts.token] - \uC778\uC99D \uD1A0\uD070\n * @param {string} [opts.peerId] - \uD53C\uC5B4 ID\n * @param {MediaStreamConstraints} [opts.media] - getUserMedia \uC81C\uC57D\n * @param {MediaStream} [opts.stream] - \uC774\uBBF8 \uD68D\uB4DD\uD55C MediaStream\n * @param {RTCConfiguration} [opts.rtcConfig] - WebRTC \uC124\uC815\n * @param {boolean} [opts.autoReconnect=true]\n */\n constructor(opts) {\n // WHIP or WebSocket\n this.whipUrl = opts.whipUrl || null;\n this.url = opts.url || null;\n this.hubUrl = opts.hubUrl || null;\n this.channelId = opts.channelId || null;\n this.mode = opts.mode || SessionMode.BROADCAST;\n this.nickname = opts.nickname || null;\n this.token = opts.token || null;\n this.peerId = opts.peerId || `pub-${Math.random().toString(36).slice(2, 10)}`;\n this.autoReconnect = opts.autoReconnect !== false;\n\n this.mediaConstraints = opts.media || { video: true, audio: true };\n this._externalStream = opts.stream || null;\n\n this.rtcConfig = opts.rtcConfig || {\n iceServers: DEFAULT_ICE_SERVERS,\n bundlePolicy: 'max-bundle',\n rtcpMuxPolicy: 'require',\n };\n\n /** @private */\n this._signaling = null;\n this._pc = null;\n this._localStream = null;\n this._listeners = {};\n this._candidateQueue = [];\n this._whipResourceUrl = null;\n this._maxSlots = MAX_SLOTS[this.mode] || 1;\n this._slots = new Map();\n this._connected = false;\n }\n\n // \u2500\u2500 Event System \u2500\u2500\n\n on(event, handler) {\n if (!this._listeners[event]) this._listeners[event] = [];\n this._listeners[event].push(handler);\n return this;\n }\n\n /** @private */\n _emit(event, ...args) {\n (this._listeners[event] || []).forEach((fn) => fn(...args));\n }\n\n // \u2500\u2500 Lifecycle \u2500\u2500\n\n /** \uC5F0\uACB0 \uC2DC\uC791 (\uBBF8\uB514\uC5B4 \uD68D\uB4DD \u2192 WebSocket \uB610\uB294 WHIP). */\n async connect() {\n try {\n // Hub \uB77C\uC6B0\uD305: hubUrl\uC774 \uC788\uC73C\uBA74 Hub\uC5D0\uC11C \uCC44\uB110 \uC0DD\uC131 (Origin \uD560\uB2F9)\n if (!this.url && !this.whipUrl && this.hubUrl && this.channelId) {\n // Publisher\uB294 POST \uC6B0\uC120: \uCC44\uB110 \uC0DD\uC131 \u2192 Origin ws_url \uBC18\uD658\n // GET\uC740 Viewer\uC6A9 (Edge \uBD84\uC0B0 \uD2B8\uB9AC\uAC70) \u2192 Publisher\uAC00 \uD638\uCD9C\uD558\uBA74 \uC548 \uB428\n const createRes = await fetch(`${this.hubUrl}/api/channels`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n channel_id: this.channelId,\n source_type: 'webrtc',\n }),\n });\n if (createRes.status === 409) {\n throw new Error(`Channel already exists: ${this.channelId}`);\n }\n if (!createRes.ok) throw new Error(`Failed to create channel: ${this.channelId}`);\n const data = await createRes.json();\n if (data.ws_url) {\n const isSecure = this.hubUrl.startsWith('https');\n this.url = data.ws_url.replace(/^ws(s?):/, isSecure ? 'wss:' : 'ws:');\n } else {\n const isSecure = this.hubUrl.startsWith('https');\n const wsProto = isSecure ? 'wss' : 'ws';\n this.url = `${wsProto}://${data.media_ip}:${data.webrtc_port}`;\n }\n }\n\n await this._acquireMedia();\n\n if (this.whipUrl) {\n await this._connectWhip();\n } else if (this.url) {\n this._connectWebSocket();\n } else {\n throw new Error('url, hubUrl, \uB610\uB294 whipUrl \uC911 \uD558\uB098\uB97C \uC9C0\uC815\uD574\uC57C \uD569\uB2C8\uB2E4.');\n }\n } catch (e) {\n this._emit('error', e);\n }\n }\n\n /** \uC5F0\uACB0 \uC885\uB8CC. */\n async disconnect() {\n if (this.whipUrl && this._whipResourceUrl) {\n // WHIP DELETE\n try {\n const baseUrl = new URL(this.whipUrl).origin;\n await fetch(`${baseUrl}${this._whipResourceUrl}`, { method: 'DELETE' });\n } catch (e) {\n console.warn('[FuzionX] WHIP DELETE error:', e);\n }\n this._whipResourceUrl = null;\n }\n\n if (this._signaling) {\n this._signaling.sendLeave();\n this._signaling.disconnect();\n }\n\n this._closePeerConnection();\n this._stopMedia();\n this._connected = false;\n }\n\n /** \uCC44\uD305 \uC804\uC1A1. */\n chat(text) {\n if (this._signaling) {\n this._signaling.sendChat(text, this.nickname);\n }\n }\n\n /** @returns {MediaStream|null} \uB85C\uCEEC \uC2A4\uD2B8\uB9BC */\n get localStream() {\n return this._localStream;\n }\n\n /** @returns {Map} \uC2AC\uB86F \uC815\uBCF4 (videochat: \uB2E4\uB978 \uCC38\uAC00\uC790) */\n get slots() {\n return this._slots;\n }\n\n // \u2500\u2500 Media \u2500\u2500\n\n /** @private \uBBF8\uB514\uC5B4 \uD68D\uB4DD */\n async _acquireMedia() {\n if (this._externalStream) {\n this._localStream = this._externalStream;\n } else {\n this._localStream = await navigator.mediaDevices.getUserMedia(this.mediaConstraints);\n }\n this._emit('media', this._localStream);\n }\n\n /** @private */\n _stopMedia() {\n if (this._localStream && !this._externalStream) {\n this._localStream.getTracks().forEach((t) => t.stop());\n }\n this._localStream = null;\n }\n\n // \u2500\u2500 WHIP \u2500\u2500\n\n /** @private WHIP \uC5F0\uACB0 */\n async _connectWhip() {\n this._pc = new RTCPeerConnection(this.rtcConfig);\n\n // \uD2B8\uB799 \uCD94\uAC00\n this._localStream.getTracks().forEach((track) => {\n this._pc.addTrack(track, this._localStream);\n });\n\n // ICE Gathering \uC644\uB8CC \uB300\uAE30\n const offer = await this._pc.createOffer();\n offer.sdp = FuzionXPublisher._forceCodecs(offer.sdp);\n await this._pc.setLocalDescription(offer);\n\n // Gathering \uC644\uB8CC \uB300\uAE30\n await new Promise((resolve) => {\n if (this._pc.iceGatheringState === 'complete') {\n resolve();\n } else {\n this._pc.onicegatheringstatechange = () => {\n if (this._pc.iceGatheringState === 'complete') resolve();\n };\n // \uC548\uC804 \uD0C0\uC784\uC544\uC6C3\n setTimeout(resolve, 150);\n }\n });\n\n const localDesc = this._pc.localDescription;\n // Hub API\uC758 whip_url\uC740 \uC774\uBBF8 ?token=xxx \uD3EC\uD568 \uAC00\uB2A5\n let url = this.whipUrl;\n if (this.token && !url.includes('token=')) {\n url += (url.includes('?') ? '&' : '?') + `token=${this.token}`;\n }\n\n const response = await fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/sdp' },\n body: localDesc.sdp,\n });\n\n if (response.status !== 201) {\n throw new Error(`WHIP failed: ${response.status} ${await response.text()}`);\n }\n\n const answerSdp = await response.text();\n this._whipResourceUrl = response.headers.get('location');\n\n await this._pc.setRemoteDescription(\n new RTCSessionDescription({ type: 'answer', sdp: answerSdp })\n );\n\n this._pc.onconnectionstatechange = () => {\n const state = this._pc?.connectionState;\n if (state === 'connected') {\n this._connected = true;\n this._emit('ready');\n } else if (state === 'failed' || state === 'disconnected') {\n this._emit('error', new Error(`WHIP PeerConnection ${state}`));\n }\n };\n\n this._emit('ready');\n }\n\n // \u2500\u2500 WebSocket \u2500\u2500\n\n /** @private WS \uC5F0\uACB0 */\n _connectWebSocket() {\n this._signaling = new FuzionXSignaling({\n url: this.url,\n autoReconnect: this.autoReconnect,\n onOpen: () => this._onSignalingOpen(),\n onMessage: (msg) => this._onSignalingMessage(msg),\n onClose: (evt) => {\n this._closePeerConnection();\n this._connected = false;\n this._emit('close', evt);\n },\n onError: (err) => this._emit('error', err),\n });\n this._signaling.connect();\n }\n\n /** @private */\n async _onSignalingOpen() {\n this._signaling.sendJoin(this.peerId, this.channelId, {\n nickname: this.nickname,\n token: this.token,\n mode: this.mode,\n });\n await this._createPeerConnection();\n }\n\n /** @private */\n async _createPeerConnection() {\n this._pc = new RTCPeerConnection(this.rtcConfig);\n\n // Non-Trickle: ICE candidate\uB294 SDP\uC5D0 \uD3EC\uD568\n\n // \uB85C\uCEEC \uD2B8\uB799 \uCD94\uAC00 (\uC1A1\uCD9C)\n this._localStream.getTracks().forEach((track) => {\n this._pc.addTrack(track, this._localStream);\n });\n\n // videochat \uBAA8\uB4DC: \uC218\uC2E0 \uD2B8\uB799\uB3C4 \uC900\uBE44 (\uC591\uBC29\uD5A5)\n if (this.mode === SessionMode.VIDEOCHAT) {\n for (let i = 0; i < this._maxSlots; i++) {\n this._pc.addTransceiver('video', { direction: 'recvonly' });\n this._pc.addTransceiver('audio', { direction: 'recvonly' });\n }\n\n // \uC1A1\uCD9C \uD2B8\uB799\uC740 addTrack\uC73C\uB85C \uCD94\uAC00\uB428 \u2192 transceiver\uBC29\uD5A5\uC744 sendrecv\uB85C \uC5C5\uADF8\uB808\uC774\uB4DC\n const transceivers = this._pc.getTransceivers();\n transceivers.forEach((t) => {\n if (t.sender.track && t.direction === 'recvonly') {\n t.direction = 'sendrecv';\n }\n });\n\n let videoTrackCount = 0;\n this._pc.ontrack = (event) => {\n const track = event.track;\n if (track.kind === 'video') {\n const slotIndex = videoTrackCount++;\n let stream = event.streams[0];\n if (!stream) {\n stream = new MediaStream();\n stream.addTrack(track);\n }\n const slot = this._slots.get(slotIndex) || { slotIndex };\n slot.stream = stream;\n this._slots.set(slotIndex, slot);\n this._emit('stream', stream, slotIndex);\n }\n };\n }\n\n this._pc.onconnectionstatechange = () => {\n const state = this._pc?.connectionState;\n if (state === 'connected' && !this._connected) {\n this._connected = true;\n this._emit('ready');\n } else if (state === 'failed' || state === 'disconnected') {\n this._emit('error', new Error(`PeerConnection ${state}`));\n }\n };\n\n // Offer + H264/Opus SDP \uAC15\uC81C\n const offer = await this._pc.createOffer();\n offer.sdp = FuzionXPublisher._forceCodecs(offer.sdp);\n await this._pc.setLocalDescription(offer);\n\n // Non-Trickle: ICE gathering \uC644\uB8CC \uB300\uAE30\n await this._waitForIceGathering();\n\n const finalSdp = this._pc.localDescription?.sdp;\n if (finalSdp) {\n this._signaling.sendOffer(finalSdp);\n }\n }\n\n /** @private */\n _waitForIceGathering() {\n return new Promise((resolve) => {\n if (this._pc.iceGatheringState === 'complete') {\n return resolve();\n }\n const check = () => {\n if (this._pc?.iceGatheringState === 'complete') {\n this._pc.removeEventListener('icegatheringstatechange', check);\n resolve();\n }\n };\n this._pc.addEventListener('icegatheringstatechange', check);\n setTimeout(() => {\n if (this._pc) {\n this._pc.removeEventListener('icegatheringstatechange', check);\n }\n resolve();\n }, 150);\n });\n }\n\n /** @private */\n static _forceCodecs(sdp) {\n let lines = sdp.split('\\r\\n');\n const videoIdx = lines.findIndex((l) => l.startsWith('m=video'));\n if (videoIdx !== -1) {\n const h264Pts = [];\n const h264Scores = new Map();\n lines.forEach((l) => {\n const m = l.match(/a=rtpmap:(\\d+) H264\\/90000/);\n if (m) { h264Pts.push(m[1]); h264Scores.set(m[1], 0); }\n });\n lines.forEach((l) => {\n if (l.startsWith('a=fmtp:')) {\n const pt = l.split(' ')[0].split(':')[1];\n if (h264Scores.has(pt)) {\n if (l.includes('profile-level-id=42e01f')) h264Scores.set(pt, 100);\n else if (l.includes('profile-level-id=42001f')) h264Scores.set(pt, 80);\n if (l.includes('packetization-mode=1')) h264Scores.set(pt, (h264Scores.get(pt) || 0) + 10);\n }\n }\n });\n if (h264Pts.length > 0) {\n h264Pts.sort((a, b) => h264Scores.get(b) - h264Scores.get(a));\n const parts = lines[videoIdx].split(' ');\n const otherPts = parts.slice(3).filter((pt) => !h264Pts.includes(pt));\n lines[videoIdx] = [...parts.slice(0, 3), ...h264Pts, ...otherPts].join(' ');\n }\n }\n const audioIdx = lines.findIndex((l) => l.startsWith('m=audio'));\n if (audioIdx !== -1) {\n const opusPts = [];\n lines.forEach((l) => {\n const m = l.match(/a=rtpmap:(\\d+) opus\\/48000/);\n if (m) opusPts.push(m[1]);\n });\n if (opusPts.length > 0) {\n const parts = lines[audioIdx].split(' ');\n const otherPts = parts.slice(3).filter((pt) => !opusPts.includes(pt));\n lines[audioIdx] = [...parts.slice(0, 3), ...opusPts, ...otherPts].join(' ');\n }\n }\n return lines.join('\\r\\n');\n }\n\n /** @private */\n _onSignalingMessage(msg) {\n switch (msg.type) {\n case SignalType.ANSWER:\n this._handleAnswer(msg);\n break;\n case SignalType.CANDIDATE:\n this._handleCandidate(msg);\n break;\n case SignalType.SLOT_INFO:\n this._handleSlotInfo(msg);\n break;\n case SignalType.CHAT:\n this._emit('chat', {\n peerId: msg.peer_id,\n nickname: msg.nickname,\n text: msg.text,\n });\n break;\n case SignalType.ERROR:\n this._emit('error', new Error(msg.message));\n break;\n }\n }\n\n /** @private */\n async _handleAnswer(msg) {\n if (!this._pc) return;\n try {\n await this._pc.setRemoteDescription(\n new RTCSessionDescription({ type: 'answer', sdp: msg.sdp })\n );\n for (const c of this._candidateQueue) {\n await this._pc.addIceCandidate(c);\n }\n this._candidateQueue = [];\n } catch (e) {\n this._emit('error', e);\n }\n }\n\n /** @private */\n async _handleCandidate(msg) {\n const candidate = new RTCIceCandidate({\n candidate: msg.candidate,\n sdpMid: msg.sdp_mid,\n sdpMLineIndex: msg.sdp_m_line_index,\n });\n if (this._pc && this._pc.remoteDescription) {\n try { await this._pc.addIceCandidate(candidate); } catch (e) { /* ignore */ }\n } else {\n this._candidateQueue.push(candidate);\n }\n }\n\n /** @private */\n _handleSlotInfo(msg) {\n const slotIndex = parseInt(msg.stream_id.replace('stream_', ''), 10);\n if (!msg.nickname || msg.nickname === '') {\n this._slots.delete(slotIndex);\n this._emit('slot_remove', { slotIndex, senderId: msg.sender_id });\n return;\n }\n const existing = this._slots.get(slotIndex) || {};\n this._slots.set(slotIndex, {\n ...existing,\n slotIndex,\n streamId: msg.stream_id,\n nickname: msg.nickname,\n senderId: msg.sender_id,\n });\n this._emit('slot', this._slots.get(slotIndex));\n }\n\n /** @private */\n _closePeerConnection() {\n if (this._pc) {\n this._pc.close();\n this._pc = null;\n }\n this._candidateQueue = [];\n }\n}\n"],
5
+ "mappings": "AAKO,IAAMA,EAAsB,CACjC,CAAE,KAAM,8BAA+B,EACvC,CAAE,KAAM,+BAAgC,CAC1C,EAGaC,EAAa,CACxB,KAAM,OACN,MAAO,QACP,OAAQ,SACR,UAAW,YACX,IAAK,MACL,MAAO,QACP,UAAW,YACX,KAAM,OACN,MAAO,OACT,EAGaC,EAAc,CACzB,UAAW,YACX,UAAW,WACb,EAGaC,EAAY,CACvB,CAACD,EAAY,SAAS,EAAG,EACzB,CAACA,EAAY,SAAS,EAAG,CAC3B,EAGaE,EAAY,CACvB,YAAa,EACb,cAAe,IACf,aAAc,GAChB,EAGaC,EAAQ,CACnB,WAAY,aACZ,WAAY,aACZ,YAAa,IACb,YAAa,IACf,ECvCO,IAAMC,EAAN,KAAuB,CAU5B,YAAYC,EAAM,CAChB,KAAK,IAAMA,EAAK,IAChB,KAAK,UAAYA,EAAK,YAAc,IAAM,CAAC,GAC3C,KAAK,OAASA,EAAK,SAAW,IAAM,CAAC,GACrC,KAAK,QAAUA,EAAK,UAAY,IAAM,CAAC,GACvC,KAAK,QAAUA,EAAK,UAAY,IAAM,CAAC,GACvC,KAAK,cAAgBA,EAAK,gBAAkB,GAG5C,KAAK,IAAM,KACX,KAAK,YAAc,EACnB,KAAK,gBAAkB,KACvB,KAAK,kBAAoB,EAC3B,CAGA,SAAU,CACR,KAAK,kBAAoB,GACzB,KAAK,WAAW,CAClB,CAGA,YAAa,CACX,GAAI,CACF,KAAK,IAAM,IAAI,UAAU,KAAK,GAAG,CACnC,OAAS,EAAG,CACV,KAAK,QAAQ,CAAC,EACd,KAAK,mBAAmB,EACxB,MACF,CAEA,KAAK,IAAI,OAAS,IAAM,CACtB,KAAK,YAAc,EACnB,KAAK,OAAO,CACd,EAEA,KAAK,IAAI,UAAaC,GAAU,CAC9B,GAAI,CACF,IAAMC,EAAM,KAAK,MAAMD,EAAM,IAAI,EACjC,KAAK,UAAUC,CAAG,CACpB,MAAY,CACV,QAAQ,KAAK,0BAA2BD,EAAM,IAAI,CACpD,CACF,EAEA,KAAK,IAAI,QAAWA,GAAU,CAC5B,KAAK,QAAQA,CAAK,EACd,CAAC,KAAK,mBAAqB,KAAK,eAClC,KAAK,mBAAmB,CAE5B,EAEA,KAAK,IAAI,QAAWA,GAAU,CAC5B,KAAK,QAAQA,CAAK,CACpB,CACF,CAGA,KAAKC,EAAK,CACR,OAAI,KAAK,KAAO,KAAK,IAAI,aAAe,UAAU,MAChD,KAAK,IAAI,KAAK,KAAK,UAAUA,CAAG,CAAC,EAC1B,IAEF,EACT,CAKA,SAASC,EAAQC,EAAWJ,EAAO,CAAC,EAAG,CACrC,OAAO,KAAK,KAAK,CACf,KAAMK,EAAW,KACjB,QAASF,EACT,WAAYC,EACZ,SAAUJ,EAAK,UAAY,KAC3B,MAAOA,EAAK,OAAS,KACrB,KAAMA,EAAK,MAAQ,IACrB,CAAC,CACH,CAGA,UAAUM,EAAK,CACb,OAAO,KAAK,KAAK,CAAE,KAAMD,EAAW,MAAO,IAAAC,CAAI,CAAC,CAClD,CAGA,WAAWA,EAAK,CACd,OAAO,KAAK,KAAK,CAAE,KAAMD,EAAW,OAAQ,IAAAC,CAAI,CAAC,CACnD,CAGA,cAAcC,EAAW,CACvB,OAAO,KAAK,KAAK,CACf,KAAMF,EAAW,UACjB,UAAWE,EAAU,UACrB,QAASA,EAAU,OACnB,iBAAkBA,EAAU,aAC9B,CAAC,CACH,CAGA,SAASC,EAAMC,EAAU,CACvB,OAAO,KAAK,KAAK,CACf,KAAMJ,EAAW,KACjB,KAAAG,EACA,SAAUC,GAAY,KACtB,QAAS,IACX,CAAC,CACH,CAGA,SAAU,CACR,OAAO,KAAK,KAAK,CAAE,KAAMJ,EAAW,GAAI,CAAC,CAC3C,CAGA,WAAY,CACV,OAAO,KAAK,KAAK,CAAE,KAAMA,EAAW,KAAM,CAAC,CAC7C,CAGA,YAAa,CACX,KAAK,kBAAoB,GACzB,aAAa,KAAK,eAAe,EAC7B,KAAK,MACP,KAAK,IAAI,MAAM,EACf,KAAK,IAAM,KAEf,CAGA,IAAI,WAAY,CACd,OAAO,KAAK,KAAO,KAAK,IAAI,aAAe,UAAU,IACvD,CAGA,oBAAqB,CACnB,GAAI,KAAK,aAAeK,EAAU,YAAa,CAC7C,QAAQ,MAAM,0CAA0C,EACxD,KAAK,QAAQ,IAAI,MAAM,uBAAuB,CAAC,EAC/C,MACF,CACA,IAAMC,EAAQ,KAAK,IACjBD,EAAU,cAAgB,KAAK,IAAI,EAAG,KAAK,WAAW,EACtDA,EAAU,YACZ,EACA,KAAK,cACL,QAAQ,IAAI,6BAA6BC,CAAK,OAAO,KAAK,WAAW,IAAID,EAAU,WAAW,GAAG,EACjG,KAAK,gBAAkB,WAAW,IAAM,KAAK,WAAW,EAAGC,CAAK,CAClE,CACF,ECpJO,IAAMC,EAAN,MAAMC,CAAc,CAazB,YAAYC,EAAM,CAChB,KAAK,IAAMA,EAAK,KAAO,KACvB,KAAK,OAASA,EAAK,QAAU,KAC7B,KAAK,UAAYA,EAAK,UACtB,KAAK,KAAOA,EAAK,MAAQC,EAAY,UACrC,KAAK,SAAWD,EAAK,UAAY,KACjC,KAAK,MAAQA,EAAK,OAAS,KAC3B,KAAK,OAASA,EAAK,QAAU,UAAU,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,MAAM,EAAG,EAAE,CAAC,GAC9E,KAAK,cAAgBA,EAAK,gBAAkB,GAE5C,KAAK,UAAYA,EAAK,WAAa,CACjC,WAAYE,EACZ,aAAc,aACd,cAAe,SACjB,EAGA,KAAK,WAAa,KAClB,KAAK,IAAM,KACX,KAAK,WAAa,CAAC,EACnB,KAAK,OAAS,IAAI,IAClB,KAAK,UAAYC,EAAU,KAAK,IAAI,GAAK,EACzC,KAAK,gBAAkB,CAAC,EACxB,KAAK,WAAa,EACpB,CASA,GAAGC,EAAOC,EAAS,CACjB,OAAK,KAAK,WAAWD,CAAK,IAAG,KAAK,WAAWA,CAAK,EAAI,CAAC,GACvD,KAAK,WAAWA,CAAK,EAAE,KAAKC,CAAO,EAC5B,IACT,CAGA,MAAMD,KAAUE,EAAM,EACnB,KAAK,WAAWF,CAAK,GAAK,CAAC,GAAG,QAASG,GAAOA,EAAG,GAAGD,CAAI,CAAC,CAC5D,CAKA,MAAM,SAAU,CAEd,GAAI,CAAC,KAAK,KAAO,KAAK,OACpB,GAAI,CACF,IAAME,EAAM,MAAM,MAAM,GAAG,KAAK,MAAM,iBAAiB,KAAK,SAAS,EAAE,EACvE,GAAI,CAACA,EAAI,GAAI,MAAM,IAAI,MAAM,sBAAsB,KAAK,SAAS,EAAE,EACnE,IAAMC,EAAO,MAAMD,EAAI,KAAK,EAE5B,GAAIC,EAAK,OAAQ,CAEf,IAAMC,EAAW,KAAK,OAAO,WAAW,OAAO,EAC/C,KAAK,IAAMD,EAAK,OAAO,QAAQ,WAAYC,EAAW,OAAS,KAAK,CACtE,KAAO,CAEL,IAAMC,EADW,KAAK,OAAO,WAAW,OAAO,EACpB,MAAQ,KACnC,KAAK,IAAM,GAAGA,CAAO,MAAMF,EAAK,QAAQ,IAAIA,EAAK,WAAW,EAC9D,CACF,OAAS,EAAG,CACV,KAAK,MAAM,QAAS,CAAC,EACrB,MACF,CAGF,GAAI,CAAC,KAAK,IAAK,CACb,KAAK,MAAM,QAAS,IAAI,MAAM,4EAA0B,CAAC,EACzD,MACF,CAEA,KAAK,WAAa,IAAIG,EAAiB,CACrC,IAAK,KAAK,IACV,cAAe,KAAK,cACpB,OAAQ,IAAM,KAAK,iBAAiB,EACpC,UAAYC,GAAQ,KAAK,oBAAoBA,CAAG,EAChD,QAAUC,GAAQ,KAAK,kBAAkBA,CAAG,EAC5C,QAAUC,GAAQ,KAAK,MAAM,QAASA,CAAG,CAC3C,CAAC,EACD,KAAK,WAAW,QAAQ,CAC1B,CAGA,YAAa,CACP,KAAK,aACP,KAAK,WAAW,UAAU,EAC1B,KAAK,WAAW,WAAW,GAE7B,KAAK,qBAAqB,EAC1B,KAAK,OAAO,MAAM,EAClB,KAAK,WAAa,EACpB,CAGA,KAAKC,EAAM,CACL,KAAK,YACP,KAAK,WAAW,SAASA,EAAM,KAAK,QAAQ,CAEhD,CAGA,iBAAkB,CACZ,KAAK,YACP,KAAK,WAAW,QAAQ,CAE5B,CAGA,IAAI,OAAQ,CACV,OAAO,KAAK,MACd,CAKA,kBAAmB,CACjB,KAAK,WAAW,SAAS,KAAK,OAAQ,KAAK,UAAW,CACpD,SAAU,KAAK,SACf,MAAO,KAAK,MACZ,KAAM,KAAK,IACb,CAAC,EAED,KAAK,sBAAsB,EAAE,MAAO,GAAM,KAAK,MAAM,QAAS,CAAC,CAAC,CAClE,CAGA,oBAAoBH,EAAK,CACvB,OAAQA,EAAI,KAAM,CAChB,KAAKI,EAAW,OACd,KAAK,cAAcJ,CAAG,EACtB,MACF,KAAKI,EAAW,UACd,KAAK,iBAAiBJ,CAAG,EACzB,MACF,KAAKI,EAAW,UACd,KAAK,gBAAgBJ,CAAG,EACxB,MACF,KAAKI,EAAW,KACd,KAAK,MAAM,OAAQ,CACjB,OAAQJ,EAAI,QACZ,SAAUA,EAAI,SACd,KAAMA,EAAI,IACZ,CAAC,EACD,MACF,KAAKI,EAAW,MACd,KAAK,MAAM,QAAS,IAAI,MAAMJ,EAAI,OAAO,CAAC,EAC1C,KACJ,CACF,CAGA,kBAAkBC,EAAK,CACrB,KAAK,qBAAqB,EAC1B,KAAK,WAAa,GAClB,KAAK,MAAM,QAASA,CAAG,CACzB,CAGA,MAAM,cAAcD,EAAK,CACvB,GAAK,KAAK,IACV,GAAI,CACF,MAAM,KAAK,IAAI,qBACb,IAAI,sBAAsB,CAAE,KAAM,SAAU,IAAKA,EAAI,GAAI,CAAC,CAC5D,EAEA,QAAWK,KAAK,KAAK,gBACnB,MAAM,KAAK,IAAI,gBAAgBA,CAAC,EAElC,KAAK,gBAAkB,CAAC,CAC1B,OAASC,EAAG,CACV,KAAK,MAAM,QAASA,CAAC,CACvB,CACF,CAGA,MAAM,iBAAiBN,EAAK,CAC1B,IAAMO,EAAY,IAAI,gBAAgB,CACpC,UAAWP,EAAI,UACf,OAAQA,EAAI,QACZ,cAAeA,EAAI,gBACrB,CAAC,EACD,GAAI,KAAK,KAAO,KAAK,IAAI,kBACvB,GAAI,CACF,MAAM,KAAK,IAAI,gBAAgBO,CAAS,CAC1C,OAASD,EAAG,CACV,QAAQ,KAAK,iCAAkCA,CAAC,CAClD,MAEA,KAAK,gBAAgB,KAAKC,CAAS,CAEvC,CAGA,gBAAgBP,EAAK,CACnB,IAAMQ,EAAY,SAASR,EAAI,UAAU,QAAQ,UAAW,EAAE,EAAG,EAAE,EAGnE,GAAI,CAACA,EAAI,UAAYA,EAAI,WAAa,GAAI,CACxC,KAAK,OAAO,OAAOQ,CAAS,EAC5B,KAAK,MAAM,cAAe,CAAE,UAAAA,EAAW,SAAUR,EAAI,SAAU,CAAC,EAChE,MACF,CAEA,IAAMS,EAAW,KAAK,OAAO,IAAID,CAAS,GAAK,CAAC,EAChD,KAAK,OAAO,IAAIA,EAAW,CACzB,GAAGC,EACH,UAAAD,EACA,SAAUR,EAAI,UACd,SAAUA,EAAI,SACd,SAAUA,EAAI,SAChB,CAAC,EACD,KAAK,MAAM,OAAQ,KAAK,OAAO,IAAIQ,CAAS,CAAC,CAC/C,CAUA,MAAM,uBAAwB,CAC5B,KAAK,IAAM,IAAI,kBAAkB,KAAK,SAAS,EAM/C,IAAIE,EAAkB,EACtB,KAAK,IAAI,QAAWnB,GAAU,CAC5B,IAAMoB,EAAQpB,EAAM,MAEpB,GAAIoB,EAAM,OAAS,QAAS,CAC1B,IAAMH,EAAYE,IAEdE,EAASrB,EAAM,QAAQ,CAAC,EACvBqB,IACHA,EAAS,IAAI,YACbA,EAAO,SAASD,CAAK,GAGvB,IAAME,EAAO,KAAK,OAAO,IAAIL,CAAS,GAAK,CAAE,UAAAA,CAAU,EACvDK,EAAK,OAASD,EACd,KAAK,OAAO,IAAIJ,EAAWK,CAAI,EAE/B,KAAK,MAAM,SAAUD,EAAQJ,CAAS,EAEjC,KAAK,aACR,KAAK,WAAa,GAClB,KAAK,MAAM,WAAW,EAGlB,KAAK,YACP,KAAK,WAAW,QAAQ,EAG9B,CACF,EAEA,KAAK,IAAI,wBAA0B,IAAM,CACvC,IAAMM,EAAQ,KAAK,KAAK,iBACpBA,IAAU,UAAYA,IAAU,iBAClC,KAAK,MAAM,QAAS,IAAI,MAAM,kBAAkBA,CAAK,EAAE,CAAC,CAE5D,EAGA,QAASC,EAAI,EAAGA,EAAI,KAAK,UAAWA,IAClC,KAAK,IAAI,eAAe,QAAS,CAAE,UAAW,UAAW,CAAC,EAC1D,KAAK,IAAI,eAAe,QAAS,CAAE,UAAW,UAAW,CAAC,EAI5D,IAAMC,EAAQ,MAAM,KAAK,IAAI,YAAY,EACzCA,EAAM,IAAM9B,EAAc,aAAa8B,EAAM,GAAG,EAChD,MAAM,KAAK,IAAI,oBAAoBA,CAAK,EAGxC,MAAM,KAAK,qBAAqB,EAGhC,IAAMC,EAAW,KAAK,IAAI,kBAAkB,IACxCA,GACF,KAAK,WAAW,UAAUA,CAAQ,CAEtC,CAMA,sBAAuB,CACrB,OAAO,IAAI,QAASC,GAAY,CAC9B,GAAI,KAAK,IAAI,oBAAsB,WACjC,OAAOA,EAAQ,EAEjB,IAAMC,EAAQ,IAAM,CACd,KAAK,KAAK,oBAAsB,aAClC,KAAK,IAAI,oBAAoB,0BAA2BA,CAAK,EAC7DD,EAAQ,EAEZ,EACA,KAAK,IAAI,iBAAiB,0BAA2BC,CAAK,EAE1D,WAAW,IAAM,CACX,KAAK,KACP,KAAK,IAAI,oBAAoB,0BAA2BA,CAAK,EAE/DD,EAAQ,CACV,EAAG,GAAG,CACR,CAAC,CACH,CAMA,OAAO,aAAaE,EAAK,CACvB,IAAIC,EAAQD,EAAI,MAAM;AAAA,CAAM,EAGtBE,EAAWD,EAAM,UAAWE,GAAMA,EAAE,WAAW,SAAS,CAAC,EAC/D,GAAID,IAAa,GAAI,CACnB,IAAME,EAAU,CAAC,EACXC,EAAa,IAAI,IAkBvB,GAjBAJ,EAAM,QAASE,GAAM,CACnB,IAAMG,EAAIH,EAAE,MAAM,4BAA4B,EAC1CG,IACFF,EAAQ,KAAKE,EAAE,CAAC,CAAC,EACjBD,EAAW,IAAIC,EAAE,CAAC,EAAG,CAAC,EAE1B,CAAC,EACDL,EAAM,QAASE,GAAM,CACnB,GAAIA,EAAE,WAAW,SAAS,EAAG,CAC3B,IAAMI,EAAKJ,EAAE,MAAM,GAAG,EAAE,CAAC,EAAE,MAAM,GAAG,EAAE,CAAC,EACnCE,EAAW,IAAIE,CAAE,IACfJ,EAAE,SAAS,yBAAyB,EAAGE,EAAW,IAAIE,EAAI,GAAG,EACxDJ,EAAE,SAAS,yBAAyB,GAAGE,EAAW,IAAIE,EAAI,EAAE,EACjEJ,EAAE,SAAS,sBAAsB,GAAGE,EAAW,IAAIE,GAAKF,EAAW,IAAIE,CAAE,GAAK,GAAK,EAAE,EAE7F,CACF,CAAC,EACGH,EAAQ,OAAS,EAAG,CACtBA,EAAQ,KAAK,CAACI,EAAGC,IAAMJ,EAAW,IAAII,CAAC,EAAIJ,EAAW,IAAIG,CAAC,CAAC,EAC5D,IAAME,EAAQT,EAAMC,CAAQ,EAAE,MAAM,GAAG,EACjCS,EAAWD,EAAM,MAAM,CAAC,EAAE,OAAQH,GAAO,CAACH,EAAQ,SAASG,CAAE,CAAC,EACpEN,EAAMC,CAAQ,EAAI,CAAC,GAAGQ,EAAM,MAAM,EAAG,CAAC,EAAG,GAAGN,EAAS,GAAGO,CAAQ,EAAE,KAAK,GAAG,CAC5E,CACF,CAGA,IAAMC,EAAWX,EAAM,UAAWE,GAAMA,EAAE,WAAW,SAAS,CAAC,EAC/D,GAAIS,IAAa,GAAI,CACnB,IAAMC,EAAU,CAAC,EAKjB,GAJAZ,EAAM,QAASE,GAAM,CACnB,IAAMG,EAAIH,EAAE,MAAM,4BAA4B,EAC1CG,GAAGO,EAAQ,KAAKP,EAAE,CAAC,CAAC,CAC1B,CAAC,EACGO,EAAQ,OAAS,EAAG,CACtB,IAAMH,EAAQT,EAAMW,CAAQ,EAAE,MAAM,GAAG,EACjCD,EAAWD,EAAM,MAAM,CAAC,EAAE,OAAQH,GAAO,CAACM,EAAQ,SAASN,CAAE,CAAC,EACpEN,EAAMW,CAAQ,EAAI,CAAC,GAAGF,EAAM,MAAM,EAAG,CAAC,EAAG,GAAGG,EAAS,GAAGF,CAAQ,EAAE,KAAK,GAAG,CAC5E,CACF,CAEA,OAAOV,EAAM,KAAK;AAAA,CAAM,CAC1B,CAGA,sBAAuB,CACjB,KAAK,MACP,KAAK,IAAI,MAAM,EACf,KAAK,IAAM,MAEb,KAAK,gBAAkB,CAAC,CAC1B,CAEF,ECpYO,IAAMa,EAAN,MAAMC,CAAiB,CAe5B,YAAYC,EAAM,CAEhB,KAAK,QAAUA,EAAK,SAAW,KAC/B,KAAK,IAAMA,EAAK,KAAO,KACvB,KAAK,OAASA,EAAK,QAAU,KAC7B,KAAK,UAAYA,EAAK,WAAa,KACnC,KAAK,KAAOA,EAAK,MAAQC,EAAY,UACrC,KAAK,SAAWD,EAAK,UAAY,KACjC,KAAK,MAAQA,EAAK,OAAS,KAC3B,KAAK,OAASA,EAAK,QAAU,OAAO,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,MAAM,EAAG,EAAE,CAAC,GAC3E,KAAK,cAAgBA,EAAK,gBAAkB,GAE5C,KAAK,iBAAmBA,EAAK,OAAS,CAAE,MAAO,GAAM,MAAO,EAAK,EACjE,KAAK,gBAAkBA,EAAK,QAAU,KAEtC,KAAK,UAAYA,EAAK,WAAa,CACjC,WAAYE,EACZ,aAAc,aACd,cAAe,SACjB,EAGA,KAAK,WAAa,KAClB,KAAK,IAAM,KACX,KAAK,aAAe,KACpB,KAAK,WAAa,CAAC,EACnB,KAAK,gBAAkB,CAAC,EACxB,KAAK,iBAAmB,KACxB,KAAK,UAAYC,EAAU,KAAK,IAAI,GAAK,EACzC,KAAK,OAAS,IAAI,IAClB,KAAK,WAAa,EACpB,CAIA,GAAGC,EAAOC,EAAS,CACjB,OAAK,KAAK,WAAWD,CAAK,IAAG,KAAK,WAAWA,CAAK,EAAI,CAAC,GACvD,KAAK,WAAWA,CAAK,EAAE,KAAKC,CAAO,EAC5B,IACT,CAGA,MAAMD,KAAUE,EAAM,EACnB,KAAK,WAAWF,CAAK,GAAK,CAAC,GAAG,QAASG,GAAOA,EAAG,GAAGD,CAAI,CAAC,CAC5D,CAKA,MAAM,SAAU,CACd,GAAI,CAEF,GAAI,CAAC,KAAK,KAAO,CAAC,KAAK,SAAW,KAAK,QAAU,KAAK,UAAW,CAG/D,IAAME,EAAY,MAAM,MAAM,GAAG,KAAK,MAAM,gBAAiB,CAC3D,OAAQ,OACR,QAAS,CAAE,eAAgB,kBAAmB,EAC9C,KAAM,KAAK,UAAU,CACnB,WAAY,KAAK,UACjB,YAAa,QACf,CAAC,CACH,CAAC,EACD,GAAIA,EAAU,SAAW,IACvB,MAAM,IAAI,MAAM,2BAA2B,KAAK,SAAS,EAAE,EAE7D,GAAI,CAACA,EAAU,GAAI,MAAM,IAAI,MAAM,6BAA6B,KAAK,SAAS,EAAE,EAChF,IAAMC,EAAO,MAAMD,EAAU,KAAK,EAClC,GAAIC,EAAK,OAAQ,CACf,IAAMC,EAAW,KAAK,OAAO,WAAW,OAAO,EAC/C,KAAK,IAAMD,EAAK,OAAO,QAAQ,WAAYC,EAAW,OAAS,KAAK,CACtE,KAAO,CAEL,IAAMC,EADW,KAAK,OAAO,WAAW,OAAO,EACpB,MAAQ,KACnC,KAAK,IAAM,GAAGA,CAAO,MAAMF,EAAK,QAAQ,IAAIA,EAAK,WAAW,EAC9D,CACF,CAIA,GAFA,MAAM,KAAK,cAAc,EAErB,KAAK,QACP,MAAM,KAAK,aAAa,UACf,KAAK,IACd,KAAK,kBAAkB,MAEvB,OAAM,IAAI,MAAM,0GAAyC,CAE7D,OAAS,EAAG,CACV,KAAK,MAAM,QAAS,CAAC,CACvB,CACF,CAGA,MAAM,YAAa,CACjB,GAAI,KAAK,SAAW,KAAK,iBAAkB,CAEzC,GAAI,CACF,IAAMG,EAAU,IAAI,IAAI,KAAK,OAAO,EAAE,OACtC,MAAM,MAAM,GAAGA,CAAO,GAAG,KAAK,gBAAgB,GAAI,CAAE,OAAQ,QAAS,CAAC,CACxE,OAAS,EAAG,CACV,QAAQ,KAAK,+BAAgC,CAAC,CAChD,CACA,KAAK,iBAAmB,IAC1B,CAEI,KAAK,aACP,KAAK,WAAW,UAAU,EAC1B,KAAK,WAAW,WAAW,GAG7B,KAAK,qBAAqB,EAC1B,KAAK,WAAW,EAChB,KAAK,WAAa,EACpB,CAGA,KAAKC,EAAM,CACL,KAAK,YACP,KAAK,WAAW,SAASA,EAAM,KAAK,QAAQ,CAEhD,CAGA,IAAI,aAAc,CAChB,OAAO,KAAK,YACd,CAGA,IAAI,OAAQ,CACV,OAAO,KAAK,MACd,CAKA,MAAM,eAAgB,CAChB,KAAK,gBACP,KAAK,aAAe,KAAK,gBAEzB,KAAK,aAAe,MAAM,UAAU,aAAa,aAAa,KAAK,gBAAgB,EAErF,KAAK,MAAM,QAAS,KAAK,YAAY,CACvC,CAGA,YAAa,CACP,KAAK,cAAgB,CAAC,KAAK,iBAC7B,KAAK,aAAa,UAAU,EAAE,QAASC,GAAMA,EAAE,KAAK,CAAC,EAEvD,KAAK,aAAe,IACtB,CAKA,MAAM,cAAe,CACnB,KAAK,IAAM,IAAI,kBAAkB,KAAK,SAAS,EAG/C,KAAK,aAAa,UAAU,EAAE,QAASC,GAAU,CAC/C,KAAK,IAAI,SAASA,EAAO,KAAK,YAAY,CAC5C,CAAC,EAGD,IAAMC,EAAQ,MAAM,KAAK,IAAI,YAAY,EACzCA,EAAM,IAAMjB,EAAiB,aAAaiB,EAAM,GAAG,EACnD,MAAM,KAAK,IAAI,oBAAoBA,CAAK,EAGxC,MAAM,IAAI,QAASC,GAAY,CACzB,KAAK,IAAI,oBAAsB,WACjCA,EAAQ,GAER,KAAK,IAAI,0BAA4B,IAAM,CACrC,KAAK,IAAI,oBAAsB,YAAYA,EAAQ,CACzD,EAEA,WAAWA,EAAS,GAAG,EAE3B,CAAC,EAED,IAAMC,EAAY,KAAK,IAAI,iBAEvBC,EAAM,KAAK,QACX,KAAK,OAAS,CAACA,EAAI,SAAS,QAAQ,IACtCA,IAAQA,EAAI,SAAS,GAAG,EAAI,IAAM,KAAO,SAAS,KAAK,KAAK,IAG9D,IAAMC,EAAW,MAAM,MAAMD,EAAK,CAChC,OAAQ,OACR,QAAS,CAAE,eAAgB,iBAAkB,EAC7C,KAAMD,EAAU,GAClB,CAAC,EAED,GAAIE,EAAS,SAAW,IACtB,MAAM,IAAI,MAAM,gBAAgBA,EAAS,MAAM,IAAI,MAAMA,EAAS,KAAK,CAAC,EAAE,EAG5E,IAAMC,EAAY,MAAMD,EAAS,KAAK,EACtC,KAAK,iBAAmBA,EAAS,QAAQ,IAAI,UAAU,EAEvD,MAAM,KAAK,IAAI,qBACb,IAAI,sBAAsB,CAAE,KAAM,SAAU,IAAKC,CAAU,CAAC,CAC9D,EAEA,KAAK,IAAI,wBAA0B,IAAM,CACvC,IAAMC,EAAQ,KAAK,KAAK,gBACpBA,IAAU,aACZ,KAAK,WAAa,GAClB,KAAK,MAAM,OAAO,IACTA,IAAU,UAAYA,IAAU,iBACzC,KAAK,MAAM,QAAS,IAAI,MAAM,uBAAuBA,CAAK,EAAE,CAAC,CAEjE,EAEA,KAAK,MAAM,OAAO,CACpB,CAKA,mBAAoB,CAClB,KAAK,WAAa,IAAIC,EAAiB,CACrC,IAAK,KAAK,IACV,cAAe,KAAK,cACpB,OAAQ,IAAM,KAAK,iBAAiB,EACpC,UAAYC,GAAQ,KAAK,oBAAoBA,CAAG,EAChD,QAAUC,GAAQ,CAChB,KAAK,qBAAqB,EAC1B,KAAK,WAAa,GAClB,KAAK,MAAM,QAASA,CAAG,CACzB,EACA,QAAUC,GAAQ,KAAK,MAAM,QAASA,CAAG,CAC3C,CAAC,EACD,KAAK,WAAW,QAAQ,CAC1B,CAGA,MAAM,kBAAmB,CACvB,KAAK,WAAW,SAAS,KAAK,OAAQ,KAAK,UAAW,CACpD,SAAU,KAAK,SACf,MAAO,KAAK,MACZ,KAAM,KAAK,IACb,CAAC,EACD,MAAM,KAAK,sBAAsB,CACnC,CAGA,MAAM,uBAAwB,CAW5B,GAVA,KAAK,IAAM,IAAI,kBAAkB,KAAK,SAAS,EAK/C,KAAK,aAAa,UAAU,EAAE,QAASX,GAAU,CAC/C,KAAK,IAAI,SAASA,EAAO,KAAK,YAAY,CAC5C,CAAC,EAGG,KAAK,OAASd,EAAY,UAAW,CACvC,QAAS0B,EAAI,EAAGA,EAAI,KAAK,UAAWA,IAClC,KAAK,IAAI,eAAe,QAAS,CAAE,UAAW,UAAW,CAAC,EAC1D,KAAK,IAAI,eAAe,QAAS,CAAE,UAAW,UAAW,CAAC,EAIvC,KAAK,IAAI,gBAAgB,EACjC,QAASb,GAAM,CACtBA,EAAE,OAAO,OAASA,EAAE,YAAc,aACpCA,EAAE,UAAY,WAElB,CAAC,EAED,IAAIc,EAAkB,EACtB,KAAK,IAAI,QAAWxB,GAAU,CAC5B,IAAMW,EAAQX,EAAM,MACpB,GAAIW,EAAM,OAAS,QAAS,CAC1B,IAAMc,EAAYD,IACdE,EAAS1B,EAAM,QAAQ,CAAC,EACvB0B,IACHA,EAAS,IAAI,YACbA,EAAO,SAASf,CAAK,GAEvB,IAAMgB,EAAO,KAAK,OAAO,IAAIF,CAAS,GAAK,CAAE,UAAAA,CAAU,EACvDE,EAAK,OAASD,EACd,KAAK,OAAO,IAAID,EAAWE,CAAI,EAC/B,KAAK,MAAM,SAAUD,EAAQD,CAAS,CACxC,CACF,CACF,CAEA,KAAK,IAAI,wBAA0B,IAAM,CACvC,IAAMP,EAAQ,KAAK,KAAK,gBACpBA,IAAU,aAAe,CAAC,KAAK,YACjC,KAAK,WAAa,GAClB,KAAK,MAAM,OAAO,IACTA,IAAU,UAAYA,IAAU,iBACzC,KAAK,MAAM,QAAS,IAAI,MAAM,kBAAkBA,CAAK,EAAE,CAAC,CAE5D,EAGA,IAAMN,EAAQ,MAAM,KAAK,IAAI,YAAY,EACzCA,EAAM,IAAMjB,EAAiB,aAAaiB,EAAM,GAAG,EACnD,MAAM,KAAK,IAAI,oBAAoBA,CAAK,EAGxC,MAAM,KAAK,qBAAqB,EAEhC,IAAMgB,EAAW,KAAK,IAAI,kBAAkB,IACxCA,GACF,KAAK,WAAW,UAAUA,CAAQ,CAEtC,CAGA,sBAAuB,CACrB,OAAO,IAAI,QAASf,GAAY,CAC9B,GAAI,KAAK,IAAI,oBAAsB,WACjC,OAAOA,EAAQ,EAEjB,IAAMgB,EAAQ,IAAM,CACd,KAAK,KAAK,oBAAsB,aAClC,KAAK,IAAI,oBAAoB,0BAA2BA,CAAK,EAC7DhB,EAAQ,EAEZ,EACA,KAAK,IAAI,iBAAiB,0BAA2BgB,CAAK,EAC1D,WAAW,IAAM,CACX,KAAK,KACP,KAAK,IAAI,oBAAoB,0BAA2BA,CAAK,EAE/DhB,EAAQ,CACV,EAAG,GAAG,CACR,CAAC,CACH,CAGA,OAAO,aAAaiB,EAAK,CACvB,IAAIC,EAAQD,EAAI,MAAM;AAAA,CAAM,EACtBE,EAAWD,EAAM,UAAWE,GAAMA,EAAE,WAAW,SAAS,CAAC,EAC/D,GAAID,IAAa,GAAI,CACnB,IAAME,EAAU,CAAC,EACXC,EAAa,IAAI,IAevB,GAdAJ,EAAM,QAASE,GAAM,CACnB,IAAMG,EAAIH,EAAE,MAAM,4BAA4B,EAC1CG,IAAKF,EAAQ,KAAKE,EAAE,CAAC,CAAC,EAAGD,EAAW,IAAIC,EAAE,CAAC,EAAG,CAAC,EACrD,CAAC,EACDL,EAAM,QAASE,GAAM,CACnB,GAAIA,EAAE,WAAW,SAAS,EAAG,CAC3B,IAAMI,EAAKJ,EAAE,MAAM,GAAG,EAAE,CAAC,EAAE,MAAM,GAAG,EAAE,CAAC,EACnCE,EAAW,IAAIE,CAAE,IACfJ,EAAE,SAAS,yBAAyB,EAAGE,EAAW,IAAIE,EAAI,GAAG,EACxDJ,EAAE,SAAS,yBAAyB,GAAGE,EAAW,IAAIE,EAAI,EAAE,EACjEJ,EAAE,SAAS,sBAAsB,GAAGE,EAAW,IAAIE,GAAKF,EAAW,IAAIE,CAAE,GAAK,GAAK,EAAE,EAE7F,CACF,CAAC,EACGH,EAAQ,OAAS,EAAG,CACtBA,EAAQ,KAAK,CAACI,EAAGC,IAAMJ,EAAW,IAAII,CAAC,EAAIJ,EAAW,IAAIG,CAAC,CAAC,EAC5D,IAAME,EAAQT,EAAMC,CAAQ,EAAE,MAAM,GAAG,EACjCS,EAAWD,EAAM,MAAM,CAAC,EAAE,OAAQH,GAAO,CAACH,EAAQ,SAASG,CAAE,CAAC,EACpEN,EAAMC,CAAQ,EAAI,CAAC,GAAGQ,EAAM,MAAM,EAAG,CAAC,EAAG,GAAGN,EAAS,GAAGO,CAAQ,EAAE,KAAK,GAAG,CAC5E,CACF,CACA,IAAMC,EAAWX,EAAM,UAAWE,GAAMA,EAAE,WAAW,SAAS,CAAC,EAC/D,GAAIS,IAAa,GAAI,CACnB,IAAMC,EAAU,CAAC,EAKjB,GAJAZ,EAAM,QAASE,GAAM,CACnB,IAAMG,EAAIH,EAAE,MAAM,4BAA4B,EAC1CG,GAAGO,EAAQ,KAAKP,EAAE,CAAC,CAAC,CAC1B,CAAC,EACGO,EAAQ,OAAS,EAAG,CACtB,IAAMH,EAAQT,EAAMW,CAAQ,EAAE,MAAM,GAAG,EACjCD,EAAWD,EAAM,MAAM,CAAC,EAAE,OAAQH,GAAO,CAACM,EAAQ,SAASN,CAAE,CAAC,EACpEN,EAAMW,CAAQ,EAAI,CAAC,GAAGF,EAAM,MAAM,EAAG,CAAC,EAAG,GAAGG,EAAS,GAAGF,CAAQ,EAAE,KAAK,GAAG,CAC5E,CACF,CACA,OAAOV,EAAM,KAAK;AAAA,CAAM,CAC1B,CAGA,oBAAoBX,EAAK,CACvB,OAAQA,EAAI,KAAM,CAChB,KAAKwB,EAAW,OACd,KAAK,cAAcxB,CAAG,EACtB,MACF,KAAKwB,EAAW,UACd,KAAK,iBAAiBxB,CAAG,EACzB,MACF,KAAKwB,EAAW,UACd,KAAK,gBAAgBxB,CAAG,EACxB,MACF,KAAKwB,EAAW,KACd,KAAK,MAAM,OAAQ,CACjB,OAAQxB,EAAI,QACZ,SAAUA,EAAI,SACd,KAAMA,EAAI,IACZ,CAAC,EACD,MACF,KAAKwB,EAAW,MACd,KAAK,MAAM,QAAS,IAAI,MAAMxB,EAAI,OAAO,CAAC,EAC1C,KACJ,CACF,CAGA,MAAM,cAAcA,EAAK,CACvB,GAAK,KAAK,IACV,GAAI,CACF,MAAM,KAAK,IAAI,qBACb,IAAI,sBAAsB,CAAE,KAAM,SAAU,IAAKA,EAAI,GAAI,CAAC,CAC5D,EACA,QAAWyB,KAAK,KAAK,gBACnB,MAAM,KAAK,IAAI,gBAAgBA,CAAC,EAElC,KAAK,gBAAkB,CAAC,CAC1B,OAASC,EAAG,CACV,KAAK,MAAM,QAASA,CAAC,CACvB,CACF,CAGA,MAAM,iBAAiB1B,EAAK,CAC1B,IAAM2B,EAAY,IAAI,gBAAgB,CACpC,UAAW3B,EAAI,UACf,OAAQA,EAAI,QACZ,cAAeA,EAAI,gBACrB,CAAC,EACD,GAAI,KAAK,KAAO,KAAK,IAAI,kBACvB,GAAI,CAAE,MAAM,KAAK,IAAI,gBAAgB2B,CAAS,CAAG,MAAY,CAAe,MAE5E,KAAK,gBAAgB,KAAKA,CAAS,CAEvC,CAGA,gBAAgB3B,EAAK,CACnB,IAAMK,EAAY,SAASL,EAAI,UAAU,QAAQ,UAAW,EAAE,EAAG,EAAE,EACnE,GAAI,CAACA,EAAI,UAAYA,EAAI,WAAa,GAAI,CACxC,KAAK,OAAO,OAAOK,CAAS,EAC5B,KAAK,MAAM,cAAe,CAAE,UAAAA,EAAW,SAAUL,EAAI,SAAU,CAAC,EAChE,MACF,CACA,IAAM4B,EAAW,KAAK,OAAO,IAAIvB,CAAS,GAAK,CAAC,EAChD,KAAK,OAAO,IAAIA,EAAW,CACzB,GAAGuB,EACH,UAAAvB,EACA,SAAUL,EAAI,UACd,SAAUA,EAAI,SACd,SAAUA,EAAI,SAChB,CAAC,EACD,KAAK,MAAM,OAAQ,KAAK,OAAO,IAAIK,CAAS,CAAC,CAC/C,CAGA,sBAAuB,CACjB,KAAK,MACP,KAAK,IAAI,MAAM,EACf,KAAK,IAAM,MAEb,KAAK,gBAAkB,CAAC,CAC1B,CACF",
6
+ "names": ["DEFAULT_ICE_SERVERS", "SignalType", "SessionMode", "MAX_SLOTS", "RECONNECT", "CODEC", "FuzionXSignaling", "opts", "event", "msg", "peerId", "channelId", "SignalType", "sdp", "candidate", "text", "nickname", "RECONNECT", "delay", "FuzionXViewer", "_FuzionXViewer", "opts", "SessionMode", "DEFAULT_ICE_SERVERS", "MAX_SLOTS", "event", "handler", "args", "fn", "res", "data", "isSecure", "wsProto", "FuzionXSignaling", "msg", "evt", "err", "text", "SignalType", "c", "e", "candidate", "slotIndex", "existing", "videoTrackCount", "track", "stream", "slot", "state", "i", "offer", "finalSdp", "resolve", "check", "sdp", "lines", "videoIdx", "l", "h264Pts", "h264Scores", "m", "pt", "a", "b", "parts", "otherPts", "audioIdx", "opusPts", "FuzionXPublisher", "_FuzionXPublisher", "opts", "SessionMode", "DEFAULT_ICE_SERVERS", "MAX_SLOTS", "event", "handler", "args", "fn", "createRes", "data", "isSecure", "wsProto", "baseUrl", "text", "t", "track", "offer", "resolve", "localDesc", "url", "response", "answerSdp", "state", "FuzionXSignaling", "msg", "evt", "err", "i", "videoTrackCount", "slotIndex", "stream", "slot", "finalSdp", "check", "sdp", "lines", "videoIdx", "l", "h264Pts", "h264Scores", "m", "pt", "a", "b", "parts", "otherPts", "audioIdx", "opusPts", "SignalType", "c", "e", "candidate", "existing"]
7
7
  }
@@ -1,7 +1,7 @@
1
- var FuzionXPlayer=(()=>{var C=Object.defineProperty;var E=Object.getOwnPropertyDescriptor;var I=Object.getOwnPropertyNames;var k=Object.prototype.hasOwnProperty;var T=(h,e)=>{for(var t in e)C(h,t,{get:e[t],enumerable:!0})},R=(h,e,t,c)=>{if(e&&typeof e=="object"||typeof e=="function")for(let a of I(e))!k.call(h,a)&&a!==t&&C(h,a,{get:()=>e[a],enumerable:!(c=E(e,a))||c.enumerable});return h};var y=h=>R(C({},"__esModule",{value:!0}),h);var O={};T(O,{CODEC:()=>w,DEFAULT_ICE_SERVERS:()=>u,FuzionXPublisher:()=>g,FuzionXSignaling:()=>_,FuzionXViewer:()=>m,MAX_SLOTS:()=>f,RECONNECT:()=>p,SessionMode:()=>d,SignalType:()=>r});var u=[{urls:"stun:stun.l.google.com:19302"},{urls:"stun:stun1.l.google.com:19302"}],r={JOIN:"join",OFFER:"offer",ANSWER:"answer",CANDIDATE:"candidate",PLI:"pli",LEAVE:"leave",SLOT_INFO:"slot_info",CHAT:"chat",ERROR:"error"},d={BROADCAST:"broadcast",VIDEOCHAT:"videochat"},f={[d.BROADCAST]:1,[d.VIDEOCHAT]:9},p={MAX_RETRIES:5,BASE_DELAY_MS:1e3,MAX_DELAY_MS:3e4},w={VIDEO_MIME:"video/H264",AUDIO_MIME:"audio/opus",VIDEO_CLOCK:9e4,AUDIO_CLOCK:48e3};var _=class{constructor(e){this.url=e.url,this.onMessage=e.onMessage||(()=>{}),this.onOpen=e.onOpen||(()=>{}),this.onClose=e.onClose||(()=>{}),this.onError=e.onError||(()=>{}),this.autoReconnect=e.autoReconnect!==!1,this._ws=null,this._retryCount=0,this._reconnectTimer=null,this._intentionalClose=!1}connect(){this._intentionalClose=!1,this._doConnect()}_doConnect(){try{this._ws=new WebSocket(this.url)}catch(e){this.onError(e),this._scheduleReconnect();return}this._ws.onopen=()=>{this._retryCount=0,this.onOpen()},this._ws.onmessage=e=>{try{let t=JSON.parse(e.data);this.onMessage(t)}catch{console.warn("[FuzionX] Invalid JSON:",e.data)}},this._ws.onclose=e=>{this.onClose(e),!this._intentionalClose&&this.autoReconnect&&this._scheduleReconnect()},this._ws.onerror=e=>{this.onError(e)}}send(e){return this._ws&&this._ws.readyState===WebSocket.OPEN?(this._ws.send(JSON.stringify(e)),!0):!1}sendJoin(e,t,c={}){return this.send({type:r.JOIN,peer_id:e,channel_id:t,nickname:c.nickname||null,token:c.token||null,mode:c.mode||null})}sendOffer(e){return this.send({type:r.OFFER,sdp:e})}sendAnswer(e){return this.send({type:r.ANSWER,sdp:e})}sendCandidate(e){return this.send({type:r.CANDIDATE,candidate:e.candidate,sdp_mid:e.sdpMid,sdp_m_line_index:e.sdpMLineIndex})}sendChat(e,t){return this.send({type:r.CHAT,text:e,nickname:t||null,peer_id:null})}sendPLI(){return this.send({type:r.PLI})}sendLeave(){return this.send({type:r.LEAVE})}disconnect(){this._intentionalClose=!0,clearTimeout(this._reconnectTimer),this._ws&&(this._ws.close(),this._ws=null)}get connected(){return this._ws&&this._ws.readyState===WebSocket.OPEN}_scheduleReconnect(){if(this._retryCount>=p.MAX_RETRIES){console.error("[FuzionX] Max reconnect retries reached."),this.onError(new Error("Max reconnect retries"));return}let e=Math.min(p.BASE_DELAY_MS*Math.pow(2,this._retryCount),p.MAX_DELAY_MS);this._retryCount++,console.log(`[FuzionX] Reconnecting in ${e}ms (${this._retryCount}/${p.MAX_RETRIES})`),this._reconnectTimer=setTimeout(()=>this._doConnect(),e)}};var m=class h{constructor(e){this.url=e.url,this.channelId=e.channelId,this.mode=e.mode||d.BROADCAST,this.nickname=e.nickname||null,this.token=e.token||null,this.peerId=e.peerId||`viewer-${Math.random().toString(36).slice(2,10)}`,this.autoReconnect=e.autoReconnect!==!1,this.rtcConfig=e.rtcConfig||{iceServers:u,bundlePolicy:"max-bundle",rtcpMuxPolicy:"require"},this._signaling=null,this._pc=null,this._listeners={},this._slots=new Map,this._maxSlots=f[this.mode]||1,this._candidateQueue=[],this._connected=!1}on(e,t){return this._listeners[e]||(this._listeners[e]=[]),this._listeners[e].push(t),this}_emit(e,...t){(this._listeners[e]||[]).forEach(c=>c(...t))}connect(){this._signaling=new _({url:this.url,autoReconnect:this.autoReconnect,onOpen:()=>this._onSignalingOpen(),onMessage:e=>this._onSignalingMessage(e),onClose:e=>this._onSignalingClose(e),onError:e=>this._emit("error",e)}),this._signaling.connect()}disconnect(){this._signaling&&(this._signaling.sendLeave(),this._signaling.disconnect()),this._closePeerConnection(),this._slots.clear(),this._connected=!1}chat(e){this._signaling&&this._signaling.sendChat(e,this.nickname)}requestKeyframe(){this._signaling&&this._signaling.sendPLI()}get slots(){return this._slots}_onSignalingOpen(){this._signaling.sendJoin(this.peerId,this.channelId,{nickname:this.nickname,token:this.token,mode:this.mode}),this._createPeerConnection().catch(e=>this._emit("error",e))}_onSignalingMessage(e){switch(e.type){case r.ANSWER:this._handleAnswer(e);break;case r.CANDIDATE:this._handleCandidate(e);break;case r.SLOT_INFO:this._handleSlotInfo(e);break;case r.CHAT:this._emit("chat",{peerId:e.peer_id,nickname:e.nickname,text:e.text});break;case r.ERROR:this._emit("error",new Error(e.message));break}}_onSignalingClose(e){this._closePeerConnection(),this._connected=!1,this._emit("close",e)}async _handleAnswer(e){if(this._pc)try{await this._pc.setRemoteDescription(new RTCSessionDescription({type:"answer",sdp:e.sdp}));for(let t of this._candidateQueue)await this._pc.addIceCandidate(t);this._candidateQueue=[]}catch(t){this._emit("error",t)}}async _handleCandidate(e){let t=new RTCIceCandidate({candidate:e.candidate,sdpMid:e.sdp_mid,sdpMLineIndex:e.sdp_m_line_index});if(this._pc&&this._pc.remoteDescription)try{await this._pc.addIceCandidate(t)}catch(c){console.warn("[FuzionX] ICE candidate error:",c)}else this._candidateQueue.push(t)}_handleSlotInfo(e){let t=parseInt(e.stream_id.replace("stream_",""),10);if(!e.nickname||e.nickname===""){this._slots.delete(t),this._emit("slot_remove",{slotIndex:t,senderId:e.sender_id});return}let c=this._slots.get(t)||{};this._slots.set(t,{...c,slotIndex:t,streamId:e.stream_id,nickname:e.nickname,senderId:e.sender_id}),this._emit("slot",this._slots.get(t))}async _createPeerConnection(){this._pc=new RTCPeerConnection(this.rtcConfig);let e=0;this._pc.ontrack=a=>{let n=a.track;if(n.kind==="video"){let i=e++,s=a.streams[0];s||(s=new MediaStream,s.addTrack(n));let o=this._slots.get(i)||{slotIndex:i};o.stream=s,this._slots.set(i,o),this._emit("stream",s,i),this._connected||(this._connected=!0,this._emit("connected"))}},this._pc.onconnectionstatechange=()=>{let a=this._pc?.connectionState;(a==="failed"||a==="disconnected")&&this._emit("error",new Error(`PeerConnection ${a}`))};for(let a=0;a<this._maxSlots;a++)this._pc.addTransceiver("video",{direction:"recvonly"}),this._pc.addTransceiver("audio",{direction:"recvonly"});let t=await this._pc.createOffer();t.sdp=h._forceCodecs(t.sdp),await this._pc.setLocalDescription(t),await this._waitForIceGathering();let c=this._pc.localDescription?.sdp;c&&this._signaling.sendOffer(c)}_waitForIceGathering(){return new Promise(e=>{if(this._pc.iceGatheringState==="complete")return e();let t=()=>{this._pc?.iceGatheringState==="complete"&&(this._pc.removeEventListener("icegatheringstatechange",t),e())};this._pc.addEventListener("icegatheringstatechange",t),setTimeout(()=>{this._pc&&this._pc.removeEventListener("icegatheringstatechange",t),e()},3e3)})}static _forceCodecs(e){let t=e.split(`\r
2
- `),c=t.findIndex(n=>n.startsWith("m=video"));if(c!==-1){let n=[],i=new Map;if(t.forEach(s=>{let o=s.match(/a=rtpmap:(\d+) H264\/90000/);o&&(n.push(o[1]),i.set(o[1],0))}),t.forEach(s=>{if(s.startsWith("a=fmtp:")){let o=s.split(" ")[0].split(":")[1];i.has(o)&&(s.includes("profile-level-id=42e01f")?i.set(o,100):s.includes("profile-level-id=42001f")&&i.set(o,80),s.includes("packetization-mode=1")&&i.set(o,(i.get(o)||0)+10))}}),n.length>0){n.sort((l,S)=>i.get(S)-i.get(l));let s=t[c].split(" "),o=s.slice(3).filter(l=>!n.includes(l));t[c]=[...s.slice(0,3),...n,...o].join(" ")}}let a=t.findIndex(n=>n.startsWith("m=audio"));if(a!==-1){let n=[];if(t.forEach(i=>{let s=i.match(/a=rtpmap:(\d+) opus\/48000/);s&&n.push(s[1])}),n.length>0){let i=t[a].split(" "),s=i.slice(3).filter(o=>!n.includes(o));t[a]=[...i.slice(0,3),...n,...s].join(" ")}}return t.join(`\r
3
- `)}_closePeerConnection(){this._pc&&(this._pc.close(),this._pc=null),this._candidateQueue=[]}};var g=class h{constructor(e){this.whipUrl=e.whipUrl||null,this.url=e.url||null,this.channelId=e.channelId||null,this.mode=e.mode||d.BROADCAST,this.nickname=e.nickname||null,this.token=e.token||null,this.peerId=e.peerId||`pub-${Math.random().toString(36).slice(2,10)}`,this.autoReconnect=e.autoReconnect!==!1,this.mediaConstraints=e.media||{video:!0,audio:!0},this._externalStream=e.stream||null,this.rtcConfig=e.rtcConfig||{iceServers:u,bundlePolicy:"max-bundle",rtcpMuxPolicy:"require"},this._signaling=null,this._pc=null,this._localStream=null,this._listeners={},this._candidateQueue=[],this._whipResourceUrl=null,this._maxSlots=f[this.mode]||1,this._slots=new Map,this._connected=!1}on(e,t){return this._listeners[e]||(this._listeners[e]=[]),this._listeners[e].push(t),this}_emit(e,...t){(this._listeners[e]||[]).forEach(c=>c(...t))}async connect(){try{if(await this._acquireMedia(),this.whipUrl)await this._connectWhip();else if(this.url)this._connectWebSocket();else throw new Error("url \uB610\uB294 whipUrl \uC911 \uD558\uB098\uB97C \uC9C0\uC815\uD574\uC57C \uD569\uB2C8\uB2E4.")}catch(e){this._emit("error",e)}}async disconnect(){if(this.whipUrl&&this._whipResourceUrl){try{let e=new URL(this.whipUrl).origin;await fetch(`${e}${this._whipResourceUrl}`,{method:"DELETE"})}catch(e){console.warn("[FuzionX] WHIP DELETE error:",e)}this._whipResourceUrl=null}this._signaling&&(this._signaling.sendLeave(),this._signaling.disconnect()),this._closePeerConnection(),this._stopMedia(),this._connected=!1}chat(e){this._signaling&&this._signaling.sendChat(e,this.nickname)}get localStream(){return this._localStream}get slots(){return this._slots}async _acquireMedia(){this._externalStream?this._localStream=this._externalStream:this._localStream=await navigator.mediaDevices.getUserMedia(this.mediaConstraints),this._emit("media",this._localStream)}_stopMedia(){this._localStream&&!this._externalStream&&this._localStream.getTracks().forEach(e=>e.stop()),this._localStream=null}async _connectWhip(){this._pc=new RTCPeerConnection(this.rtcConfig),this._localStream.getTracks().forEach(i=>{this._pc.addTrack(i,this._localStream)});let e=await this._pc.createOffer();e.sdp=h._forceCodecs(e.sdp),await this._pc.setLocalDescription(e),await new Promise(i=>{this._pc.iceGatheringState==="complete"?i():(this._pc.onicegatheringstatechange=()=>{this._pc.iceGatheringState==="complete"&&i()},setTimeout(i,3e3))});let t=this._pc.localDescription,c=this.whipUrl;this.token&&!c.includes("token=")&&(c+=(c.includes("?")?"&":"?")+`token=${this.token}`);let a=await fetch(c,{method:"POST",headers:{"Content-Type":"application/sdp"},body:t.sdp});if(a.status!==201)throw new Error(`WHIP failed: ${a.status} ${await a.text()}`);let n=await a.text();this._whipResourceUrl=a.headers.get("location"),await this._pc.setRemoteDescription(new RTCSessionDescription({type:"answer",sdp:n})),this._pc.onconnectionstatechange=()=>{let i=this._pc?.connectionState;i==="connected"?(this._connected=!0,this._emit("ready")):(i==="failed"||i==="disconnected")&&this._emit("error",new Error(`WHIP PeerConnection ${i}`))},this._emit("ready")}_connectWebSocket(){this._signaling=new _({url:this.url,autoReconnect:this.autoReconnect,onOpen:()=>this._onSignalingOpen(),onMessage:e=>this._onSignalingMessage(e),onClose:e=>{this._closePeerConnection(),this._connected=!1,this._emit("close",e)},onError:e=>this._emit("error",e)}),this._signaling.connect()}async _onSignalingOpen(){this._signaling.sendJoin(this.peerId,this.channelId,{nickname:this.nickname,token:this.token,mode:this.mode}),await this._createPeerConnection()}async _createPeerConnection(){if(this._pc=new RTCPeerConnection(this.rtcConfig),this._localStream.getTracks().forEach(c=>{this._pc.addTrack(c,this._localStream)}),this.mode===d.VIDEOCHAT){for(let n=0;n<this._maxSlots;n++)this._pc.addTransceiver("video",{direction:"recvonly"}),this._pc.addTransceiver("audio",{direction:"recvonly"});this._pc.getTransceivers().forEach(n=>{n.sender.track&&n.direction==="recvonly"&&(n.direction="sendrecv")});let a=0;this._pc.ontrack=n=>{let i=n.track;if(i.kind==="video"){let s=a++,o=n.streams[0];o||(o=new MediaStream,o.addTrack(i));let l=this._slots.get(s)||{slotIndex:s};l.stream=o,this._slots.set(s,l),this._emit("stream",o,s)}}}this._pc.onconnectionstatechange=()=>{let c=this._pc?.connectionState;c==="connected"&&!this._connected?(this._connected=!0,this._emit("ready")):(c==="failed"||c==="disconnected")&&this._emit("error",new Error(`PeerConnection ${c}`))};let e=await this._pc.createOffer();e.sdp=h._forceCodecs(e.sdp),await this._pc.setLocalDescription(e),await this._waitForIceGathering();let t=this._pc.localDescription?.sdp;t&&this._signaling.sendOffer(t)}_waitForIceGathering(){return new Promise(e=>{if(this._pc.iceGatheringState==="complete")return e();let t=()=>{this._pc?.iceGatheringState==="complete"&&(this._pc.removeEventListener("icegatheringstatechange",t),e())};this._pc.addEventListener("icegatheringstatechange",t),setTimeout(()=>{this._pc&&this._pc.removeEventListener("icegatheringstatechange",t),e()},3e3)})}static _forceCodecs(e){let t=e.split(`\r
4
- `),c=t.findIndex(n=>n.startsWith("m=video"));if(c!==-1){let n=[],i=new Map;if(t.forEach(s=>{let o=s.match(/a=rtpmap:(\d+) H264\/90000/);o&&(n.push(o[1]),i.set(o[1],0))}),t.forEach(s=>{if(s.startsWith("a=fmtp:")){let o=s.split(" ")[0].split(":")[1];i.has(o)&&(s.includes("profile-level-id=42e01f")?i.set(o,100):s.includes("profile-level-id=42001f")&&i.set(o,80),s.includes("packetization-mode=1")&&i.set(o,(i.get(o)||0)+10))}}),n.length>0){n.sort((l,S)=>i.get(S)-i.get(l));let s=t[c].split(" "),o=s.slice(3).filter(l=>!n.includes(l));t[c]=[...s.slice(0,3),...n,...o].join(" ")}}let a=t.findIndex(n=>n.startsWith("m=audio"));if(a!==-1){let n=[];if(t.forEach(i=>{let s=i.match(/a=rtpmap:(\d+) opus\/48000/);s&&n.push(s[1])}),n.length>0){let i=t[a].split(" "),s=i.slice(3).filter(o=>!n.includes(o));t[a]=[...i.slice(0,3),...n,...s].join(" ")}}return t.join(`\r
5
- `)}_onSignalingMessage(e){switch(e.type){case r.ANSWER:this._handleAnswer(e);break;case r.CANDIDATE:this._handleCandidate(e);break;case r.SLOT_INFO:this._handleSlotInfo(e);break;case r.CHAT:this._emit("chat",{peerId:e.peer_id,nickname:e.nickname,text:e.text});break;case r.ERROR:this._emit("error",new Error(e.message));break}}async _handleAnswer(e){if(this._pc)try{await this._pc.setRemoteDescription(new RTCSessionDescription({type:"answer",sdp:e.sdp}));for(let t of this._candidateQueue)await this._pc.addIceCandidate(t);this._candidateQueue=[]}catch(t){this._emit("error",t)}}async _handleCandidate(e){let t=new RTCIceCandidate({candidate:e.candidate,sdpMid:e.sdp_mid,sdpMLineIndex:e.sdp_m_line_index});if(this._pc&&this._pc.remoteDescription)try{await this._pc.addIceCandidate(t)}catch{}else this._candidateQueue.push(t)}_handleSlotInfo(e){let t=parseInt(e.stream_id.replace("stream_",""),10);if(!e.nickname||e.nickname===""){this._slots.delete(t),this._emit("slot_remove",{slotIndex:t,senderId:e.sender_id});return}let c=this._slots.get(t)||{};this._slots.set(t,{...c,slotIndex:t,streamId:e.stream_id,nickname:e.nickname,senderId:e.sender_id}),this._emit("slot",this._slots.get(t))}_closePeerConnection(){this._pc&&(this._pc.close(),this._pc=null),this._candidateQueue=[]}};return y(O);})();
1
+ var FuzionXPlayer=(()=>{var S=Object.defineProperty;var E=Object.getOwnPropertyDescriptor;var I=Object.getOwnPropertyNames;var k=Object.prototype.hasOwnProperty;var T=(h,e)=>{for(var t in e)S(h,t,{get:e[t],enumerable:!0})},R=(h,e,t,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let o of I(e))!k.call(h,o)&&o!==t&&S(h,o,{get:()=>e[o],enumerable:!(i=E(e,o))||i.enumerable});return h};var y=h=>R(S({},"__esModule",{value:!0}),h);var O={};T(O,{CODEC:()=>C,DEFAULT_ICE_SERVERS:()=>u,FuzionXPublisher:()=>g,FuzionXSignaling:()=>_,FuzionXViewer:()=>m,MAX_SLOTS:()=>f,RECONNECT:()=>p,SessionMode:()=>d,SignalType:()=>a});var u=[{urls:"stun:stun.l.google.com:19302"},{urls:"stun:stun1.l.google.com:19302"}],a={JOIN:"join",OFFER:"offer",ANSWER:"answer",CANDIDATE:"candidate",PLI:"pli",LEAVE:"leave",SLOT_INFO:"slot_info",CHAT:"chat",ERROR:"error"},d={BROADCAST:"broadcast",VIDEOCHAT:"videochat"},f={[d.BROADCAST]:1,[d.VIDEOCHAT]:9},p={MAX_RETRIES:5,BASE_DELAY_MS:1e3,MAX_DELAY_MS:3e4},C={VIDEO_MIME:"video/H264",AUDIO_MIME:"audio/opus",VIDEO_CLOCK:9e4,AUDIO_CLOCK:48e3};var _=class{constructor(e){this.url=e.url,this.onMessage=e.onMessage||(()=>{}),this.onOpen=e.onOpen||(()=>{}),this.onClose=e.onClose||(()=>{}),this.onError=e.onError||(()=>{}),this.autoReconnect=e.autoReconnect!==!1,this._ws=null,this._retryCount=0,this._reconnectTimer=null,this._intentionalClose=!1}connect(){this._intentionalClose=!1,this._doConnect()}_doConnect(){try{this._ws=new WebSocket(this.url)}catch(e){this.onError(e),this._scheduleReconnect();return}this._ws.onopen=()=>{this._retryCount=0,this.onOpen()},this._ws.onmessage=e=>{try{let t=JSON.parse(e.data);this.onMessage(t)}catch{console.warn("[FuzionX] Invalid JSON:",e.data)}},this._ws.onclose=e=>{this.onClose(e),!this._intentionalClose&&this.autoReconnect&&this._scheduleReconnect()},this._ws.onerror=e=>{this.onError(e)}}send(e){return this._ws&&this._ws.readyState===WebSocket.OPEN?(this._ws.send(JSON.stringify(e)),!0):!1}sendJoin(e,t,i={}){return this.send({type:a.JOIN,peer_id:e,channel_id:t,nickname:i.nickname||null,token:i.token||null,mode:i.mode||null})}sendOffer(e){return this.send({type:a.OFFER,sdp:e})}sendAnswer(e){return this.send({type:a.ANSWER,sdp:e})}sendCandidate(e){return this.send({type:a.CANDIDATE,candidate:e.candidate,sdp_mid:e.sdpMid,sdp_m_line_index:e.sdpMLineIndex})}sendChat(e,t){return this.send({type:a.CHAT,text:e,nickname:t||null,peer_id:null})}sendPLI(){return this.send({type:a.PLI})}sendLeave(){return this.send({type:a.LEAVE})}disconnect(){this._intentionalClose=!0,clearTimeout(this._reconnectTimer),this._ws&&(this._ws.close(),this._ws=null)}get connected(){return this._ws&&this._ws.readyState===WebSocket.OPEN}_scheduleReconnect(){if(this._retryCount>=p.MAX_RETRIES){console.error("[FuzionX] Max reconnect retries reached."),this.onError(new Error("Max reconnect retries"));return}let e=Math.min(p.BASE_DELAY_MS*Math.pow(2,this._retryCount),p.MAX_DELAY_MS);this._retryCount++,console.log(`[FuzionX] Reconnecting in ${e}ms (${this._retryCount}/${p.MAX_RETRIES})`),this._reconnectTimer=setTimeout(()=>this._doConnect(),e)}};var m=class h{constructor(e){this.url=e.url||null,this.hubUrl=e.hubUrl||null,this.channelId=e.channelId,this.mode=e.mode||d.BROADCAST,this.nickname=e.nickname||null,this.token=e.token||null,this.peerId=e.peerId||`viewer-${Math.random().toString(36).slice(2,10)}`,this.autoReconnect=e.autoReconnect!==!1,this.rtcConfig=e.rtcConfig||{iceServers:u,bundlePolicy:"max-bundle",rtcpMuxPolicy:"require"},this._signaling=null,this._pc=null,this._listeners={},this._slots=new Map,this._maxSlots=f[this.mode]||1,this._candidateQueue=[],this._connected=!1}on(e,t){return this._listeners[e]||(this._listeners[e]=[]),this._listeners[e].push(t),this}_emit(e,...t){(this._listeners[e]||[]).forEach(i=>i(...t))}async connect(){if(!this.url&&this.hubUrl)try{let e=await fetch(`${this.hubUrl}/api/channels/${this.channelId}`);if(!e.ok)throw new Error(`Channel not found: ${this.channelId}`);let t=await e.json();if(t.ws_url){let i=this.hubUrl.startsWith("https");this.url=t.ws_url.replace(/^ws(s?):/,i?"wss:":"ws:")}else{let o=this.hubUrl.startsWith("https")?"wss":"ws";this.url=`${o}://${t.media_ip}:${t.webrtc_port}`}}catch(e){this._emit("error",e);return}if(!this.url){this._emit("error",new Error("url \uB610\uB294 hubUrl\uC744 \uC9C0\uC815\uD574\uC57C \uD569\uB2C8\uB2E4."));return}this._signaling=new _({url:this.url,autoReconnect:this.autoReconnect,onOpen:()=>this._onSignalingOpen(),onMessage:e=>this._onSignalingMessage(e),onClose:e=>this._onSignalingClose(e),onError:e=>this._emit("error",e)}),this._signaling.connect()}disconnect(){this._signaling&&(this._signaling.sendLeave(),this._signaling.disconnect()),this._closePeerConnection(),this._slots.clear(),this._connected=!1}chat(e){this._signaling&&this._signaling.sendChat(e,this.nickname)}requestKeyframe(){this._signaling&&this._signaling.sendPLI()}get slots(){return this._slots}_onSignalingOpen(){this._signaling.sendJoin(this.peerId,this.channelId,{nickname:this.nickname,token:this.token,mode:this.mode}),this._createPeerConnection().catch(e=>this._emit("error",e))}_onSignalingMessage(e){switch(e.type){case a.ANSWER:this._handleAnswer(e);break;case a.CANDIDATE:this._handleCandidate(e);break;case a.SLOT_INFO:this._handleSlotInfo(e);break;case a.CHAT:this._emit("chat",{peerId:e.peer_id,nickname:e.nickname,text:e.text});break;case a.ERROR:this._emit("error",new Error(e.message));break}}_onSignalingClose(e){this._closePeerConnection(),this._connected=!1,this._emit("close",e)}async _handleAnswer(e){if(this._pc)try{await this._pc.setRemoteDescription(new RTCSessionDescription({type:"answer",sdp:e.sdp}));for(let t of this._candidateQueue)await this._pc.addIceCandidate(t);this._candidateQueue=[]}catch(t){this._emit("error",t)}}async _handleCandidate(e){let t=new RTCIceCandidate({candidate:e.candidate,sdpMid:e.sdp_mid,sdpMLineIndex:e.sdp_m_line_index});if(this._pc&&this._pc.remoteDescription)try{await this._pc.addIceCandidate(t)}catch(i){console.warn("[FuzionX] ICE candidate error:",i)}else this._candidateQueue.push(t)}_handleSlotInfo(e){let t=parseInt(e.stream_id.replace("stream_",""),10);if(!e.nickname||e.nickname===""){this._slots.delete(t),this._emit("slot_remove",{slotIndex:t,senderId:e.sender_id});return}let i=this._slots.get(t)||{};this._slots.set(t,{...i,slotIndex:t,streamId:e.stream_id,nickname:e.nickname,senderId:e.sender_id}),this._emit("slot",this._slots.get(t))}async _createPeerConnection(){this._pc=new RTCPeerConnection(this.rtcConfig);let e=0;this._pc.ontrack=o=>{let n=o.track;if(n.kind==="video"){let s=e++,r=o.streams[0];r||(r=new MediaStream,r.addTrack(n));let c=this._slots.get(s)||{slotIndex:s};c.stream=r,this._slots.set(s,c),this._emit("stream",r,s),this._connected||(this._connected=!0,this._emit("connected"),this._signaling&&this._signaling.sendPLI())}},this._pc.onconnectionstatechange=()=>{let o=this._pc?.connectionState;(o==="failed"||o==="disconnected")&&this._emit("error",new Error(`PeerConnection ${o}`))};for(let o=0;o<this._maxSlots;o++)this._pc.addTransceiver("video",{direction:"recvonly"}),this._pc.addTransceiver("audio",{direction:"recvonly"});let t=await this._pc.createOffer();t.sdp=h._forceCodecs(t.sdp),await this._pc.setLocalDescription(t),await this._waitForIceGathering();let i=this._pc.localDescription?.sdp;i&&this._signaling.sendOffer(i)}_waitForIceGathering(){return new Promise(e=>{if(this._pc.iceGatheringState==="complete")return e();let t=()=>{this._pc?.iceGatheringState==="complete"&&(this._pc.removeEventListener("icegatheringstatechange",t),e())};this._pc.addEventListener("icegatheringstatechange",t),setTimeout(()=>{this._pc&&this._pc.removeEventListener("icegatheringstatechange",t),e()},150)})}static _forceCodecs(e){let t=e.split(`\r
2
+ `),i=t.findIndex(n=>n.startsWith("m=video"));if(i!==-1){let n=[],s=new Map;if(t.forEach(r=>{let c=r.match(/a=rtpmap:(\d+) H264\/90000/);c&&(n.push(c[1]),s.set(c[1],0))}),t.forEach(r=>{if(r.startsWith("a=fmtp:")){let c=r.split(" ")[0].split(":")[1];s.has(c)&&(r.includes("profile-level-id=42e01f")?s.set(c,100):r.includes("profile-level-id=42001f")&&s.set(c,80),r.includes("packetization-mode=1")&&s.set(c,(s.get(c)||0)+10))}}),n.length>0){n.sort((l,w)=>s.get(w)-s.get(l));let r=t[i].split(" "),c=r.slice(3).filter(l=>!n.includes(l));t[i]=[...r.slice(0,3),...n,...c].join(" ")}}let o=t.findIndex(n=>n.startsWith("m=audio"));if(o!==-1){let n=[];if(t.forEach(s=>{let r=s.match(/a=rtpmap:(\d+) opus\/48000/);r&&n.push(r[1])}),n.length>0){let s=t[o].split(" "),r=s.slice(3).filter(c=>!n.includes(c));t[o]=[...s.slice(0,3),...n,...r].join(" ")}}return t.join(`\r
3
+ `)}_closePeerConnection(){this._pc&&(this._pc.close(),this._pc=null),this._candidateQueue=[]}};var g=class h{constructor(e){this.whipUrl=e.whipUrl||null,this.url=e.url||null,this.hubUrl=e.hubUrl||null,this.channelId=e.channelId||null,this.mode=e.mode||d.BROADCAST,this.nickname=e.nickname||null,this.token=e.token||null,this.peerId=e.peerId||`pub-${Math.random().toString(36).slice(2,10)}`,this.autoReconnect=e.autoReconnect!==!1,this.mediaConstraints=e.media||{video:!0,audio:!0},this._externalStream=e.stream||null,this.rtcConfig=e.rtcConfig||{iceServers:u,bundlePolicy:"max-bundle",rtcpMuxPolicy:"require"},this._signaling=null,this._pc=null,this._localStream=null,this._listeners={},this._candidateQueue=[],this._whipResourceUrl=null,this._maxSlots=f[this.mode]||1,this._slots=new Map,this._connected=!1}on(e,t){return this._listeners[e]||(this._listeners[e]=[]),this._listeners[e].push(t),this}_emit(e,...t){(this._listeners[e]||[]).forEach(i=>i(...t))}async connect(){try{if(!this.url&&!this.whipUrl&&this.hubUrl&&this.channelId){let e=await fetch(`${this.hubUrl}/api/channels`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({channel_id:this.channelId,source_type:"webrtc"})});if(e.status===409)throw new Error(`Channel already exists: ${this.channelId}`);if(!e.ok)throw new Error(`Failed to create channel: ${this.channelId}`);let t=await e.json();if(t.ws_url){let i=this.hubUrl.startsWith("https");this.url=t.ws_url.replace(/^ws(s?):/,i?"wss:":"ws:")}else{let o=this.hubUrl.startsWith("https")?"wss":"ws";this.url=`${o}://${t.media_ip}:${t.webrtc_port}`}}if(await this._acquireMedia(),this.whipUrl)await this._connectWhip();else if(this.url)this._connectWebSocket();else throw new Error("url, hubUrl, \uB610\uB294 whipUrl \uC911 \uD558\uB098\uB97C \uC9C0\uC815\uD574\uC57C \uD569\uB2C8\uB2E4.")}catch(e){this._emit("error",e)}}async disconnect(){if(this.whipUrl&&this._whipResourceUrl){try{let e=new URL(this.whipUrl).origin;await fetch(`${e}${this._whipResourceUrl}`,{method:"DELETE"})}catch(e){console.warn("[FuzionX] WHIP DELETE error:",e)}this._whipResourceUrl=null}this._signaling&&(this._signaling.sendLeave(),this._signaling.disconnect()),this._closePeerConnection(),this._stopMedia(),this._connected=!1}chat(e){this._signaling&&this._signaling.sendChat(e,this.nickname)}get localStream(){return this._localStream}get slots(){return this._slots}async _acquireMedia(){this._externalStream?this._localStream=this._externalStream:this._localStream=await navigator.mediaDevices.getUserMedia(this.mediaConstraints),this._emit("media",this._localStream)}_stopMedia(){this._localStream&&!this._externalStream&&this._localStream.getTracks().forEach(e=>e.stop()),this._localStream=null}async _connectWhip(){this._pc=new RTCPeerConnection(this.rtcConfig),this._localStream.getTracks().forEach(s=>{this._pc.addTrack(s,this._localStream)});let e=await this._pc.createOffer();e.sdp=h._forceCodecs(e.sdp),await this._pc.setLocalDescription(e),await new Promise(s=>{this._pc.iceGatheringState==="complete"?s():(this._pc.onicegatheringstatechange=()=>{this._pc.iceGatheringState==="complete"&&s()},setTimeout(s,150))});let t=this._pc.localDescription,i=this.whipUrl;this.token&&!i.includes("token=")&&(i+=(i.includes("?")?"&":"?")+`token=${this.token}`);let o=await fetch(i,{method:"POST",headers:{"Content-Type":"application/sdp"},body:t.sdp});if(o.status!==201)throw new Error(`WHIP failed: ${o.status} ${await o.text()}`);let n=await o.text();this._whipResourceUrl=o.headers.get("location"),await this._pc.setRemoteDescription(new RTCSessionDescription({type:"answer",sdp:n})),this._pc.onconnectionstatechange=()=>{let s=this._pc?.connectionState;s==="connected"?(this._connected=!0,this._emit("ready")):(s==="failed"||s==="disconnected")&&this._emit("error",new Error(`WHIP PeerConnection ${s}`))},this._emit("ready")}_connectWebSocket(){this._signaling=new _({url:this.url,autoReconnect:this.autoReconnect,onOpen:()=>this._onSignalingOpen(),onMessage:e=>this._onSignalingMessage(e),onClose:e=>{this._closePeerConnection(),this._connected=!1,this._emit("close",e)},onError:e=>this._emit("error",e)}),this._signaling.connect()}async _onSignalingOpen(){this._signaling.sendJoin(this.peerId,this.channelId,{nickname:this.nickname,token:this.token,mode:this.mode}),await this._createPeerConnection()}async _createPeerConnection(){if(this._pc=new RTCPeerConnection(this.rtcConfig),this._localStream.getTracks().forEach(i=>{this._pc.addTrack(i,this._localStream)}),this.mode===d.VIDEOCHAT){for(let n=0;n<this._maxSlots;n++)this._pc.addTransceiver("video",{direction:"recvonly"}),this._pc.addTransceiver("audio",{direction:"recvonly"});this._pc.getTransceivers().forEach(n=>{n.sender.track&&n.direction==="recvonly"&&(n.direction="sendrecv")});let o=0;this._pc.ontrack=n=>{let s=n.track;if(s.kind==="video"){let r=o++,c=n.streams[0];c||(c=new MediaStream,c.addTrack(s));let l=this._slots.get(r)||{slotIndex:r};l.stream=c,this._slots.set(r,l),this._emit("stream",c,r)}}}this._pc.onconnectionstatechange=()=>{let i=this._pc?.connectionState;i==="connected"&&!this._connected?(this._connected=!0,this._emit("ready")):(i==="failed"||i==="disconnected")&&this._emit("error",new Error(`PeerConnection ${i}`))};let e=await this._pc.createOffer();e.sdp=h._forceCodecs(e.sdp),await this._pc.setLocalDescription(e),await this._waitForIceGathering();let t=this._pc.localDescription?.sdp;t&&this._signaling.sendOffer(t)}_waitForIceGathering(){return new Promise(e=>{if(this._pc.iceGatheringState==="complete")return e();let t=()=>{this._pc?.iceGatheringState==="complete"&&(this._pc.removeEventListener("icegatheringstatechange",t),e())};this._pc.addEventListener("icegatheringstatechange",t),setTimeout(()=>{this._pc&&this._pc.removeEventListener("icegatheringstatechange",t),e()},150)})}static _forceCodecs(e){let t=e.split(`\r
4
+ `),i=t.findIndex(n=>n.startsWith("m=video"));if(i!==-1){let n=[],s=new Map;if(t.forEach(r=>{let c=r.match(/a=rtpmap:(\d+) H264\/90000/);c&&(n.push(c[1]),s.set(c[1],0))}),t.forEach(r=>{if(r.startsWith("a=fmtp:")){let c=r.split(" ")[0].split(":")[1];s.has(c)&&(r.includes("profile-level-id=42e01f")?s.set(c,100):r.includes("profile-level-id=42001f")&&s.set(c,80),r.includes("packetization-mode=1")&&s.set(c,(s.get(c)||0)+10))}}),n.length>0){n.sort((l,w)=>s.get(w)-s.get(l));let r=t[i].split(" "),c=r.slice(3).filter(l=>!n.includes(l));t[i]=[...r.slice(0,3),...n,...c].join(" ")}}let o=t.findIndex(n=>n.startsWith("m=audio"));if(o!==-1){let n=[];if(t.forEach(s=>{let r=s.match(/a=rtpmap:(\d+) opus\/48000/);r&&n.push(r[1])}),n.length>0){let s=t[o].split(" "),r=s.slice(3).filter(c=>!n.includes(c));t[o]=[...s.slice(0,3),...n,...r].join(" ")}}return t.join(`\r
5
+ `)}_onSignalingMessage(e){switch(e.type){case a.ANSWER:this._handleAnswer(e);break;case a.CANDIDATE:this._handleCandidate(e);break;case a.SLOT_INFO:this._handleSlotInfo(e);break;case a.CHAT:this._emit("chat",{peerId:e.peer_id,nickname:e.nickname,text:e.text});break;case a.ERROR:this._emit("error",new Error(e.message));break}}async _handleAnswer(e){if(this._pc)try{await this._pc.setRemoteDescription(new RTCSessionDescription({type:"answer",sdp:e.sdp}));for(let t of this._candidateQueue)await this._pc.addIceCandidate(t);this._candidateQueue=[]}catch(t){this._emit("error",t)}}async _handleCandidate(e){let t=new RTCIceCandidate({candidate:e.candidate,sdpMid:e.sdp_mid,sdpMLineIndex:e.sdp_m_line_index});if(this._pc&&this._pc.remoteDescription)try{await this._pc.addIceCandidate(t)}catch{}else this._candidateQueue.push(t)}_handleSlotInfo(e){let t=parseInt(e.stream_id.replace("stream_",""),10);if(!e.nickname||e.nickname===""){this._slots.delete(t),this._emit("slot_remove",{slotIndex:t,senderId:e.sender_id});return}let i=this._slots.get(t)||{};this._slots.set(t,{...i,slotIndex:t,streamId:e.stream_id,nickname:e.nickname,senderId:e.sender_id}),this._emit("slot",this._slots.get(t))}_closePeerConnection(){this._pc&&(this._pc.close(),this._pc=null),this._candidateQueue=[]}};return y(O);})();
6
6
  if(typeof module!=="undefined")module.exports=FuzionXPlayer;
7
7
  //# sourceMappingURL=fuzionx-player.umd.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/index.js", "../src/constants.js", "../src/FuzionXSignaling.js", "../src/FuzionXViewer.js", "../src/FuzionXPublisher.js"],
4
- "sourcesContent": ["/**\n * @fuzionx/player \u2014 Entry Point\n *\n * FuzionX WebRTC Player SDK\n */\n\nexport { FuzionXViewer } from './FuzionXViewer.js';\nexport { FuzionXPublisher } from './FuzionXPublisher.js';\nexport { FuzionXSignaling } from './FuzionXSignaling.js';\nexport { SignalType, SessionMode, MAX_SLOTS, DEFAULT_ICE_SERVERS, CODEC, RECONNECT } from './constants.js';\n", "/**\n * @fuzionx/player \u2014 Constants & Default Configuration\n */\n\n/** Default ICE Servers */\nexport const DEFAULT_ICE_SERVERS = [\n { urls: 'stun:stun.l.google.com:19302' },\n { urls: 'stun:stun1.l.google.com:19302' },\n];\n\n/** Signaling message types (maps to server SignalMessage enum) */\nexport const SignalType = {\n JOIN: 'join',\n OFFER: 'offer',\n ANSWER: 'answer',\n CANDIDATE: 'candidate',\n PLI: 'pli',\n LEAVE: 'leave',\n SLOT_INFO: 'slot_info',\n CHAT: 'chat',\n ERROR: 'error',\n};\n\n/** Session modes */\nexport const SessionMode = {\n BROADCAST: 'broadcast',\n VIDEOCHAT: 'videochat',\n};\n\n/** Max slots per mode */\nexport const MAX_SLOTS = {\n [SessionMode.BROADCAST]: 1,\n [SessionMode.VIDEOCHAT]: 9,\n};\n\n/** Default reconnect settings */\nexport const RECONNECT = {\n MAX_RETRIES: 5,\n BASE_DELAY_MS: 1000,\n MAX_DELAY_MS: 30000,\n};\n\n/** Codec preferences */\nexport const CODEC = {\n VIDEO_MIME: 'video/H264',\n AUDIO_MIME: 'audio/opus',\n VIDEO_CLOCK: 90000,\n AUDIO_CLOCK: 48000,\n};\n", "/**\n * @fuzionx/player \u2014 WebSocket Signaling Layer\n *\n * FuzionX Media Server\uC758 JSON \uC2DC\uADF8\uB110\uB9C1 \uD504\uB85C\uD1A0\uCF5C \uCEA1\uC290\uD654.\n * \uC790\uB3D9 \uC7AC\uC5F0\uACB0 + \uC774\uBCA4\uD2B8 \uC2DC\uC2A4\uD15C.\n */\n\nimport { SignalType, RECONNECT } from './constants.js';\n\nexport class FuzionXSignaling {\n /**\n * @param {Object} opts\n * @param {string} opts.url - WebSocket URL (ws:// or wss://)\n * @param {Function} [opts.onMessage] - \uBA54\uC2DC\uC9C0 \uC218\uC2E0 \uCF5C\uBC31\n * @param {Function} [opts.onOpen] - \uC5F0\uACB0 \uC131\uACF5 \uCF5C\uBC31\n * @param {Function} [opts.onClose] - \uC5F0\uACB0 \uC885\uB8CC \uCF5C\uBC31\n * @param {Function} [opts.onError] - \uC5D0\uB7EC \uCF5C\uBC31\n * @param {boolean} [opts.autoReconnect=true]\n */\n constructor(opts) {\n this.url = opts.url;\n this.onMessage = opts.onMessage || (() => {});\n this.onOpen = opts.onOpen || (() => {});\n this.onClose = opts.onClose || (() => {});\n this.onError = opts.onError || (() => {});\n this.autoReconnect = opts.autoReconnect !== false;\n\n /** @private */\n this._ws = null;\n this._retryCount = 0;\n this._reconnectTimer = null;\n this._intentionalClose = false;\n }\n\n /** WebSocket \uC5F0\uACB0. */\n connect() {\n this._intentionalClose = false;\n this._doConnect();\n }\n\n /** @private */\n _doConnect() {\n try {\n this._ws = new WebSocket(this.url);\n } catch (e) {\n this.onError(e);\n this._scheduleReconnect();\n return;\n }\n\n this._ws.onopen = () => {\n this._retryCount = 0;\n this.onOpen();\n };\n\n this._ws.onmessage = (event) => {\n try {\n const msg = JSON.parse(event.data);\n this.onMessage(msg);\n } catch (e) {\n console.warn('[FuzionX] Invalid JSON:', event.data);\n }\n };\n\n this._ws.onclose = (event) => {\n this.onClose(event);\n if (!this._intentionalClose && this.autoReconnect) {\n this._scheduleReconnect();\n }\n };\n\n this._ws.onerror = (event) => {\n this.onError(event);\n };\n }\n\n /** JSON \uBA54\uC2DC\uC9C0 \uC804\uC1A1. */\n send(msg) {\n if (this._ws && this._ws.readyState === WebSocket.OPEN) {\n this._ws.send(JSON.stringify(msg));\n return true;\n }\n return false;\n }\n\n // \u2500\u2500 \uC2DC\uADF8\uB110\uB9C1 \uD5EC\uD37C \u2500\u2500\n\n /** Join \uC804\uC1A1 */\n sendJoin(peerId, channelId, opts = {}) {\n return this.send({\n type: SignalType.JOIN,\n peer_id: peerId,\n channel_id: channelId,\n nickname: opts.nickname || null,\n token: opts.token || null,\n mode: opts.mode || null,\n });\n }\n\n /** SDP Offer \uC804\uC1A1 */\n sendOffer(sdp) {\n return this.send({ type: SignalType.OFFER, sdp });\n }\n\n /** SDP Answer \uC804\uC1A1 */\n sendAnswer(sdp) {\n return this.send({ type: SignalType.ANSWER, sdp });\n }\n\n /** ICE Candidate \uC804\uC1A1 */\n sendCandidate(candidate) {\n return this.send({\n type: SignalType.CANDIDATE,\n candidate: candidate.candidate,\n sdp_mid: candidate.sdpMid,\n sdp_m_line_index: candidate.sdpMLineIndex,\n });\n }\n\n /** Chat \uC804\uC1A1 */\n sendChat(text, nickname) {\n return this.send({\n type: SignalType.CHAT,\n text,\n nickname: nickname || null,\n peer_id: null,\n });\n }\n\n /** PLI \uC694\uCCAD */\n sendPLI() {\n return this.send({ type: SignalType.PLI });\n }\n\n /** Leave \uC804\uC1A1 */\n sendLeave() {\n return this.send({ type: SignalType.LEAVE });\n }\n\n /** \uC5F0\uACB0 \uC885\uB8CC (\uC7AC\uC5F0\uACB0 \uC548 \uD568). */\n disconnect() {\n this._intentionalClose = true;\n clearTimeout(this._reconnectTimer);\n if (this._ws) {\n this._ws.close();\n this._ws = null;\n }\n }\n\n /** @returns {boolean} \uC5F0\uACB0 \uC0C1\uD0DC */\n get connected() {\n return this._ws && this._ws.readyState === WebSocket.OPEN;\n }\n\n /** @private \uC7AC\uC5F0\uACB0 \uC2A4\uCF00\uC904 (Exponential Backoff). */\n _scheduleReconnect() {\n if (this._retryCount >= RECONNECT.MAX_RETRIES) {\n console.error('[FuzionX] Max reconnect retries reached.');\n this.onError(new Error('Max reconnect retries'));\n return;\n }\n const delay = Math.min(\n RECONNECT.BASE_DELAY_MS * Math.pow(2, this._retryCount),\n RECONNECT.MAX_DELAY_MS\n );\n this._retryCount++;\n console.log(`[FuzionX] Reconnecting in ${delay}ms (${this._retryCount}/${RECONNECT.MAX_RETRIES})`);\n this._reconnectTimer = setTimeout(() => this._doConnect(), delay);\n }\n}\n", "/**\n * @fuzionx/player \u2014 FuzionXViewer (\uC218\uC2E0\uC790 \uBAA8\uB4DC)\n *\n * \uC11C\uBC84\uC5D0\uC11C MediaStream\uC744 \uC218\uC2E0\uD558\uC5EC <video> \uC5D8\uB9AC\uBA3C\uD2B8\uC5D0 \uB80C\uB354\uB9C1.\n * broadcast(1 stream) / videochat(\uCD5C\uB300 9 streams) \uC9C0\uC6D0.\n *\n * @example\n * const viewer = new FuzionXViewer({\n * url: 'wss://media:50002',\n * channelId: 'my-live',\n * mode: 'broadcast',\n * });\n * viewer.on('stream', (stream, slotIndex) => {\n * videoEl.srcObject = stream;\n * });\n * viewer.connect();\n */\n\nimport { FuzionXSignaling } from './FuzionXSignaling.js';\nimport { DEFAULT_ICE_SERVERS, SignalType, SessionMode, MAX_SLOTS } from './constants.js';\n\nexport class FuzionXViewer {\n /**\n * @param {Object} opts\n * @param {string} opts.url - WebSocket URL\n * @param {string} opts.channelId - \uCC44\uB110 ID\n * @param {string} [opts.mode='broadcast'] - 'broadcast' | 'videochat'\n * @param {string} [opts.nickname] - \uB2C9\uB124\uC784\n * @param {string} [opts.token] - \uC778\uC99D \uD1A0\uD070\n * @param {string} [opts.peerId] - \uD53C\uC5B4 ID (\uC790\uB3D9 \uC0DD\uC131)\n * @param {RTCConfiguration} [opts.rtcConfig] - WebRTC \uC124\uC815 \uC624\uBC84\uB77C\uC774\uB4DC\n * @param {boolean} [opts.autoReconnect=true]\n */\n constructor(opts) {\n this.url = opts.url;\n this.channelId = opts.channelId;\n this.mode = opts.mode || SessionMode.BROADCAST;\n this.nickname = opts.nickname || null;\n this.token = opts.token || null;\n this.peerId = opts.peerId || `viewer-${Math.random().toString(36).slice(2, 10)}`;\n this.autoReconnect = opts.autoReconnect !== false;\n\n this.rtcConfig = opts.rtcConfig || {\n iceServers: DEFAULT_ICE_SERVERS,\n bundlePolicy: 'max-bundle',\n rtcpMuxPolicy: 'require',\n };\n\n /** @private */\n this._signaling = null;\n this._pc = null;\n this._listeners = {};\n this._slots = new Map(); // slotIndex \u2192 { streamId, nickname, senderId, stream }\n this._maxSlots = MAX_SLOTS[this.mode] || 1;\n this._candidateQueue = [];\n this._connected = false;\n }\n\n // \u2500\u2500 Event System \u2500\u2500\n\n /**\n * \uC774\uBCA4\uD2B8 \uB9AC\uC2A4\uB108 \uB4F1\uB85D.\n * @param {'stream'|'slot'|'slot_remove'|'chat'|'error'|'close'|'connected'} event\n * @param {Function} handler\n */\n on(event, handler) {\n if (!this._listeners[event]) this._listeners[event] = [];\n this._listeners[event].push(handler);\n return this;\n }\n\n /** @private */\n _emit(event, ...args) {\n (this._listeners[event] || []).forEach((fn) => fn(...args));\n }\n\n // \u2500\u2500 Lifecycle \u2500\u2500\n\n /** \uC11C\uBC84 \uC5F0\uACB0 + WebRTC \uC138\uC158 \uC2DC\uC791. */\n connect() {\n this._signaling = new FuzionXSignaling({\n url: this.url,\n autoReconnect: this.autoReconnect,\n onOpen: () => this._onSignalingOpen(),\n onMessage: (msg) => this._onSignalingMessage(msg),\n onClose: (evt) => this._onSignalingClose(evt),\n onError: (err) => this._emit('error', err),\n });\n this._signaling.connect();\n }\n\n /** \uC5F0\uACB0 \uC885\uB8CC. */\n disconnect() {\n if (this._signaling) {\n this._signaling.sendLeave();\n this._signaling.disconnect();\n }\n this._closePeerConnection();\n this._slots.clear();\n this._connected = false;\n }\n\n /** \uCC44\uD305 \uC804\uC1A1. */\n chat(text) {\n if (this._signaling) {\n this._signaling.sendChat(text, this.nickname);\n }\n }\n\n /** \uD0A4\uD504\uB808\uC784 \uC694\uCCAD. */\n requestKeyframe() {\n if (this._signaling) {\n this._signaling.sendPLI();\n }\n }\n\n /** @returns {Map} \uD604\uC7AC \uC2AC\uB86F \uC815\uBCF4 */\n get slots() {\n return this._slots;\n }\n\n // \u2500\u2500 Internal \u2500\u2500\n\n /** @private */\n _onSignalingOpen() {\n this._signaling.sendJoin(this.peerId, this.channelId, {\n nickname: this.nickname,\n token: this.token,\n mode: this.mode,\n });\n // Join \uC804\uC1A1 \uD6C4 \uBC14\uB85C PeerConnection \uC0DD\uC131\n this._createPeerConnection().catch((e) => this._emit('error', e));\n }\n\n /** @private */\n _onSignalingMessage(msg) {\n switch (msg.type) {\n case SignalType.ANSWER:\n this._handleAnswer(msg);\n break;\n case SignalType.CANDIDATE:\n this._handleCandidate(msg);\n break;\n case SignalType.SLOT_INFO:\n this._handleSlotInfo(msg);\n break;\n case SignalType.CHAT:\n this._emit('chat', {\n peerId: msg.peer_id,\n nickname: msg.nickname,\n text: msg.text,\n });\n break;\n case SignalType.ERROR:\n this._emit('error', new Error(msg.message));\n break;\n }\n }\n\n /** @private */\n _onSignalingClose(evt) {\n this._closePeerConnection();\n this._connected = false;\n this._emit('close', evt);\n }\n\n /** @private */\n async _handleAnswer(msg) {\n if (!this._pc) return;\n try {\n await this._pc.setRemoteDescription(\n new RTCSessionDescription({ type: 'answer', sdp: msg.sdp })\n );\n // Flush queued ICE candidates\n for (const c of this._candidateQueue) {\n await this._pc.addIceCandidate(c);\n }\n this._candidateQueue = [];\n } catch (e) {\n this._emit('error', e);\n }\n }\n\n /** @private */\n async _handleCandidate(msg) {\n const candidate = new RTCIceCandidate({\n candidate: msg.candidate,\n sdpMid: msg.sdp_mid,\n sdpMLineIndex: msg.sdp_m_line_index,\n });\n if (this._pc && this._pc.remoteDescription) {\n try {\n await this._pc.addIceCandidate(candidate);\n } catch (e) {\n console.warn('[FuzionX] ICE candidate error:', e);\n }\n } else {\n this._candidateQueue.push(candidate);\n }\n }\n\n /** @private */\n _handleSlotInfo(msg) {\n const slotIndex = parseInt(msg.stream_id.replace('stream_', ''), 10);\n\n // nickname\uC774 \uBE48 \uBB38\uC790\uC5F4\uC774\uBA74 \uC2AC\uB86F \uD574\uC81C\n if (!msg.nickname || msg.nickname === '') {\n this._slots.delete(slotIndex);\n this._emit('slot_remove', { slotIndex, senderId: msg.sender_id });\n return;\n }\n\n const existing = this._slots.get(slotIndex) || {};\n this._slots.set(slotIndex, {\n ...existing,\n slotIndex,\n streamId: msg.stream_id,\n nickname: msg.nickname,\n senderId: msg.sender_id,\n });\n this._emit('slot', this._slots.get(slotIndex));\n }\n\n /**\n * PeerConnection \uC0DD\uC131 + Non-Trickle ICE Offer.\n * old alloy-player \uAC80\uC99D \uD328\uD134:\n * 1. addTransceiver(recvonly) \u00D7 maxSlots \u2014 \uC11C\uBC84 m-line \uB9E4\uD551\n * 2. createOffer \u2192 H264 SDP \uAC15\uC81C\n * 3. ICE gathering \uC644\uB8CC \uB300\uAE30 \uD6C4 SDP \uC804\uCCB4 \uC804\uC1A1 (Non-Trickle)\n * @private\n */\n async _createPeerConnection() {\n this._pc = new RTCPeerConnection(this.rtcConfig);\n\n // Non-Trickle: ICE candidate\uB294 SDP\uC5D0 \uD3EC\uD568\uB418\uC5B4 \uC804\uC1A1\uB428\n // (onicecandidate\uB294 gathering \uCD94\uC801\uC6A9\uC73C\uB85C\uB9CC \uC720\uC9C0)\n\n // \uC218\uC2E0 \uD2B8\uB799 \uB9E4\uD551\n let videoTrackCount = 0;\n this._pc.ontrack = (event) => {\n const track = event.track;\n\n if (track.kind === 'video') {\n const slotIndex = videoTrackCount++;\n\n let stream = event.streams[0];\n if (!stream) {\n stream = new MediaStream();\n stream.addTrack(track);\n }\n\n const slot = this._slots.get(slotIndex) || { slotIndex };\n slot.stream = stream;\n this._slots.set(slotIndex, slot);\n\n this._emit('stream', stream, slotIndex);\n\n if (!this._connected) {\n this._connected = true;\n this._emit('connected');\n }\n }\n };\n\n this._pc.onconnectionstatechange = () => {\n const state = this._pc?.connectionState;\n if (state === 'failed' || state === 'disconnected') {\n this._emit('error', new Error(`PeerConnection ${state}`));\n }\n };\n\n // recvonly Transceiver \uCD94\uAC00 (\uC11C\uBC84 \uC2AC\uB86F \uC218\uC5D0 \uB9DE\uCDA4)\n for (let i = 0; i < this._maxSlots; i++) {\n this._pc.addTransceiver('video', { direction: 'recvonly' });\n this._pc.addTransceiver('audio', { direction: 'recvonly' });\n }\n\n // Offer \uC0DD\uC131 + H264/Opus \uCF54\uB371 \uAC15\uC81C\n const offer = await this._pc.createOffer();\n offer.sdp = FuzionXViewer._forceCodecs(offer.sdp);\n await this._pc.setLocalDescription(offer);\n\n // Non-Trickle ICE: gathering \uC644\uB8CC \uB300\uAE30 \uD6C4 \uC804\uCCB4 SDP \uC804\uC1A1\n await this._waitForIceGathering();\n\n // gathering \uC644\uB8CC \uD6C4 \uCD5C\uC885 SDP (ICE candidates \uD3EC\uD568) \uC804\uC1A1\n const finalSdp = this._pc.localDescription?.sdp;\n if (finalSdp) {\n this._signaling.sendOffer(finalSdp);\n }\n }\n\n /**\n * ICE gathering \uC644\uB8CC \uB300\uAE30 (Non-Trickle)\n * @private\n */\n _waitForIceGathering() {\n return new Promise((resolve) => {\n if (this._pc.iceGatheringState === 'complete') {\n return resolve();\n }\n const check = () => {\n if (this._pc?.iceGatheringState === 'complete') {\n this._pc.removeEventListener('icegatheringstatechange', check);\n resolve();\n }\n };\n this._pc.addEventListener('icegatheringstatechange', check);\n // Timeout 3s (Safari \uD638\uD658)\n setTimeout(() => {\n if (this._pc) {\n this._pc.removeEventListener('icegatheringstatechange', check);\n }\n resolve();\n }, 3000);\n });\n }\n\n /**\n * SDP\uC5D0\uC11C H264 + Opus \uCF54\uB371 \uC6B0\uC120 \uAC15\uC81C.\n * @private\n */\n static _forceCodecs(sdp) {\n let lines = sdp.split('\\r\\n');\n\n // Video: H264 \uC6B0\uC120\n const videoIdx = lines.findIndex((l) => l.startsWith('m=video'));\n if (videoIdx !== -1) {\n const h264Pts = [];\n const h264Scores = new Map();\n lines.forEach((l) => {\n const m = l.match(/a=rtpmap:(\\d+) H264\\/90000/);\n if (m) {\n h264Pts.push(m[1]);\n h264Scores.set(m[1], 0);\n }\n });\n lines.forEach((l) => {\n if (l.startsWith('a=fmtp:')) {\n const pt = l.split(' ')[0].split(':')[1];\n if (h264Scores.has(pt)) {\n if (l.includes('profile-level-id=42e01f')) h264Scores.set(pt, 100);\n else if (l.includes('profile-level-id=42001f')) h264Scores.set(pt, 80);\n if (l.includes('packetization-mode=1')) h264Scores.set(pt, (h264Scores.get(pt) || 0) + 10);\n }\n }\n });\n if (h264Pts.length > 0) {\n h264Pts.sort((a, b) => h264Scores.get(b) - h264Scores.get(a));\n const parts = lines[videoIdx].split(' ');\n const otherPts = parts.slice(3).filter((pt) => !h264Pts.includes(pt));\n lines[videoIdx] = [...parts.slice(0, 3), ...h264Pts, ...otherPts].join(' ');\n }\n }\n\n // Audio: Opus \uC6B0\uC120\n const audioIdx = lines.findIndex((l) => l.startsWith('m=audio'));\n if (audioIdx !== -1) {\n const opusPts = [];\n lines.forEach((l) => {\n const m = l.match(/a=rtpmap:(\\d+) opus\\/48000/);\n if (m) opusPts.push(m[1]);\n });\n if (opusPts.length > 0) {\n const parts = lines[audioIdx].split(' ');\n const otherPts = parts.slice(3).filter((pt) => !opusPts.includes(pt));\n lines[audioIdx] = [...parts.slice(0, 3), ...opusPts, ...otherPts].join(' ');\n }\n }\n\n return lines.join('\\r\\n');\n }\n\n /** @private */\n _closePeerConnection() {\n if (this._pc) {\n this._pc.close();\n this._pc = null;\n }\n this._candidateQueue = [];\n }\n\n}\n", "/**\n * @fuzionx/player \u2014 FuzionXPublisher (\uC1A1\uCD9C\uC790 \uBAA8\uB4DC)\n *\n * \uB85C\uCEEC \uCE74\uBA54\uB77C/\uB9C8\uC774\uD06C \u2192 \uC11C\uBC84 \uC804\uC1A1.\n * 2\uAC00\uC9C0 \uBC29\uC2DD \uC9C0\uC6D0:\n * A. WebSocket \uBC29\uC2DD (videochat \uC591\uBC29\uD5A5)\n * B. WHIP \uBC29\uC2DD (OBS/\uB2E8\uBC29\uD5A5 \uBC29\uC1A1)\n *\n * @example WebSocket\n * const pub = new FuzionXPublisher({\n * url: 'wss://media:50002',\n * channelId: 'my-live',\n * mode: 'videochat',\n * nickname: '\uBC1C\uD45C\uC790',\n * });\n * pub.on('ready', () => console.log('Publishing!'));\n * pub.connect();\n *\n * @example WHIP\n * const pub = new FuzionXPublisher({\n * whipUrl: 'https://media:7777/whip/my-live',\n * });\n * pub.connect();\n */\n\nimport { FuzionXSignaling } from './FuzionXSignaling.js';\nimport { DEFAULT_ICE_SERVERS, SignalType, SessionMode, MAX_SLOTS } from './constants.js';\n\nexport class FuzionXPublisher {\n /**\n * @param {Object} opts\n * @param {string} [opts.url] - WebSocket URL (WS \uBC29\uC2DD)\n * @param {string} [opts.whipUrl] - WHIP URL (WHIP \uBC29\uC2DD)\n * @param {string} [opts.channelId] - \uCC44\uB110 ID (WS \uBC29\uC2DD \uD544\uC218)\n * @param {string} [opts.mode='broadcast'] - 'broadcast' | 'videochat'\n * @param {string} [opts.nickname] - \uB2C9\uB124\uC784\n * @param {string} [opts.token] - \uC778\uC99D \uD1A0\uD070\n * @param {string} [opts.peerId] - \uD53C\uC5B4 ID\n * @param {MediaStreamConstraints} [opts.media] - getUserMedia \uC81C\uC57D\n * @param {MediaStream} [opts.stream] - \uC774\uBBF8 \uD68D\uB4DD\uD55C MediaStream\n * @param {RTCConfiguration} [opts.rtcConfig] - WebRTC \uC124\uC815\n * @param {boolean} [opts.autoReconnect=true]\n */\n constructor(opts) {\n // WHIP or WebSocket\n this.whipUrl = opts.whipUrl || null;\n this.url = opts.url || null;\n this.channelId = opts.channelId || null;\n this.mode = opts.mode || SessionMode.BROADCAST;\n this.nickname = opts.nickname || null;\n this.token = opts.token || null;\n this.peerId = opts.peerId || `pub-${Math.random().toString(36).slice(2, 10)}`;\n this.autoReconnect = opts.autoReconnect !== false;\n\n this.mediaConstraints = opts.media || { video: true, audio: true };\n this._externalStream = opts.stream || null;\n\n this.rtcConfig = opts.rtcConfig || {\n iceServers: DEFAULT_ICE_SERVERS,\n bundlePolicy: 'max-bundle',\n rtcpMuxPolicy: 'require',\n };\n\n /** @private */\n this._signaling = null;\n this._pc = null;\n this._localStream = null;\n this._listeners = {};\n this._candidateQueue = [];\n this._whipResourceUrl = null;\n this._maxSlots = MAX_SLOTS[this.mode] || 1;\n this._slots = new Map();\n this._connected = false;\n }\n\n // \u2500\u2500 Event System \u2500\u2500\n\n on(event, handler) {\n if (!this._listeners[event]) this._listeners[event] = [];\n this._listeners[event].push(handler);\n return this;\n }\n\n /** @private */\n _emit(event, ...args) {\n (this._listeners[event] || []).forEach((fn) => fn(...args));\n }\n\n // \u2500\u2500 Lifecycle \u2500\u2500\n\n /** \uC5F0\uACB0 \uC2DC\uC791 (\uBBF8\uB514\uC5B4 \uD68D\uB4DD \u2192 WebSocket \uB610\uB294 WHIP). */\n async connect() {\n try {\n await this._acquireMedia();\n\n if (this.whipUrl) {\n await this._connectWhip();\n } else if (this.url) {\n this._connectWebSocket();\n } else {\n throw new Error('url \uB610\uB294 whipUrl \uC911 \uD558\uB098\uB97C \uC9C0\uC815\uD574\uC57C \uD569\uB2C8\uB2E4.');\n }\n } catch (e) {\n this._emit('error', e);\n }\n }\n\n /** \uC5F0\uACB0 \uC885\uB8CC. */\n async disconnect() {\n if (this.whipUrl && this._whipResourceUrl) {\n // WHIP DELETE\n try {\n const baseUrl = new URL(this.whipUrl).origin;\n await fetch(`${baseUrl}${this._whipResourceUrl}`, { method: 'DELETE' });\n } catch (e) {\n console.warn('[FuzionX] WHIP DELETE error:', e);\n }\n this._whipResourceUrl = null;\n }\n\n if (this._signaling) {\n this._signaling.sendLeave();\n this._signaling.disconnect();\n }\n\n this._closePeerConnection();\n this._stopMedia();\n this._connected = false;\n }\n\n /** \uCC44\uD305 \uC804\uC1A1. */\n chat(text) {\n if (this._signaling) {\n this._signaling.sendChat(text, this.nickname);\n }\n }\n\n /** @returns {MediaStream|null} \uB85C\uCEEC \uC2A4\uD2B8\uB9BC */\n get localStream() {\n return this._localStream;\n }\n\n /** @returns {Map} \uC2AC\uB86F \uC815\uBCF4 (videochat: \uB2E4\uB978 \uCC38\uAC00\uC790) */\n get slots() {\n return this._slots;\n }\n\n // \u2500\u2500 Media \u2500\u2500\n\n /** @private \uBBF8\uB514\uC5B4 \uD68D\uB4DD */\n async _acquireMedia() {\n if (this._externalStream) {\n this._localStream = this._externalStream;\n } else {\n this._localStream = await navigator.mediaDevices.getUserMedia(this.mediaConstraints);\n }\n this._emit('media', this._localStream);\n }\n\n /** @private */\n _stopMedia() {\n if (this._localStream && !this._externalStream) {\n this._localStream.getTracks().forEach((t) => t.stop());\n }\n this._localStream = null;\n }\n\n // \u2500\u2500 WHIP \u2500\u2500\n\n /** @private WHIP \uC5F0\uACB0 */\n async _connectWhip() {\n this._pc = new RTCPeerConnection(this.rtcConfig);\n\n // \uD2B8\uB799 \uCD94\uAC00\n this._localStream.getTracks().forEach((track) => {\n this._pc.addTrack(track, this._localStream);\n });\n\n // ICE Gathering \uC644\uB8CC \uB300\uAE30\n const offer = await this._pc.createOffer();\n offer.sdp = FuzionXPublisher._forceCodecs(offer.sdp);\n await this._pc.setLocalDescription(offer);\n\n // Gathering \uC644\uB8CC \uB300\uAE30\n await new Promise((resolve) => {\n if (this._pc.iceGatheringState === 'complete') {\n resolve();\n } else {\n this._pc.onicegatheringstatechange = () => {\n if (this._pc.iceGatheringState === 'complete') resolve();\n };\n // \uC548\uC804 \uD0C0\uC784\uC544\uC6C3\n setTimeout(resolve, 3000);\n }\n });\n\n const localDesc = this._pc.localDescription;\n // Hub API\uC758 whip_url\uC740 \uC774\uBBF8 ?token=xxx \uD3EC\uD568 \uAC00\uB2A5\n let url = this.whipUrl;\n if (this.token && !url.includes('token=')) {\n url += (url.includes('?') ? '&' : '?') + `token=${this.token}`;\n }\n\n const response = await fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/sdp' },\n body: localDesc.sdp,\n });\n\n if (response.status !== 201) {\n throw new Error(`WHIP failed: ${response.status} ${await response.text()}`);\n }\n\n const answerSdp = await response.text();\n this._whipResourceUrl = response.headers.get('location');\n\n await this._pc.setRemoteDescription(\n new RTCSessionDescription({ type: 'answer', sdp: answerSdp })\n );\n\n this._pc.onconnectionstatechange = () => {\n const state = this._pc?.connectionState;\n if (state === 'connected') {\n this._connected = true;\n this._emit('ready');\n } else if (state === 'failed' || state === 'disconnected') {\n this._emit('error', new Error(`WHIP PeerConnection ${state}`));\n }\n };\n\n this._emit('ready');\n }\n\n // \u2500\u2500 WebSocket \u2500\u2500\n\n /** @private WS \uC5F0\uACB0 */\n _connectWebSocket() {\n this._signaling = new FuzionXSignaling({\n url: this.url,\n autoReconnect: this.autoReconnect,\n onOpen: () => this._onSignalingOpen(),\n onMessage: (msg) => this._onSignalingMessage(msg),\n onClose: (evt) => {\n this._closePeerConnection();\n this._connected = false;\n this._emit('close', evt);\n },\n onError: (err) => this._emit('error', err),\n });\n this._signaling.connect();\n }\n\n /** @private */\n async _onSignalingOpen() {\n this._signaling.sendJoin(this.peerId, this.channelId, {\n nickname: this.nickname,\n token: this.token,\n mode: this.mode,\n });\n await this._createPeerConnection();\n }\n\n /** @private */\n async _createPeerConnection() {\n this._pc = new RTCPeerConnection(this.rtcConfig);\n\n // Non-Trickle: ICE candidate\uB294 SDP\uC5D0 \uD3EC\uD568\n\n // \uB85C\uCEEC \uD2B8\uB799 \uCD94\uAC00 (\uC1A1\uCD9C)\n this._localStream.getTracks().forEach((track) => {\n this._pc.addTrack(track, this._localStream);\n });\n\n // videochat \uBAA8\uB4DC: \uC218\uC2E0 \uD2B8\uB799\uB3C4 \uC900\uBE44 (\uC591\uBC29\uD5A5)\n if (this.mode === SessionMode.VIDEOCHAT) {\n for (let i = 0; i < this._maxSlots; i++) {\n this._pc.addTransceiver('video', { direction: 'recvonly' });\n this._pc.addTransceiver('audio', { direction: 'recvonly' });\n }\n\n // \uC1A1\uCD9C \uD2B8\uB799\uC740 addTrack\uC73C\uB85C \uCD94\uAC00\uB428 \u2192 transceiver\uBC29\uD5A5\uC744 sendrecv\uB85C \uC5C5\uADF8\uB808\uC774\uB4DC\n const transceivers = this._pc.getTransceivers();\n transceivers.forEach((t) => {\n if (t.sender.track && t.direction === 'recvonly') {\n t.direction = 'sendrecv';\n }\n });\n\n let videoTrackCount = 0;\n this._pc.ontrack = (event) => {\n const track = event.track;\n if (track.kind === 'video') {\n const slotIndex = videoTrackCount++;\n let stream = event.streams[0];\n if (!stream) {\n stream = new MediaStream();\n stream.addTrack(track);\n }\n const slot = this._slots.get(slotIndex) || { slotIndex };\n slot.stream = stream;\n this._slots.set(slotIndex, slot);\n this._emit('stream', stream, slotIndex);\n }\n };\n }\n\n this._pc.onconnectionstatechange = () => {\n const state = this._pc?.connectionState;\n if (state === 'connected' && !this._connected) {\n this._connected = true;\n this._emit('ready');\n } else if (state === 'failed' || state === 'disconnected') {\n this._emit('error', new Error(`PeerConnection ${state}`));\n }\n };\n\n // Offer + H264/Opus SDP \uAC15\uC81C\n const offer = await this._pc.createOffer();\n offer.sdp = FuzionXPublisher._forceCodecs(offer.sdp);\n await this._pc.setLocalDescription(offer);\n\n // Non-Trickle: ICE gathering \uC644\uB8CC \uB300\uAE30\n await this._waitForIceGathering();\n\n const finalSdp = this._pc.localDescription?.sdp;\n if (finalSdp) {\n this._signaling.sendOffer(finalSdp);\n }\n }\n\n /** @private */\n _waitForIceGathering() {\n return new Promise((resolve) => {\n if (this._pc.iceGatheringState === 'complete') {\n return resolve();\n }\n const check = () => {\n if (this._pc?.iceGatheringState === 'complete') {\n this._pc.removeEventListener('icegatheringstatechange', check);\n resolve();\n }\n };\n this._pc.addEventListener('icegatheringstatechange', check);\n setTimeout(() => {\n if (this._pc) {\n this._pc.removeEventListener('icegatheringstatechange', check);\n }\n resolve();\n }, 3000);\n });\n }\n\n /** @private */\n static _forceCodecs(sdp) {\n let lines = sdp.split('\\r\\n');\n const videoIdx = lines.findIndex((l) => l.startsWith('m=video'));\n if (videoIdx !== -1) {\n const h264Pts = [];\n const h264Scores = new Map();\n lines.forEach((l) => {\n const m = l.match(/a=rtpmap:(\\d+) H264\\/90000/);\n if (m) { h264Pts.push(m[1]); h264Scores.set(m[1], 0); }\n });\n lines.forEach((l) => {\n if (l.startsWith('a=fmtp:')) {\n const pt = l.split(' ')[0].split(':')[1];\n if (h264Scores.has(pt)) {\n if (l.includes('profile-level-id=42e01f')) h264Scores.set(pt, 100);\n else if (l.includes('profile-level-id=42001f')) h264Scores.set(pt, 80);\n if (l.includes('packetization-mode=1')) h264Scores.set(pt, (h264Scores.get(pt) || 0) + 10);\n }\n }\n });\n if (h264Pts.length > 0) {\n h264Pts.sort((a, b) => h264Scores.get(b) - h264Scores.get(a));\n const parts = lines[videoIdx].split(' ');\n const otherPts = parts.slice(3).filter((pt) => !h264Pts.includes(pt));\n lines[videoIdx] = [...parts.slice(0, 3), ...h264Pts, ...otherPts].join(' ');\n }\n }\n const audioIdx = lines.findIndex((l) => l.startsWith('m=audio'));\n if (audioIdx !== -1) {\n const opusPts = [];\n lines.forEach((l) => {\n const m = l.match(/a=rtpmap:(\\d+) opus\\/48000/);\n if (m) opusPts.push(m[1]);\n });\n if (opusPts.length > 0) {\n const parts = lines[audioIdx].split(' ');\n const otherPts = parts.slice(3).filter((pt) => !opusPts.includes(pt));\n lines[audioIdx] = [...parts.slice(0, 3), ...opusPts, ...otherPts].join(' ');\n }\n }\n return lines.join('\\r\\n');\n }\n\n /** @private */\n _onSignalingMessage(msg) {\n switch (msg.type) {\n case SignalType.ANSWER:\n this._handleAnswer(msg);\n break;\n case SignalType.CANDIDATE:\n this._handleCandidate(msg);\n break;\n case SignalType.SLOT_INFO:\n this._handleSlotInfo(msg);\n break;\n case SignalType.CHAT:\n this._emit('chat', {\n peerId: msg.peer_id,\n nickname: msg.nickname,\n text: msg.text,\n });\n break;\n case SignalType.ERROR:\n this._emit('error', new Error(msg.message));\n break;\n }\n }\n\n /** @private */\n async _handleAnswer(msg) {\n if (!this._pc) return;\n try {\n await this._pc.setRemoteDescription(\n new RTCSessionDescription({ type: 'answer', sdp: msg.sdp })\n );\n for (const c of this._candidateQueue) {\n await this._pc.addIceCandidate(c);\n }\n this._candidateQueue = [];\n } catch (e) {\n this._emit('error', e);\n }\n }\n\n /** @private */\n async _handleCandidate(msg) {\n const candidate = new RTCIceCandidate({\n candidate: msg.candidate,\n sdpMid: msg.sdp_mid,\n sdpMLineIndex: msg.sdp_m_line_index,\n });\n if (this._pc && this._pc.remoteDescription) {\n try { await this._pc.addIceCandidate(candidate); } catch (e) { /* ignore */ }\n } else {\n this._candidateQueue.push(candidate);\n }\n }\n\n /** @private */\n _handleSlotInfo(msg) {\n const slotIndex = parseInt(msg.stream_id.replace('stream_', ''), 10);\n if (!msg.nickname || msg.nickname === '') {\n this._slots.delete(slotIndex);\n this._emit('slot_remove', { slotIndex, senderId: msg.sender_id });\n return;\n }\n const existing = this._slots.get(slotIndex) || {};\n this._slots.set(slotIndex, {\n ...existing,\n slotIndex,\n streamId: msg.stream_id,\n nickname: msg.nickname,\n senderId: msg.sender_id,\n });\n this._emit('slot', this._slots.get(slotIndex));\n }\n\n /** @private */\n _closePeerConnection() {\n if (this._pc) {\n this._pc.close();\n this._pc = null;\n }\n this._candidateQueue = [];\n }\n}\n"],
5
- "mappings": "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",
6
- "names": ["src_exports", "__export", "CODEC", "DEFAULT_ICE_SERVERS", "FuzionXPublisher", "FuzionXSignaling", "FuzionXViewer", "MAX_SLOTS", "RECONNECT", "SessionMode", "SignalType", "DEFAULT_ICE_SERVERS", "SignalType", "SessionMode", "MAX_SLOTS", "RECONNECT", "CODEC", "FuzionXSignaling", "opts", "event", "msg", "peerId", "channelId", "SignalType", "sdp", "candidate", "text", "nickname", "RECONNECT", "delay", "FuzionXViewer", "_FuzionXViewer", "opts", "SessionMode", "DEFAULT_ICE_SERVERS", "MAX_SLOTS", "event", "handler", "args", "fn", "FuzionXSignaling", "msg", "evt", "err", "text", "SignalType", "c", "e", "candidate", "slotIndex", "existing", "videoTrackCount", "track", "stream", "slot", "state", "i", "offer", "finalSdp", "resolve", "check", "sdp", "lines", "videoIdx", "l", "h264Pts", "h264Scores", "m", "pt", "a", "b", "parts", "otherPts", "audioIdx", "opusPts", "FuzionXPublisher", "_FuzionXPublisher", "opts", "SessionMode", "DEFAULT_ICE_SERVERS", "MAX_SLOTS", "event", "handler", "args", "fn", "baseUrl", "text", "t", "track", "offer", "resolve", "localDesc", "url", "response", "answerSdp", "state", "FuzionXSignaling", "msg", "evt", "err", "i", "videoTrackCount", "slotIndex", "stream", "slot", "finalSdp", "check", "sdp", "lines", "videoIdx", "l", "h264Pts", "h264Scores", "m", "pt", "a", "b", "parts", "otherPts", "audioIdx", "opusPts", "SignalType", "c", "e", "candidate", "existing"]
4
+ "sourcesContent": ["/**\n * @fuzionx/player \u2014 Entry Point\n *\n * FuzionX WebRTC Player SDK\n */\n\nexport { FuzionXViewer } from './FuzionXViewer.js';\nexport { FuzionXPublisher } from './FuzionXPublisher.js';\nexport { FuzionXSignaling } from './FuzionXSignaling.js';\nexport { SignalType, SessionMode, MAX_SLOTS, DEFAULT_ICE_SERVERS, CODEC, RECONNECT } from './constants.js';\n", "/**\n * @fuzionx/player \u2014 Constants & Default Configuration\n */\n\n/** Default ICE Servers */\nexport const DEFAULT_ICE_SERVERS = [\n { urls: 'stun:stun.l.google.com:19302' },\n { urls: 'stun:stun1.l.google.com:19302' },\n];\n\n/** Signaling message types (maps to server SignalMessage enum) */\nexport const SignalType = {\n JOIN: 'join',\n OFFER: 'offer',\n ANSWER: 'answer',\n CANDIDATE: 'candidate',\n PLI: 'pli',\n LEAVE: 'leave',\n SLOT_INFO: 'slot_info',\n CHAT: 'chat',\n ERROR: 'error',\n};\n\n/** Session modes */\nexport const SessionMode = {\n BROADCAST: 'broadcast',\n VIDEOCHAT: 'videochat',\n};\n\n/** Max slots per mode */\nexport const MAX_SLOTS = {\n [SessionMode.BROADCAST]: 1,\n [SessionMode.VIDEOCHAT]: 9,\n};\n\n/** Default reconnect settings */\nexport const RECONNECT = {\n MAX_RETRIES: 5,\n BASE_DELAY_MS: 1000,\n MAX_DELAY_MS: 30000,\n};\n\n/** Codec preferences */\nexport const CODEC = {\n VIDEO_MIME: 'video/H264',\n AUDIO_MIME: 'audio/opus',\n VIDEO_CLOCK: 90000,\n AUDIO_CLOCK: 48000,\n};\n", "/**\n * @fuzionx/player \u2014 WebSocket Signaling Layer\n *\n * FuzionX Media Server\uC758 JSON \uC2DC\uADF8\uB110\uB9C1 \uD504\uB85C\uD1A0\uCF5C \uCEA1\uC290\uD654.\n * \uC790\uB3D9 \uC7AC\uC5F0\uACB0 + \uC774\uBCA4\uD2B8 \uC2DC\uC2A4\uD15C.\n */\n\nimport { SignalType, RECONNECT } from './constants.js';\n\nexport class FuzionXSignaling {\n /**\n * @param {Object} opts\n * @param {string} opts.url - WebSocket URL (ws:// or wss://)\n * @param {Function} [opts.onMessage] - \uBA54\uC2DC\uC9C0 \uC218\uC2E0 \uCF5C\uBC31\n * @param {Function} [opts.onOpen] - \uC5F0\uACB0 \uC131\uACF5 \uCF5C\uBC31\n * @param {Function} [opts.onClose] - \uC5F0\uACB0 \uC885\uB8CC \uCF5C\uBC31\n * @param {Function} [opts.onError] - \uC5D0\uB7EC \uCF5C\uBC31\n * @param {boolean} [opts.autoReconnect=true]\n */\n constructor(opts) {\n this.url = opts.url;\n this.onMessage = opts.onMessage || (() => {});\n this.onOpen = opts.onOpen || (() => {});\n this.onClose = opts.onClose || (() => {});\n this.onError = opts.onError || (() => {});\n this.autoReconnect = opts.autoReconnect !== false;\n\n /** @private */\n this._ws = null;\n this._retryCount = 0;\n this._reconnectTimer = null;\n this._intentionalClose = false;\n }\n\n /** WebSocket \uC5F0\uACB0. */\n connect() {\n this._intentionalClose = false;\n this._doConnect();\n }\n\n /** @private */\n _doConnect() {\n try {\n this._ws = new WebSocket(this.url);\n } catch (e) {\n this.onError(e);\n this._scheduleReconnect();\n return;\n }\n\n this._ws.onopen = () => {\n this._retryCount = 0;\n this.onOpen();\n };\n\n this._ws.onmessage = (event) => {\n try {\n const msg = JSON.parse(event.data);\n this.onMessage(msg);\n } catch (e) {\n console.warn('[FuzionX] Invalid JSON:', event.data);\n }\n };\n\n this._ws.onclose = (event) => {\n this.onClose(event);\n if (!this._intentionalClose && this.autoReconnect) {\n this._scheduleReconnect();\n }\n };\n\n this._ws.onerror = (event) => {\n this.onError(event);\n };\n }\n\n /** JSON \uBA54\uC2DC\uC9C0 \uC804\uC1A1. */\n send(msg) {\n if (this._ws && this._ws.readyState === WebSocket.OPEN) {\n this._ws.send(JSON.stringify(msg));\n return true;\n }\n return false;\n }\n\n // \u2500\u2500 \uC2DC\uADF8\uB110\uB9C1 \uD5EC\uD37C \u2500\u2500\n\n /** Join \uC804\uC1A1 */\n sendJoin(peerId, channelId, opts = {}) {\n return this.send({\n type: SignalType.JOIN,\n peer_id: peerId,\n channel_id: channelId,\n nickname: opts.nickname || null,\n token: opts.token || null,\n mode: opts.mode || null,\n });\n }\n\n /** SDP Offer \uC804\uC1A1 */\n sendOffer(sdp) {\n return this.send({ type: SignalType.OFFER, sdp });\n }\n\n /** SDP Answer \uC804\uC1A1 */\n sendAnswer(sdp) {\n return this.send({ type: SignalType.ANSWER, sdp });\n }\n\n /** ICE Candidate \uC804\uC1A1 */\n sendCandidate(candidate) {\n return this.send({\n type: SignalType.CANDIDATE,\n candidate: candidate.candidate,\n sdp_mid: candidate.sdpMid,\n sdp_m_line_index: candidate.sdpMLineIndex,\n });\n }\n\n /** Chat \uC804\uC1A1 */\n sendChat(text, nickname) {\n return this.send({\n type: SignalType.CHAT,\n text,\n nickname: nickname || null,\n peer_id: null,\n });\n }\n\n /** PLI \uC694\uCCAD */\n sendPLI() {\n return this.send({ type: SignalType.PLI });\n }\n\n /** Leave \uC804\uC1A1 */\n sendLeave() {\n return this.send({ type: SignalType.LEAVE });\n }\n\n /** \uC5F0\uACB0 \uC885\uB8CC (\uC7AC\uC5F0\uACB0 \uC548 \uD568). */\n disconnect() {\n this._intentionalClose = true;\n clearTimeout(this._reconnectTimer);\n if (this._ws) {\n this._ws.close();\n this._ws = null;\n }\n }\n\n /** @returns {boolean} \uC5F0\uACB0 \uC0C1\uD0DC */\n get connected() {\n return this._ws && this._ws.readyState === WebSocket.OPEN;\n }\n\n /** @private \uC7AC\uC5F0\uACB0 \uC2A4\uCF00\uC904 (Exponential Backoff). */\n _scheduleReconnect() {\n if (this._retryCount >= RECONNECT.MAX_RETRIES) {\n console.error('[FuzionX] Max reconnect retries reached.');\n this.onError(new Error('Max reconnect retries'));\n return;\n }\n const delay = Math.min(\n RECONNECT.BASE_DELAY_MS * Math.pow(2, this._retryCount),\n RECONNECT.MAX_DELAY_MS\n );\n this._retryCount++;\n console.log(`[FuzionX] Reconnecting in ${delay}ms (${this._retryCount}/${RECONNECT.MAX_RETRIES})`);\n this._reconnectTimer = setTimeout(() => this._doConnect(), delay);\n }\n}\n", "/**\n * @fuzionx/player \u2014 FuzionXViewer (\uC218\uC2E0\uC790 \uBAA8\uB4DC)\n *\n * \uC11C\uBC84\uC5D0\uC11C MediaStream\uC744 \uC218\uC2E0\uD558\uC5EC <video> \uC5D8\uB9AC\uBA3C\uD2B8\uC5D0 \uB80C\uB354\uB9C1.\n * broadcast(1 stream) / videochat(\uCD5C\uB300 9 streams) \uC9C0\uC6D0.\n *\n * @example\n * const viewer = new FuzionXViewer({\n * url: 'wss://media:50002',\n * channelId: 'my-live',\n * mode: 'broadcast',\n * });\n * viewer.on('stream', (stream, slotIndex) => {\n * videoEl.srcObject = stream;\n * });\n * viewer.connect();\n */\n\nimport { FuzionXSignaling } from './FuzionXSignaling.js';\nimport { DEFAULT_ICE_SERVERS, SignalType, SessionMode, MAX_SLOTS } from './constants.js';\n\nexport class FuzionXViewer {\n /**\n * @param {Object} opts\n * @param {string} [opts.url] - WebSocket URL (\uC9C1\uC811 \uC9C0\uC815)\n * @param {string} [opts.hubUrl] - Hub API URL (\uC790\uB3D9 \uB77C\uC6B0\uD305: Hub\uC5D0\uC11C \uBBF8\uB514\uC5B4 \uC11C\uBC84 \uC870\uD68C)\n * @param {string} opts.channelId - \uCC44\uB110 ID\n * @param {string} [opts.mode='broadcast'] - 'broadcast' | 'videochat'\n * @param {string} [opts.nickname] - \uB2C9\uB124\uC784\n * @param {string} [opts.token] - \uC778\uC99D \uD1A0\uD070\n * @param {string} [opts.peerId] - \uD53C\uC5B4 ID (\uC790\uB3D9 \uC0DD\uC131)\n * @param {RTCConfiguration} [opts.rtcConfig] - WebRTC \uC124\uC815 \uC624\uBC84\uB77C\uC774\uB4DC\n * @param {boolean} [opts.autoReconnect=true]\n */\n constructor(opts) {\n this.url = opts.url || null;\n this.hubUrl = opts.hubUrl || null;\n this.channelId = opts.channelId;\n this.mode = opts.mode || SessionMode.BROADCAST;\n this.nickname = opts.nickname || null;\n this.token = opts.token || null;\n this.peerId = opts.peerId || `viewer-${Math.random().toString(36).slice(2, 10)}`;\n this.autoReconnect = opts.autoReconnect !== false;\n\n this.rtcConfig = opts.rtcConfig || {\n iceServers: DEFAULT_ICE_SERVERS,\n bundlePolicy: 'max-bundle',\n rtcpMuxPolicy: 'require',\n };\n\n /** @private */\n this._signaling = null;\n this._pc = null;\n this._listeners = {};\n this._slots = new Map(); // slotIndex \u2192 { streamId, nickname, senderId, stream }\n this._maxSlots = MAX_SLOTS[this.mode] || 1;\n this._candidateQueue = [];\n this._connected = false;\n }\n\n // \u2500\u2500 Event System \u2500\u2500\n\n /**\n * \uC774\uBCA4\uD2B8 \uB9AC\uC2A4\uB108 \uB4F1\uB85D.\n * @param {'stream'|'slot'|'slot_remove'|'chat'|'error'|'close'|'connected'} event\n * @param {Function} handler\n */\n on(event, handler) {\n if (!this._listeners[event]) this._listeners[event] = [];\n this._listeners[event].push(handler);\n return this;\n }\n\n /** @private */\n _emit(event, ...args) {\n (this._listeners[event] || []).forEach((fn) => fn(...args));\n }\n\n // \u2500\u2500 Lifecycle \u2500\u2500\n\n /** \uC11C\uBC84 \uC5F0\uACB0 + WebRTC \uC138\uC158 \uC2DC\uC791. */\n async connect() {\n // Hub \uB77C\uC6B0\uD305: hubUrl\uC774 \uC788\uC73C\uBA74 Hub\uC5D0\uC11C \uBBF8\uB514\uC5B4 \uC11C\uBC84 \uC870\uD68C\n if (!this.url && this.hubUrl) {\n try {\n const res = await fetch(`${this.hubUrl}/api/channels/${this.channelId}`);\n if (!res.ok) throw new Error(`Channel not found: ${this.channelId}`);\n const data = await res.json();\n // ws_url \uC0AC\uC6A9 \uB610\uB294 ip:port\uC5D0\uC11C \uC0DD\uC131\n if (data.ws_url) {\n // Hub\uC640 \uAC19\uC740 \uD504\uB85C\uD1A0\uCF5C \uC0AC\uC6A9 (https\u2192wss, http\u2192ws)\n const isSecure = this.hubUrl.startsWith('https');\n this.url = data.ws_url.replace(/^ws(s?):/, isSecure ? 'wss:' : 'ws:');\n } else {\n const isSecure = this.hubUrl.startsWith('https');\n const wsProto = isSecure ? 'wss' : 'ws';\n this.url = `${wsProto}://${data.media_ip}:${data.webrtc_port}`;\n }\n } catch (e) {\n this._emit('error', e);\n return;\n }\n }\n\n if (!this.url) {\n this._emit('error', new Error('url \uB610\uB294 hubUrl\uC744 \uC9C0\uC815\uD574\uC57C \uD569\uB2C8\uB2E4.'));\n return;\n }\n\n this._signaling = new FuzionXSignaling({\n url: this.url,\n autoReconnect: this.autoReconnect,\n onOpen: () => this._onSignalingOpen(),\n onMessage: (msg) => this._onSignalingMessage(msg),\n onClose: (evt) => this._onSignalingClose(evt),\n onError: (err) => this._emit('error', err),\n });\n this._signaling.connect();\n }\n\n /** \uC5F0\uACB0 \uC885\uB8CC. */\n disconnect() {\n if (this._signaling) {\n this._signaling.sendLeave();\n this._signaling.disconnect();\n }\n this._closePeerConnection();\n this._slots.clear();\n this._connected = false;\n }\n\n /** \uCC44\uD305 \uC804\uC1A1. */\n chat(text) {\n if (this._signaling) {\n this._signaling.sendChat(text, this.nickname);\n }\n }\n\n /** \uD0A4\uD504\uB808\uC784 \uC694\uCCAD. */\n requestKeyframe() {\n if (this._signaling) {\n this._signaling.sendPLI();\n }\n }\n\n /** @returns {Map} \uD604\uC7AC \uC2AC\uB86F \uC815\uBCF4 */\n get slots() {\n return this._slots;\n }\n\n // \u2500\u2500 Internal \u2500\u2500\n\n /** @private */\n _onSignalingOpen() {\n this._signaling.sendJoin(this.peerId, this.channelId, {\n nickname: this.nickname,\n token: this.token,\n mode: this.mode,\n });\n // Join \uC804\uC1A1 \uD6C4 \uBC14\uB85C PeerConnection \uC0DD\uC131\n this._createPeerConnection().catch((e) => this._emit('error', e));\n }\n\n /** @private */\n _onSignalingMessage(msg) {\n switch (msg.type) {\n case SignalType.ANSWER:\n this._handleAnswer(msg);\n break;\n case SignalType.CANDIDATE:\n this._handleCandidate(msg);\n break;\n case SignalType.SLOT_INFO:\n this._handleSlotInfo(msg);\n break;\n case SignalType.CHAT:\n this._emit('chat', {\n peerId: msg.peer_id,\n nickname: msg.nickname,\n text: msg.text,\n });\n break;\n case SignalType.ERROR:\n this._emit('error', new Error(msg.message));\n break;\n }\n }\n\n /** @private */\n _onSignalingClose(evt) {\n this._closePeerConnection();\n this._connected = false;\n this._emit('close', evt);\n }\n\n /** @private */\n async _handleAnswer(msg) {\n if (!this._pc) return;\n try {\n await this._pc.setRemoteDescription(\n new RTCSessionDescription({ type: 'answer', sdp: msg.sdp })\n );\n // Flush queued ICE candidates\n for (const c of this._candidateQueue) {\n await this._pc.addIceCandidate(c);\n }\n this._candidateQueue = [];\n } catch (e) {\n this._emit('error', e);\n }\n }\n\n /** @private */\n async _handleCandidate(msg) {\n const candidate = new RTCIceCandidate({\n candidate: msg.candidate,\n sdpMid: msg.sdp_mid,\n sdpMLineIndex: msg.sdp_m_line_index,\n });\n if (this._pc && this._pc.remoteDescription) {\n try {\n await this._pc.addIceCandidate(candidate);\n } catch (e) {\n console.warn('[FuzionX] ICE candidate error:', e);\n }\n } else {\n this._candidateQueue.push(candidate);\n }\n }\n\n /** @private */\n _handleSlotInfo(msg) {\n const slotIndex = parseInt(msg.stream_id.replace('stream_', ''), 10);\n\n // nickname\uC774 \uBE48 \uBB38\uC790\uC5F4\uC774\uBA74 \uC2AC\uB86F \uD574\uC81C\n if (!msg.nickname || msg.nickname === '') {\n this._slots.delete(slotIndex);\n this._emit('slot_remove', { slotIndex, senderId: msg.sender_id });\n return;\n }\n\n const existing = this._slots.get(slotIndex) || {};\n this._slots.set(slotIndex, {\n ...existing,\n slotIndex,\n streamId: msg.stream_id,\n nickname: msg.nickname,\n senderId: msg.sender_id,\n });\n this._emit('slot', this._slots.get(slotIndex));\n }\n\n /**\n * PeerConnection \uC0DD\uC131 + Non-Trickle ICE Offer.\n * old alloy-player \uAC80\uC99D \uD328\uD134:\n * 1. addTransceiver(recvonly) \u00D7 maxSlots \u2014 \uC11C\uBC84 m-line \uB9E4\uD551\n * 2. createOffer \u2192 H264 SDP \uAC15\uC81C\n * 3. ICE gathering \uC644\uB8CC \uB300\uAE30 \uD6C4 SDP \uC804\uCCB4 \uC804\uC1A1 (Non-Trickle)\n * @private\n */\n async _createPeerConnection() {\n this._pc = new RTCPeerConnection(this.rtcConfig);\n\n // Non-Trickle: ICE candidate\uB294 SDP\uC5D0 \uD3EC\uD568\uB418\uC5B4 \uC804\uC1A1\uB428\n // (onicecandidate\uB294 gathering \uCD94\uC801\uC6A9\uC73C\uB85C\uB9CC \uC720\uC9C0)\n\n // \uC218\uC2E0 \uD2B8\uB799 \uB9E4\uD551\n let videoTrackCount = 0;\n this._pc.ontrack = (event) => {\n const track = event.track;\n\n if (track.kind === 'video') {\n const slotIndex = videoTrackCount++;\n\n let stream = event.streams[0];\n if (!stream) {\n stream = new MediaStream();\n stream.addTrack(track);\n }\n\n const slot = this._slots.get(slotIndex) || { slotIndex };\n slot.stream = stream;\n this._slots.set(slotIndex, slot);\n\n this._emit('stream', stream, slotIndex);\n\n if (!this._connected) {\n this._connected = true;\n this._emit('connected');\n\n // \uC989\uC2DC \uD0A4\uD504\uB808\uC784 \uC694\uCCAD (\uCCAB \uD504\uB808\uC784 \uBE60\uB974\uAC8C)\n if (this._signaling) {\n this._signaling.sendPLI();\n }\n }\n }\n };\n\n this._pc.onconnectionstatechange = () => {\n const state = this._pc?.connectionState;\n if (state === 'failed' || state === 'disconnected') {\n this._emit('error', new Error(`PeerConnection ${state}`));\n }\n };\n\n // recvonly Transceiver \uCD94\uAC00 (\uC11C\uBC84 \uC2AC\uB86F \uC218\uC5D0 \uB9DE\uCDA4)\n for (let i = 0; i < this._maxSlots; i++) {\n this._pc.addTransceiver('video', { direction: 'recvonly' });\n this._pc.addTransceiver('audio', { direction: 'recvonly' });\n }\n\n // Offer \uC0DD\uC131 + H264/Opus \uCF54\uB371 \uAC15\uC81C\n const offer = await this._pc.createOffer();\n offer.sdp = FuzionXViewer._forceCodecs(offer.sdp);\n await this._pc.setLocalDescription(offer);\n\n // Non-Trickle ICE: gathering \uC644\uB8CC \uB300\uAE30 \uD6C4 \uC804\uCCB4 SDP \uC804\uC1A1\n await this._waitForIceGathering();\n\n // gathering \uC644\uB8CC \uD6C4 \uCD5C\uC885 SDP (ICE candidates \uD3EC\uD568) \uC804\uC1A1\n const finalSdp = this._pc.localDescription?.sdp;\n if (finalSdp) {\n this._signaling.sendOffer(finalSdp);\n }\n }\n\n /**\n * ICE gathering \uC644\uB8CC \uB300\uAE30 (Non-Trickle)\n * @private\n */\n _waitForIceGathering() {\n return new Promise((resolve) => {\n if (this._pc.iceGatheringState === 'complete') {\n return resolve();\n }\n const check = () => {\n if (this._pc?.iceGatheringState === 'complete') {\n this._pc.removeEventListener('icegatheringstatechange', check);\n resolve();\n }\n };\n this._pc.addEventListener('icegatheringstatechange', check);\n // 500ms \uD6C4 gathering \uC548 \uB05D\uB098\uBA74 \uD604\uC7AC SDP\uB85C \uC9C4\uD589\n setTimeout(() => {\n if (this._pc) {\n this._pc.removeEventListener('icegatheringstatechange', check);\n }\n resolve();\n }, 150);\n });\n }\n\n /**\n * SDP\uC5D0\uC11C H264 + Opus \uCF54\uB371 \uC6B0\uC120 \uAC15\uC81C.\n * @private\n */\n static _forceCodecs(sdp) {\n let lines = sdp.split('\\r\\n');\n\n // Video: H264 \uC6B0\uC120\n const videoIdx = lines.findIndex((l) => l.startsWith('m=video'));\n if (videoIdx !== -1) {\n const h264Pts = [];\n const h264Scores = new Map();\n lines.forEach((l) => {\n const m = l.match(/a=rtpmap:(\\d+) H264\\/90000/);\n if (m) {\n h264Pts.push(m[1]);\n h264Scores.set(m[1], 0);\n }\n });\n lines.forEach((l) => {\n if (l.startsWith('a=fmtp:')) {\n const pt = l.split(' ')[0].split(':')[1];\n if (h264Scores.has(pt)) {\n if (l.includes('profile-level-id=42e01f')) h264Scores.set(pt, 100);\n else if (l.includes('profile-level-id=42001f')) h264Scores.set(pt, 80);\n if (l.includes('packetization-mode=1')) h264Scores.set(pt, (h264Scores.get(pt) || 0) + 10);\n }\n }\n });\n if (h264Pts.length > 0) {\n h264Pts.sort((a, b) => h264Scores.get(b) - h264Scores.get(a));\n const parts = lines[videoIdx].split(' ');\n const otherPts = parts.slice(3).filter((pt) => !h264Pts.includes(pt));\n lines[videoIdx] = [...parts.slice(0, 3), ...h264Pts, ...otherPts].join(' ');\n }\n }\n\n // Audio: Opus \uC6B0\uC120\n const audioIdx = lines.findIndex((l) => l.startsWith('m=audio'));\n if (audioIdx !== -1) {\n const opusPts = [];\n lines.forEach((l) => {\n const m = l.match(/a=rtpmap:(\\d+) opus\\/48000/);\n if (m) opusPts.push(m[1]);\n });\n if (opusPts.length > 0) {\n const parts = lines[audioIdx].split(' ');\n const otherPts = parts.slice(3).filter((pt) => !opusPts.includes(pt));\n lines[audioIdx] = [...parts.slice(0, 3), ...opusPts, ...otherPts].join(' ');\n }\n }\n\n return lines.join('\\r\\n');\n }\n\n /** @private */\n _closePeerConnection() {\n if (this._pc) {\n this._pc.close();\n this._pc = null;\n }\n this._candidateQueue = [];\n }\n\n}\n", "/**\n * @fuzionx/player \u2014 FuzionXPublisher (\uC1A1\uCD9C\uC790 \uBAA8\uB4DC)\n *\n * \uB85C\uCEEC \uCE74\uBA54\uB77C/\uB9C8\uC774\uD06C \u2192 \uC11C\uBC84 \uC804\uC1A1.\n * 2\uAC00\uC9C0 \uBC29\uC2DD \uC9C0\uC6D0:\n * A. WebSocket \uBC29\uC2DD (videochat \uC591\uBC29\uD5A5)\n * B. WHIP \uBC29\uC2DD (OBS/\uB2E8\uBC29\uD5A5 \uBC29\uC1A1)\n *\n * @example WebSocket\n * const pub = new FuzionXPublisher({\n * url: 'wss://media:50002',\n * channelId: 'my-live',\n * mode: 'videochat',\n * nickname: '\uBC1C\uD45C\uC790',\n * });\n * pub.on('ready', () => console.log('Publishing!'));\n * pub.connect();\n *\n * @example WHIP\n * const pub = new FuzionXPublisher({\n * whipUrl: 'https://media:7777/whip/my-live',\n * });\n * pub.connect();\n */\n\nimport { FuzionXSignaling } from './FuzionXSignaling.js';\nimport { DEFAULT_ICE_SERVERS, SignalType, SessionMode, MAX_SLOTS } from './constants.js';\n\nexport class FuzionXPublisher {\n /**\n * @param {Object} opts\n * @param {string} [opts.url] - WebSocket URL (WS \uBC29\uC2DD)\n * @param {string} [opts.whipUrl] - WHIP URL (WHIP \uBC29\uC2DD)\n * @param {string} [opts.channelId] - \uCC44\uB110 ID (WS \uBC29\uC2DD \uD544\uC218)\n * @param {string} [opts.mode='broadcast'] - 'broadcast' | 'videochat'\n * @param {string} [opts.nickname] - \uB2C9\uB124\uC784\n * @param {string} [opts.token] - \uC778\uC99D \uD1A0\uD070\n * @param {string} [opts.peerId] - \uD53C\uC5B4 ID\n * @param {MediaStreamConstraints} [opts.media] - getUserMedia \uC81C\uC57D\n * @param {MediaStream} [opts.stream] - \uC774\uBBF8 \uD68D\uB4DD\uD55C MediaStream\n * @param {RTCConfiguration} [opts.rtcConfig] - WebRTC \uC124\uC815\n * @param {boolean} [opts.autoReconnect=true]\n */\n constructor(opts) {\n // WHIP or WebSocket\n this.whipUrl = opts.whipUrl || null;\n this.url = opts.url || null;\n this.hubUrl = opts.hubUrl || null;\n this.channelId = opts.channelId || null;\n this.mode = opts.mode || SessionMode.BROADCAST;\n this.nickname = opts.nickname || null;\n this.token = opts.token || null;\n this.peerId = opts.peerId || `pub-${Math.random().toString(36).slice(2, 10)}`;\n this.autoReconnect = opts.autoReconnect !== false;\n\n this.mediaConstraints = opts.media || { video: true, audio: true };\n this._externalStream = opts.stream || null;\n\n this.rtcConfig = opts.rtcConfig || {\n iceServers: DEFAULT_ICE_SERVERS,\n bundlePolicy: 'max-bundle',\n rtcpMuxPolicy: 'require',\n };\n\n /** @private */\n this._signaling = null;\n this._pc = null;\n this._localStream = null;\n this._listeners = {};\n this._candidateQueue = [];\n this._whipResourceUrl = null;\n this._maxSlots = MAX_SLOTS[this.mode] || 1;\n this._slots = new Map();\n this._connected = false;\n }\n\n // \u2500\u2500 Event System \u2500\u2500\n\n on(event, handler) {\n if (!this._listeners[event]) this._listeners[event] = [];\n this._listeners[event].push(handler);\n return this;\n }\n\n /** @private */\n _emit(event, ...args) {\n (this._listeners[event] || []).forEach((fn) => fn(...args));\n }\n\n // \u2500\u2500 Lifecycle \u2500\u2500\n\n /** \uC5F0\uACB0 \uC2DC\uC791 (\uBBF8\uB514\uC5B4 \uD68D\uB4DD \u2192 WebSocket \uB610\uB294 WHIP). */\n async connect() {\n try {\n // Hub \uB77C\uC6B0\uD305: hubUrl\uC774 \uC788\uC73C\uBA74 Hub\uC5D0\uC11C \uCC44\uB110 \uC0DD\uC131 (Origin \uD560\uB2F9)\n if (!this.url && !this.whipUrl && this.hubUrl && this.channelId) {\n // Publisher\uB294 POST \uC6B0\uC120: \uCC44\uB110 \uC0DD\uC131 \u2192 Origin ws_url \uBC18\uD658\n // GET\uC740 Viewer\uC6A9 (Edge \uBD84\uC0B0 \uD2B8\uB9AC\uAC70) \u2192 Publisher\uAC00 \uD638\uCD9C\uD558\uBA74 \uC548 \uB428\n const createRes = await fetch(`${this.hubUrl}/api/channels`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n channel_id: this.channelId,\n source_type: 'webrtc',\n }),\n });\n if (createRes.status === 409) {\n throw new Error(`Channel already exists: ${this.channelId}`);\n }\n if (!createRes.ok) throw new Error(`Failed to create channel: ${this.channelId}`);\n const data = await createRes.json();\n if (data.ws_url) {\n const isSecure = this.hubUrl.startsWith('https');\n this.url = data.ws_url.replace(/^ws(s?):/, isSecure ? 'wss:' : 'ws:');\n } else {\n const isSecure = this.hubUrl.startsWith('https');\n const wsProto = isSecure ? 'wss' : 'ws';\n this.url = `${wsProto}://${data.media_ip}:${data.webrtc_port}`;\n }\n }\n\n await this._acquireMedia();\n\n if (this.whipUrl) {\n await this._connectWhip();\n } else if (this.url) {\n this._connectWebSocket();\n } else {\n throw new Error('url, hubUrl, \uB610\uB294 whipUrl \uC911 \uD558\uB098\uB97C \uC9C0\uC815\uD574\uC57C \uD569\uB2C8\uB2E4.');\n }\n } catch (e) {\n this._emit('error', e);\n }\n }\n\n /** \uC5F0\uACB0 \uC885\uB8CC. */\n async disconnect() {\n if (this.whipUrl && this._whipResourceUrl) {\n // WHIP DELETE\n try {\n const baseUrl = new URL(this.whipUrl).origin;\n await fetch(`${baseUrl}${this._whipResourceUrl}`, { method: 'DELETE' });\n } catch (e) {\n console.warn('[FuzionX] WHIP DELETE error:', e);\n }\n this._whipResourceUrl = null;\n }\n\n if (this._signaling) {\n this._signaling.sendLeave();\n this._signaling.disconnect();\n }\n\n this._closePeerConnection();\n this._stopMedia();\n this._connected = false;\n }\n\n /** \uCC44\uD305 \uC804\uC1A1. */\n chat(text) {\n if (this._signaling) {\n this._signaling.sendChat(text, this.nickname);\n }\n }\n\n /** @returns {MediaStream|null} \uB85C\uCEEC \uC2A4\uD2B8\uB9BC */\n get localStream() {\n return this._localStream;\n }\n\n /** @returns {Map} \uC2AC\uB86F \uC815\uBCF4 (videochat: \uB2E4\uB978 \uCC38\uAC00\uC790) */\n get slots() {\n return this._slots;\n }\n\n // \u2500\u2500 Media \u2500\u2500\n\n /** @private \uBBF8\uB514\uC5B4 \uD68D\uB4DD */\n async _acquireMedia() {\n if (this._externalStream) {\n this._localStream = this._externalStream;\n } else {\n this._localStream = await navigator.mediaDevices.getUserMedia(this.mediaConstraints);\n }\n this._emit('media', this._localStream);\n }\n\n /** @private */\n _stopMedia() {\n if (this._localStream && !this._externalStream) {\n this._localStream.getTracks().forEach((t) => t.stop());\n }\n this._localStream = null;\n }\n\n // \u2500\u2500 WHIP \u2500\u2500\n\n /** @private WHIP \uC5F0\uACB0 */\n async _connectWhip() {\n this._pc = new RTCPeerConnection(this.rtcConfig);\n\n // \uD2B8\uB799 \uCD94\uAC00\n this._localStream.getTracks().forEach((track) => {\n this._pc.addTrack(track, this._localStream);\n });\n\n // ICE Gathering \uC644\uB8CC \uB300\uAE30\n const offer = await this._pc.createOffer();\n offer.sdp = FuzionXPublisher._forceCodecs(offer.sdp);\n await this._pc.setLocalDescription(offer);\n\n // Gathering \uC644\uB8CC \uB300\uAE30\n await new Promise((resolve) => {\n if (this._pc.iceGatheringState === 'complete') {\n resolve();\n } else {\n this._pc.onicegatheringstatechange = () => {\n if (this._pc.iceGatheringState === 'complete') resolve();\n };\n // \uC548\uC804 \uD0C0\uC784\uC544\uC6C3\n setTimeout(resolve, 150);\n }\n });\n\n const localDesc = this._pc.localDescription;\n // Hub API\uC758 whip_url\uC740 \uC774\uBBF8 ?token=xxx \uD3EC\uD568 \uAC00\uB2A5\n let url = this.whipUrl;\n if (this.token && !url.includes('token=')) {\n url += (url.includes('?') ? '&' : '?') + `token=${this.token}`;\n }\n\n const response = await fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/sdp' },\n body: localDesc.sdp,\n });\n\n if (response.status !== 201) {\n throw new Error(`WHIP failed: ${response.status} ${await response.text()}`);\n }\n\n const answerSdp = await response.text();\n this._whipResourceUrl = response.headers.get('location');\n\n await this._pc.setRemoteDescription(\n new RTCSessionDescription({ type: 'answer', sdp: answerSdp })\n );\n\n this._pc.onconnectionstatechange = () => {\n const state = this._pc?.connectionState;\n if (state === 'connected') {\n this._connected = true;\n this._emit('ready');\n } else if (state === 'failed' || state === 'disconnected') {\n this._emit('error', new Error(`WHIP PeerConnection ${state}`));\n }\n };\n\n this._emit('ready');\n }\n\n // \u2500\u2500 WebSocket \u2500\u2500\n\n /** @private WS \uC5F0\uACB0 */\n _connectWebSocket() {\n this._signaling = new FuzionXSignaling({\n url: this.url,\n autoReconnect: this.autoReconnect,\n onOpen: () => this._onSignalingOpen(),\n onMessage: (msg) => this._onSignalingMessage(msg),\n onClose: (evt) => {\n this._closePeerConnection();\n this._connected = false;\n this._emit('close', evt);\n },\n onError: (err) => this._emit('error', err),\n });\n this._signaling.connect();\n }\n\n /** @private */\n async _onSignalingOpen() {\n this._signaling.sendJoin(this.peerId, this.channelId, {\n nickname: this.nickname,\n token: this.token,\n mode: this.mode,\n });\n await this._createPeerConnection();\n }\n\n /** @private */\n async _createPeerConnection() {\n this._pc = new RTCPeerConnection(this.rtcConfig);\n\n // Non-Trickle: ICE candidate\uB294 SDP\uC5D0 \uD3EC\uD568\n\n // \uB85C\uCEEC \uD2B8\uB799 \uCD94\uAC00 (\uC1A1\uCD9C)\n this._localStream.getTracks().forEach((track) => {\n this._pc.addTrack(track, this._localStream);\n });\n\n // videochat \uBAA8\uB4DC: \uC218\uC2E0 \uD2B8\uB799\uB3C4 \uC900\uBE44 (\uC591\uBC29\uD5A5)\n if (this.mode === SessionMode.VIDEOCHAT) {\n for (let i = 0; i < this._maxSlots; i++) {\n this._pc.addTransceiver('video', { direction: 'recvonly' });\n this._pc.addTransceiver('audio', { direction: 'recvonly' });\n }\n\n // \uC1A1\uCD9C \uD2B8\uB799\uC740 addTrack\uC73C\uB85C \uCD94\uAC00\uB428 \u2192 transceiver\uBC29\uD5A5\uC744 sendrecv\uB85C \uC5C5\uADF8\uB808\uC774\uB4DC\n const transceivers = this._pc.getTransceivers();\n transceivers.forEach((t) => {\n if (t.sender.track && t.direction === 'recvonly') {\n t.direction = 'sendrecv';\n }\n });\n\n let videoTrackCount = 0;\n this._pc.ontrack = (event) => {\n const track = event.track;\n if (track.kind === 'video') {\n const slotIndex = videoTrackCount++;\n let stream = event.streams[0];\n if (!stream) {\n stream = new MediaStream();\n stream.addTrack(track);\n }\n const slot = this._slots.get(slotIndex) || { slotIndex };\n slot.stream = stream;\n this._slots.set(slotIndex, slot);\n this._emit('stream', stream, slotIndex);\n }\n };\n }\n\n this._pc.onconnectionstatechange = () => {\n const state = this._pc?.connectionState;\n if (state === 'connected' && !this._connected) {\n this._connected = true;\n this._emit('ready');\n } else if (state === 'failed' || state === 'disconnected') {\n this._emit('error', new Error(`PeerConnection ${state}`));\n }\n };\n\n // Offer + H264/Opus SDP \uAC15\uC81C\n const offer = await this._pc.createOffer();\n offer.sdp = FuzionXPublisher._forceCodecs(offer.sdp);\n await this._pc.setLocalDescription(offer);\n\n // Non-Trickle: ICE gathering \uC644\uB8CC \uB300\uAE30\n await this._waitForIceGathering();\n\n const finalSdp = this._pc.localDescription?.sdp;\n if (finalSdp) {\n this._signaling.sendOffer(finalSdp);\n }\n }\n\n /** @private */\n _waitForIceGathering() {\n return new Promise((resolve) => {\n if (this._pc.iceGatheringState === 'complete') {\n return resolve();\n }\n const check = () => {\n if (this._pc?.iceGatheringState === 'complete') {\n this._pc.removeEventListener('icegatheringstatechange', check);\n resolve();\n }\n };\n this._pc.addEventListener('icegatheringstatechange', check);\n setTimeout(() => {\n if (this._pc) {\n this._pc.removeEventListener('icegatheringstatechange', check);\n }\n resolve();\n }, 150);\n });\n }\n\n /** @private */\n static _forceCodecs(sdp) {\n let lines = sdp.split('\\r\\n');\n const videoIdx = lines.findIndex((l) => l.startsWith('m=video'));\n if (videoIdx !== -1) {\n const h264Pts = [];\n const h264Scores = new Map();\n lines.forEach((l) => {\n const m = l.match(/a=rtpmap:(\\d+) H264\\/90000/);\n if (m) { h264Pts.push(m[1]); h264Scores.set(m[1], 0); }\n });\n lines.forEach((l) => {\n if (l.startsWith('a=fmtp:')) {\n const pt = l.split(' ')[0].split(':')[1];\n if (h264Scores.has(pt)) {\n if (l.includes('profile-level-id=42e01f')) h264Scores.set(pt, 100);\n else if (l.includes('profile-level-id=42001f')) h264Scores.set(pt, 80);\n if (l.includes('packetization-mode=1')) h264Scores.set(pt, (h264Scores.get(pt) || 0) + 10);\n }\n }\n });\n if (h264Pts.length > 0) {\n h264Pts.sort((a, b) => h264Scores.get(b) - h264Scores.get(a));\n const parts = lines[videoIdx].split(' ');\n const otherPts = parts.slice(3).filter((pt) => !h264Pts.includes(pt));\n lines[videoIdx] = [...parts.slice(0, 3), ...h264Pts, ...otherPts].join(' ');\n }\n }\n const audioIdx = lines.findIndex((l) => l.startsWith('m=audio'));\n if (audioIdx !== -1) {\n const opusPts = [];\n lines.forEach((l) => {\n const m = l.match(/a=rtpmap:(\\d+) opus\\/48000/);\n if (m) opusPts.push(m[1]);\n });\n if (opusPts.length > 0) {\n const parts = lines[audioIdx].split(' ');\n const otherPts = parts.slice(3).filter((pt) => !opusPts.includes(pt));\n lines[audioIdx] = [...parts.slice(0, 3), ...opusPts, ...otherPts].join(' ');\n }\n }\n return lines.join('\\r\\n');\n }\n\n /** @private */\n _onSignalingMessage(msg) {\n switch (msg.type) {\n case SignalType.ANSWER:\n this._handleAnswer(msg);\n break;\n case SignalType.CANDIDATE:\n this._handleCandidate(msg);\n break;\n case SignalType.SLOT_INFO:\n this._handleSlotInfo(msg);\n break;\n case SignalType.CHAT:\n this._emit('chat', {\n peerId: msg.peer_id,\n nickname: msg.nickname,\n text: msg.text,\n });\n break;\n case SignalType.ERROR:\n this._emit('error', new Error(msg.message));\n break;\n }\n }\n\n /** @private */\n async _handleAnswer(msg) {\n if (!this._pc) return;\n try {\n await this._pc.setRemoteDescription(\n new RTCSessionDescription({ type: 'answer', sdp: msg.sdp })\n );\n for (const c of this._candidateQueue) {\n await this._pc.addIceCandidate(c);\n }\n this._candidateQueue = [];\n } catch (e) {\n this._emit('error', e);\n }\n }\n\n /** @private */\n async _handleCandidate(msg) {\n const candidate = new RTCIceCandidate({\n candidate: msg.candidate,\n sdpMid: msg.sdp_mid,\n sdpMLineIndex: msg.sdp_m_line_index,\n });\n if (this._pc && this._pc.remoteDescription) {\n try { await this._pc.addIceCandidate(candidate); } catch (e) { /* ignore */ }\n } else {\n this._candidateQueue.push(candidate);\n }\n }\n\n /** @private */\n _handleSlotInfo(msg) {\n const slotIndex = parseInt(msg.stream_id.replace('stream_', ''), 10);\n if (!msg.nickname || msg.nickname === '') {\n this._slots.delete(slotIndex);\n this._emit('slot_remove', { slotIndex, senderId: msg.sender_id });\n return;\n }\n const existing = this._slots.get(slotIndex) || {};\n this._slots.set(slotIndex, {\n ...existing,\n slotIndex,\n streamId: msg.stream_id,\n nickname: msg.nickname,\n senderId: msg.sender_id,\n });\n this._emit('slot', this._slots.get(slotIndex));\n }\n\n /** @private */\n _closePeerConnection() {\n if (this._pc) {\n this._pc.close();\n this._pc = null;\n }\n this._candidateQueue = [];\n }\n}\n"],
5
+ "mappings": "obAAA,IAAAA,EAAA,GAAAC,EAAAD,EAAA,WAAAE,EAAA,wBAAAC,EAAA,qBAAAC,EAAA,qBAAAC,EAAA,kBAAAC,EAAA,cAAAC,EAAA,cAAAC,EAAA,gBAAAC,EAAA,eAAAC,ICKO,IAAMC,EAAsB,CACjC,CAAE,KAAM,8BAA+B,EACvC,CAAE,KAAM,+BAAgC,CAC1C,EAGaC,EAAa,CACxB,KAAM,OACN,MAAO,QACP,OAAQ,SACR,UAAW,YACX,IAAK,MACL,MAAO,QACP,UAAW,YACX,KAAM,OACN,MAAO,OACT,EAGaC,EAAc,CACzB,UAAW,YACX,UAAW,WACb,EAGaC,EAAY,CACvB,CAACD,EAAY,SAAS,EAAG,EACzB,CAACA,EAAY,SAAS,EAAG,CAC3B,EAGaE,EAAY,CACvB,YAAa,EACb,cAAe,IACf,aAAc,GAChB,EAGaC,EAAQ,CACnB,WAAY,aACZ,WAAY,aACZ,YAAa,IACb,YAAa,IACf,ECvCO,IAAMC,EAAN,KAAuB,CAU5B,YAAYC,EAAM,CAChB,KAAK,IAAMA,EAAK,IAChB,KAAK,UAAYA,EAAK,YAAc,IAAM,CAAC,GAC3C,KAAK,OAASA,EAAK,SAAW,IAAM,CAAC,GACrC,KAAK,QAAUA,EAAK,UAAY,IAAM,CAAC,GACvC,KAAK,QAAUA,EAAK,UAAY,IAAM,CAAC,GACvC,KAAK,cAAgBA,EAAK,gBAAkB,GAG5C,KAAK,IAAM,KACX,KAAK,YAAc,EACnB,KAAK,gBAAkB,KACvB,KAAK,kBAAoB,EAC3B,CAGA,SAAU,CACR,KAAK,kBAAoB,GACzB,KAAK,WAAW,CAClB,CAGA,YAAa,CACX,GAAI,CACF,KAAK,IAAM,IAAI,UAAU,KAAK,GAAG,CACnC,OAAS,EAAG,CACV,KAAK,QAAQ,CAAC,EACd,KAAK,mBAAmB,EACxB,MACF,CAEA,KAAK,IAAI,OAAS,IAAM,CACtB,KAAK,YAAc,EACnB,KAAK,OAAO,CACd,EAEA,KAAK,IAAI,UAAaC,GAAU,CAC9B,GAAI,CACF,IAAMC,EAAM,KAAK,MAAMD,EAAM,IAAI,EACjC,KAAK,UAAUC,CAAG,CACpB,MAAY,CACV,QAAQ,KAAK,0BAA2BD,EAAM,IAAI,CACpD,CACF,EAEA,KAAK,IAAI,QAAWA,GAAU,CAC5B,KAAK,QAAQA,CAAK,EACd,CAAC,KAAK,mBAAqB,KAAK,eAClC,KAAK,mBAAmB,CAE5B,EAEA,KAAK,IAAI,QAAWA,GAAU,CAC5B,KAAK,QAAQA,CAAK,CACpB,CACF,CAGA,KAAKC,EAAK,CACR,OAAI,KAAK,KAAO,KAAK,IAAI,aAAe,UAAU,MAChD,KAAK,IAAI,KAAK,KAAK,UAAUA,CAAG,CAAC,EAC1B,IAEF,EACT,CAKA,SAASC,EAAQC,EAAWJ,EAAO,CAAC,EAAG,CACrC,OAAO,KAAK,KAAK,CACf,KAAMK,EAAW,KACjB,QAASF,EACT,WAAYC,EACZ,SAAUJ,EAAK,UAAY,KAC3B,MAAOA,EAAK,OAAS,KACrB,KAAMA,EAAK,MAAQ,IACrB,CAAC,CACH,CAGA,UAAUM,EAAK,CACb,OAAO,KAAK,KAAK,CAAE,KAAMD,EAAW,MAAO,IAAAC,CAAI,CAAC,CAClD,CAGA,WAAWA,EAAK,CACd,OAAO,KAAK,KAAK,CAAE,KAAMD,EAAW,OAAQ,IAAAC,CAAI,CAAC,CACnD,CAGA,cAAcC,EAAW,CACvB,OAAO,KAAK,KAAK,CACf,KAAMF,EAAW,UACjB,UAAWE,EAAU,UACrB,QAASA,EAAU,OACnB,iBAAkBA,EAAU,aAC9B,CAAC,CACH,CAGA,SAASC,EAAMC,EAAU,CACvB,OAAO,KAAK,KAAK,CACf,KAAMJ,EAAW,KACjB,KAAAG,EACA,SAAUC,GAAY,KACtB,QAAS,IACX,CAAC,CACH,CAGA,SAAU,CACR,OAAO,KAAK,KAAK,CAAE,KAAMJ,EAAW,GAAI,CAAC,CAC3C,CAGA,WAAY,CACV,OAAO,KAAK,KAAK,CAAE,KAAMA,EAAW,KAAM,CAAC,CAC7C,CAGA,YAAa,CACX,KAAK,kBAAoB,GACzB,aAAa,KAAK,eAAe,EAC7B,KAAK,MACP,KAAK,IAAI,MAAM,EACf,KAAK,IAAM,KAEf,CAGA,IAAI,WAAY,CACd,OAAO,KAAK,KAAO,KAAK,IAAI,aAAe,UAAU,IACvD,CAGA,oBAAqB,CACnB,GAAI,KAAK,aAAeK,EAAU,YAAa,CAC7C,QAAQ,MAAM,0CAA0C,EACxD,KAAK,QAAQ,IAAI,MAAM,uBAAuB,CAAC,EAC/C,MACF,CACA,IAAMC,EAAQ,KAAK,IACjBD,EAAU,cAAgB,KAAK,IAAI,EAAG,KAAK,WAAW,EACtDA,EAAU,YACZ,EACA,KAAK,cACL,QAAQ,IAAI,6BAA6BC,CAAK,OAAO,KAAK,WAAW,IAAID,EAAU,WAAW,GAAG,EACjG,KAAK,gBAAkB,WAAW,IAAM,KAAK,WAAW,EAAGC,CAAK,CAClE,CACF,ECpJO,IAAMC,EAAN,MAAMC,CAAc,CAazB,YAAYC,EAAM,CAChB,KAAK,IAAMA,EAAK,KAAO,KACvB,KAAK,OAASA,EAAK,QAAU,KAC7B,KAAK,UAAYA,EAAK,UACtB,KAAK,KAAOA,EAAK,MAAQC,EAAY,UACrC,KAAK,SAAWD,EAAK,UAAY,KACjC,KAAK,MAAQA,EAAK,OAAS,KAC3B,KAAK,OAASA,EAAK,QAAU,UAAU,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,MAAM,EAAG,EAAE,CAAC,GAC9E,KAAK,cAAgBA,EAAK,gBAAkB,GAE5C,KAAK,UAAYA,EAAK,WAAa,CACjC,WAAYE,EACZ,aAAc,aACd,cAAe,SACjB,EAGA,KAAK,WAAa,KAClB,KAAK,IAAM,KACX,KAAK,WAAa,CAAC,EACnB,KAAK,OAAS,IAAI,IAClB,KAAK,UAAYC,EAAU,KAAK,IAAI,GAAK,EACzC,KAAK,gBAAkB,CAAC,EACxB,KAAK,WAAa,EACpB,CASA,GAAGC,EAAOC,EAAS,CACjB,OAAK,KAAK,WAAWD,CAAK,IAAG,KAAK,WAAWA,CAAK,EAAI,CAAC,GACvD,KAAK,WAAWA,CAAK,EAAE,KAAKC,CAAO,EAC5B,IACT,CAGA,MAAMD,KAAUE,EAAM,EACnB,KAAK,WAAWF,CAAK,GAAK,CAAC,GAAG,QAASG,GAAOA,EAAG,GAAGD,CAAI,CAAC,CAC5D,CAKA,MAAM,SAAU,CAEd,GAAI,CAAC,KAAK,KAAO,KAAK,OACpB,GAAI,CACF,IAAME,EAAM,MAAM,MAAM,GAAG,KAAK,MAAM,iBAAiB,KAAK,SAAS,EAAE,EACvE,GAAI,CAACA,EAAI,GAAI,MAAM,IAAI,MAAM,sBAAsB,KAAK,SAAS,EAAE,EACnE,IAAMC,EAAO,MAAMD,EAAI,KAAK,EAE5B,GAAIC,EAAK,OAAQ,CAEf,IAAMC,EAAW,KAAK,OAAO,WAAW,OAAO,EAC/C,KAAK,IAAMD,EAAK,OAAO,QAAQ,WAAYC,EAAW,OAAS,KAAK,CACtE,KAAO,CAEL,IAAMC,EADW,KAAK,OAAO,WAAW,OAAO,EACpB,MAAQ,KACnC,KAAK,IAAM,GAAGA,CAAO,MAAMF,EAAK,QAAQ,IAAIA,EAAK,WAAW,EAC9D,CACF,OAAS,EAAG,CACV,KAAK,MAAM,QAAS,CAAC,EACrB,MACF,CAGF,GAAI,CAAC,KAAK,IAAK,CACb,KAAK,MAAM,QAAS,IAAI,MAAM,4EAA0B,CAAC,EACzD,MACF,CAEA,KAAK,WAAa,IAAIG,EAAiB,CACrC,IAAK,KAAK,IACV,cAAe,KAAK,cACpB,OAAQ,IAAM,KAAK,iBAAiB,EACpC,UAAYC,GAAQ,KAAK,oBAAoBA,CAAG,EAChD,QAAUC,GAAQ,KAAK,kBAAkBA,CAAG,EAC5C,QAAUC,GAAQ,KAAK,MAAM,QAASA,CAAG,CAC3C,CAAC,EACD,KAAK,WAAW,QAAQ,CAC1B,CAGA,YAAa,CACP,KAAK,aACP,KAAK,WAAW,UAAU,EAC1B,KAAK,WAAW,WAAW,GAE7B,KAAK,qBAAqB,EAC1B,KAAK,OAAO,MAAM,EAClB,KAAK,WAAa,EACpB,CAGA,KAAKC,EAAM,CACL,KAAK,YACP,KAAK,WAAW,SAASA,EAAM,KAAK,QAAQ,CAEhD,CAGA,iBAAkB,CACZ,KAAK,YACP,KAAK,WAAW,QAAQ,CAE5B,CAGA,IAAI,OAAQ,CACV,OAAO,KAAK,MACd,CAKA,kBAAmB,CACjB,KAAK,WAAW,SAAS,KAAK,OAAQ,KAAK,UAAW,CACpD,SAAU,KAAK,SACf,MAAO,KAAK,MACZ,KAAM,KAAK,IACb,CAAC,EAED,KAAK,sBAAsB,EAAE,MAAO,GAAM,KAAK,MAAM,QAAS,CAAC,CAAC,CAClE,CAGA,oBAAoBH,EAAK,CACvB,OAAQA,EAAI,KAAM,CAChB,KAAKI,EAAW,OACd,KAAK,cAAcJ,CAAG,EACtB,MACF,KAAKI,EAAW,UACd,KAAK,iBAAiBJ,CAAG,EACzB,MACF,KAAKI,EAAW,UACd,KAAK,gBAAgBJ,CAAG,EACxB,MACF,KAAKI,EAAW,KACd,KAAK,MAAM,OAAQ,CACjB,OAAQJ,EAAI,QACZ,SAAUA,EAAI,SACd,KAAMA,EAAI,IACZ,CAAC,EACD,MACF,KAAKI,EAAW,MACd,KAAK,MAAM,QAAS,IAAI,MAAMJ,EAAI,OAAO,CAAC,EAC1C,KACJ,CACF,CAGA,kBAAkBC,EAAK,CACrB,KAAK,qBAAqB,EAC1B,KAAK,WAAa,GAClB,KAAK,MAAM,QAASA,CAAG,CACzB,CAGA,MAAM,cAAcD,EAAK,CACvB,GAAK,KAAK,IACV,GAAI,CACF,MAAM,KAAK,IAAI,qBACb,IAAI,sBAAsB,CAAE,KAAM,SAAU,IAAKA,EAAI,GAAI,CAAC,CAC5D,EAEA,QAAWK,KAAK,KAAK,gBACnB,MAAM,KAAK,IAAI,gBAAgBA,CAAC,EAElC,KAAK,gBAAkB,CAAC,CAC1B,OAASC,EAAG,CACV,KAAK,MAAM,QAASA,CAAC,CACvB,CACF,CAGA,MAAM,iBAAiBN,EAAK,CAC1B,IAAMO,EAAY,IAAI,gBAAgB,CACpC,UAAWP,EAAI,UACf,OAAQA,EAAI,QACZ,cAAeA,EAAI,gBACrB,CAAC,EACD,GAAI,KAAK,KAAO,KAAK,IAAI,kBACvB,GAAI,CACF,MAAM,KAAK,IAAI,gBAAgBO,CAAS,CAC1C,OAASD,EAAG,CACV,QAAQ,KAAK,iCAAkCA,CAAC,CAClD,MAEA,KAAK,gBAAgB,KAAKC,CAAS,CAEvC,CAGA,gBAAgBP,EAAK,CACnB,IAAMQ,EAAY,SAASR,EAAI,UAAU,QAAQ,UAAW,EAAE,EAAG,EAAE,EAGnE,GAAI,CAACA,EAAI,UAAYA,EAAI,WAAa,GAAI,CACxC,KAAK,OAAO,OAAOQ,CAAS,EAC5B,KAAK,MAAM,cAAe,CAAE,UAAAA,EAAW,SAAUR,EAAI,SAAU,CAAC,EAChE,MACF,CAEA,IAAMS,EAAW,KAAK,OAAO,IAAID,CAAS,GAAK,CAAC,EAChD,KAAK,OAAO,IAAIA,EAAW,CACzB,GAAGC,EACH,UAAAD,EACA,SAAUR,EAAI,UACd,SAAUA,EAAI,SACd,SAAUA,EAAI,SAChB,CAAC,EACD,KAAK,MAAM,OAAQ,KAAK,OAAO,IAAIQ,CAAS,CAAC,CAC/C,CAUA,MAAM,uBAAwB,CAC5B,KAAK,IAAM,IAAI,kBAAkB,KAAK,SAAS,EAM/C,IAAIE,EAAkB,EACtB,KAAK,IAAI,QAAWnB,GAAU,CAC5B,IAAMoB,EAAQpB,EAAM,MAEpB,GAAIoB,EAAM,OAAS,QAAS,CAC1B,IAAMH,EAAYE,IAEdE,EAASrB,EAAM,QAAQ,CAAC,EACvBqB,IACHA,EAAS,IAAI,YACbA,EAAO,SAASD,CAAK,GAGvB,IAAME,EAAO,KAAK,OAAO,IAAIL,CAAS,GAAK,CAAE,UAAAA,CAAU,EACvDK,EAAK,OAASD,EACd,KAAK,OAAO,IAAIJ,EAAWK,CAAI,EAE/B,KAAK,MAAM,SAAUD,EAAQJ,CAAS,EAEjC,KAAK,aACR,KAAK,WAAa,GAClB,KAAK,MAAM,WAAW,EAGlB,KAAK,YACP,KAAK,WAAW,QAAQ,EAG9B,CACF,EAEA,KAAK,IAAI,wBAA0B,IAAM,CACvC,IAAMM,EAAQ,KAAK,KAAK,iBACpBA,IAAU,UAAYA,IAAU,iBAClC,KAAK,MAAM,QAAS,IAAI,MAAM,kBAAkBA,CAAK,EAAE,CAAC,CAE5D,EAGA,QAASC,EAAI,EAAGA,EAAI,KAAK,UAAWA,IAClC,KAAK,IAAI,eAAe,QAAS,CAAE,UAAW,UAAW,CAAC,EAC1D,KAAK,IAAI,eAAe,QAAS,CAAE,UAAW,UAAW,CAAC,EAI5D,IAAMC,EAAQ,MAAM,KAAK,IAAI,YAAY,EACzCA,EAAM,IAAM9B,EAAc,aAAa8B,EAAM,GAAG,EAChD,MAAM,KAAK,IAAI,oBAAoBA,CAAK,EAGxC,MAAM,KAAK,qBAAqB,EAGhC,IAAMC,EAAW,KAAK,IAAI,kBAAkB,IACxCA,GACF,KAAK,WAAW,UAAUA,CAAQ,CAEtC,CAMA,sBAAuB,CACrB,OAAO,IAAI,QAASC,GAAY,CAC9B,GAAI,KAAK,IAAI,oBAAsB,WACjC,OAAOA,EAAQ,EAEjB,IAAMC,EAAQ,IAAM,CACd,KAAK,KAAK,oBAAsB,aAClC,KAAK,IAAI,oBAAoB,0BAA2BA,CAAK,EAC7DD,EAAQ,EAEZ,EACA,KAAK,IAAI,iBAAiB,0BAA2BC,CAAK,EAE1D,WAAW,IAAM,CACX,KAAK,KACP,KAAK,IAAI,oBAAoB,0BAA2BA,CAAK,EAE/DD,EAAQ,CACV,EAAG,GAAG,CACR,CAAC,CACH,CAMA,OAAO,aAAaE,EAAK,CACvB,IAAIC,EAAQD,EAAI,MAAM;AAAA,CAAM,EAGtBE,EAAWD,EAAM,UAAWE,GAAMA,EAAE,WAAW,SAAS,CAAC,EAC/D,GAAID,IAAa,GAAI,CACnB,IAAME,EAAU,CAAC,EACXC,EAAa,IAAI,IAkBvB,GAjBAJ,EAAM,QAASE,GAAM,CACnB,IAAMG,EAAIH,EAAE,MAAM,4BAA4B,EAC1CG,IACFF,EAAQ,KAAKE,EAAE,CAAC,CAAC,EACjBD,EAAW,IAAIC,EAAE,CAAC,EAAG,CAAC,EAE1B,CAAC,EACDL,EAAM,QAASE,GAAM,CACnB,GAAIA,EAAE,WAAW,SAAS,EAAG,CAC3B,IAAMI,EAAKJ,EAAE,MAAM,GAAG,EAAE,CAAC,EAAE,MAAM,GAAG,EAAE,CAAC,EACnCE,EAAW,IAAIE,CAAE,IACfJ,EAAE,SAAS,yBAAyB,EAAGE,EAAW,IAAIE,EAAI,GAAG,EACxDJ,EAAE,SAAS,yBAAyB,GAAGE,EAAW,IAAIE,EAAI,EAAE,EACjEJ,EAAE,SAAS,sBAAsB,GAAGE,EAAW,IAAIE,GAAKF,EAAW,IAAIE,CAAE,GAAK,GAAK,EAAE,EAE7F,CACF,CAAC,EACGH,EAAQ,OAAS,EAAG,CACtBA,EAAQ,KAAK,CAACI,EAAGC,IAAMJ,EAAW,IAAII,CAAC,EAAIJ,EAAW,IAAIG,CAAC,CAAC,EAC5D,IAAME,EAAQT,EAAMC,CAAQ,EAAE,MAAM,GAAG,EACjCS,EAAWD,EAAM,MAAM,CAAC,EAAE,OAAQH,GAAO,CAACH,EAAQ,SAASG,CAAE,CAAC,EACpEN,EAAMC,CAAQ,EAAI,CAAC,GAAGQ,EAAM,MAAM,EAAG,CAAC,EAAG,GAAGN,EAAS,GAAGO,CAAQ,EAAE,KAAK,GAAG,CAC5E,CACF,CAGA,IAAMC,EAAWX,EAAM,UAAWE,GAAMA,EAAE,WAAW,SAAS,CAAC,EAC/D,GAAIS,IAAa,GAAI,CACnB,IAAMC,EAAU,CAAC,EAKjB,GAJAZ,EAAM,QAASE,GAAM,CACnB,IAAMG,EAAIH,EAAE,MAAM,4BAA4B,EAC1CG,GAAGO,EAAQ,KAAKP,EAAE,CAAC,CAAC,CAC1B,CAAC,EACGO,EAAQ,OAAS,EAAG,CACtB,IAAMH,EAAQT,EAAMW,CAAQ,EAAE,MAAM,GAAG,EACjCD,EAAWD,EAAM,MAAM,CAAC,EAAE,OAAQH,GAAO,CAACM,EAAQ,SAASN,CAAE,CAAC,EACpEN,EAAMW,CAAQ,EAAI,CAAC,GAAGF,EAAM,MAAM,EAAG,CAAC,EAAG,GAAGG,EAAS,GAAGF,CAAQ,EAAE,KAAK,GAAG,CAC5E,CACF,CAEA,OAAOV,EAAM,KAAK;AAAA,CAAM,CAC1B,CAGA,sBAAuB,CACjB,KAAK,MACP,KAAK,IAAI,MAAM,EACf,KAAK,IAAM,MAEb,KAAK,gBAAkB,CAAC,CAC1B,CAEF,ECpYO,IAAMa,EAAN,MAAMC,CAAiB,CAe5B,YAAYC,EAAM,CAEhB,KAAK,QAAUA,EAAK,SAAW,KAC/B,KAAK,IAAMA,EAAK,KAAO,KACvB,KAAK,OAASA,EAAK,QAAU,KAC7B,KAAK,UAAYA,EAAK,WAAa,KACnC,KAAK,KAAOA,EAAK,MAAQC,EAAY,UACrC,KAAK,SAAWD,EAAK,UAAY,KACjC,KAAK,MAAQA,EAAK,OAAS,KAC3B,KAAK,OAASA,EAAK,QAAU,OAAO,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,MAAM,EAAG,EAAE,CAAC,GAC3E,KAAK,cAAgBA,EAAK,gBAAkB,GAE5C,KAAK,iBAAmBA,EAAK,OAAS,CAAE,MAAO,GAAM,MAAO,EAAK,EACjE,KAAK,gBAAkBA,EAAK,QAAU,KAEtC,KAAK,UAAYA,EAAK,WAAa,CACjC,WAAYE,EACZ,aAAc,aACd,cAAe,SACjB,EAGA,KAAK,WAAa,KAClB,KAAK,IAAM,KACX,KAAK,aAAe,KACpB,KAAK,WAAa,CAAC,EACnB,KAAK,gBAAkB,CAAC,EACxB,KAAK,iBAAmB,KACxB,KAAK,UAAYC,EAAU,KAAK,IAAI,GAAK,EACzC,KAAK,OAAS,IAAI,IAClB,KAAK,WAAa,EACpB,CAIA,GAAGC,EAAOC,EAAS,CACjB,OAAK,KAAK,WAAWD,CAAK,IAAG,KAAK,WAAWA,CAAK,EAAI,CAAC,GACvD,KAAK,WAAWA,CAAK,EAAE,KAAKC,CAAO,EAC5B,IACT,CAGA,MAAMD,KAAUE,EAAM,EACnB,KAAK,WAAWF,CAAK,GAAK,CAAC,GAAG,QAASG,GAAOA,EAAG,GAAGD,CAAI,CAAC,CAC5D,CAKA,MAAM,SAAU,CACd,GAAI,CAEF,GAAI,CAAC,KAAK,KAAO,CAAC,KAAK,SAAW,KAAK,QAAU,KAAK,UAAW,CAG/D,IAAME,EAAY,MAAM,MAAM,GAAG,KAAK,MAAM,gBAAiB,CAC3D,OAAQ,OACR,QAAS,CAAE,eAAgB,kBAAmB,EAC9C,KAAM,KAAK,UAAU,CACnB,WAAY,KAAK,UACjB,YAAa,QACf,CAAC,CACH,CAAC,EACD,GAAIA,EAAU,SAAW,IACvB,MAAM,IAAI,MAAM,2BAA2B,KAAK,SAAS,EAAE,EAE7D,GAAI,CAACA,EAAU,GAAI,MAAM,IAAI,MAAM,6BAA6B,KAAK,SAAS,EAAE,EAChF,IAAMC,EAAO,MAAMD,EAAU,KAAK,EAClC,GAAIC,EAAK,OAAQ,CACf,IAAMC,EAAW,KAAK,OAAO,WAAW,OAAO,EAC/C,KAAK,IAAMD,EAAK,OAAO,QAAQ,WAAYC,EAAW,OAAS,KAAK,CACtE,KAAO,CAEL,IAAMC,EADW,KAAK,OAAO,WAAW,OAAO,EACpB,MAAQ,KACnC,KAAK,IAAM,GAAGA,CAAO,MAAMF,EAAK,QAAQ,IAAIA,EAAK,WAAW,EAC9D,CACF,CAIA,GAFA,MAAM,KAAK,cAAc,EAErB,KAAK,QACP,MAAM,KAAK,aAAa,UACf,KAAK,IACd,KAAK,kBAAkB,MAEvB,OAAM,IAAI,MAAM,0GAAyC,CAE7D,OAAS,EAAG,CACV,KAAK,MAAM,QAAS,CAAC,CACvB,CACF,CAGA,MAAM,YAAa,CACjB,GAAI,KAAK,SAAW,KAAK,iBAAkB,CAEzC,GAAI,CACF,IAAMG,EAAU,IAAI,IAAI,KAAK,OAAO,EAAE,OACtC,MAAM,MAAM,GAAGA,CAAO,GAAG,KAAK,gBAAgB,GAAI,CAAE,OAAQ,QAAS,CAAC,CACxE,OAAS,EAAG,CACV,QAAQ,KAAK,+BAAgC,CAAC,CAChD,CACA,KAAK,iBAAmB,IAC1B,CAEI,KAAK,aACP,KAAK,WAAW,UAAU,EAC1B,KAAK,WAAW,WAAW,GAG7B,KAAK,qBAAqB,EAC1B,KAAK,WAAW,EAChB,KAAK,WAAa,EACpB,CAGA,KAAKC,EAAM,CACL,KAAK,YACP,KAAK,WAAW,SAASA,EAAM,KAAK,QAAQ,CAEhD,CAGA,IAAI,aAAc,CAChB,OAAO,KAAK,YACd,CAGA,IAAI,OAAQ,CACV,OAAO,KAAK,MACd,CAKA,MAAM,eAAgB,CAChB,KAAK,gBACP,KAAK,aAAe,KAAK,gBAEzB,KAAK,aAAe,MAAM,UAAU,aAAa,aAAa,KAAK,gBAAgB,EAErF,KAAK,MAAM,QAAS,KAAK,YAAY,CACvC,CAGA,YAAa,CACP,KAAK,cAAgB,CAAC,KAAK,iBAC7B,KAAK,aAAa,UAAU,EAAE,QAASC,GAAMA,EAAE,KAAK,CAAC,EAEvD,KAAK,aAAe,IACtB,CAKA,MAAM,cAAe,CACnB,KAAK,IAAM,IAAI,kBAAkB,KAAK,SAAS,EAG/C,KAAK,aAAa,UAAU,EAAE,QAASC,GAAU,CAC/C,KAAK,IAAI,SAASA,EAAO,KAAK,YAAY,CAC5C,CAAC,EAGD,IAAMC,EAAQ,MAAM,KAAK,IAAI,YAAY,EACzCA,EAAM,IAAMjB,EAAiB,aAAaiB,EAAM,GAAG,EACnD,MAAM,KAAK,IAAI,oBAAoBA,CAAK,EAGxC,MAAM,IAAI,QAASC,GAAY,CACzB,KAAK,IAAI,oBAAsB,WACjCA,EAAQ,GAER,KAAK,IAAI,0BAA4B,IAAM,CACrC,KAAK,IAAI,oBAAsB,YAAYA,EAAQ,CACzD,EAEA,WAAWA,EAAS,GAAG,EAE3B,CAAC,EAED,IAAMC,EAAY,KAAK,IAAI,iBAEvBC,EAAM,KAAK,QACX,KAAK,OAAS,CAACA,EAAI,SAAS,QAAQ,IACtCA,IAAQA,EAAI,SAAS,GAAG,EAAI,IAAM,KAAO,SAAS,KAAK,KAAK,IAG9D,IAAMC,EAAW,MAAM,MAAMD,EAAK,CAChC,OAAQ,OACR,QAAS,CAAE,eAAgB,iBAAkB,EAC7C,KAAMD,EAAU,GAClB,CAAC,EAED,GAAIE,EAAS,SAAW,IACtB,MAAM,IAAI,MAAM,gBAAgBA,EAAS,MAAM,IAAI,MAAMA,EAAS,KAAK,CAAC,EAAE,EAG5E,IAAMC,EAAY,MAAMD,EAAS,KAAK,EACtC,KAAK,iBAAmBA,EAAS,QAAQ,IAAI,UAAU,EAEvD,MAAM,KAAK,IAAI,qBACb,IAAI,sBAAsB,CAAE,KAAM,SAAU,IAAKC,CAAU,CAAC,CAC9D,EAEA,KAAK,IAAI,wBAA0B,IAAM,CACvC,IAAMC,EAAQ,KAAK,KAAK,gBACpBA,IAAU,aACZ,KAAK,WAAa,GAClB,KAAK,MAAM,OAAO,IACTA,IAAU,UAAYA,IAAU,iBACzC,KAAK,MAAM,QAAS,IAAI,MAAM,uBAAuBA,CAAK,EAAE,CAAC,CAEjE,EAEA,KAAK,MAAM,OAAO,CACpB,CAKA,mBAAoB,CAClB,KAAK,WAAa,IAAIC,EAAiB,CACrC,IAAK,KAAK,IACV,cAAe,KAAK,cACpB,OAAQ,IAAM,KAAK,iBAAiB,EACpC,UAAYC,GAAQ,KAAK,oBAAoBA,CAAG,EAChD,QAAUC,GAAQ,CAChB,KAAK,qBAAqB,EAC1B,KAAK,WAAa,GAClB,KAAK,MAAM,QAASA,CAAG,CACzB,EACA,QAAUC,GAAQ,KAAK,MAAM,QAASA,CAAG,CAC3C,CAAC,EACD,KAAK,WAAW,QAAQ,CAC1B,CAGA,MAAM,kBAAmB,CACvB,KAAK,WAAW,SAAS,KAAK,OAAQ,KAAK,UAAW,CACpD,SAAU,KAAK,SACf,MAAO,KAAK,MACZ,KAAM,KAAK,IACb,CAAC,EACD,MAAM,KAAK,sBAAsB,CACnC,CAGA,MAAM,uBAAwB,CAW5B,GAVA,KAAK,IAAM,IAAI,kBAAkB,KAAK,SAAS,EAK/C,KAAK,aAAa,UAAU,EAAE,QAASX,GAAU,CAC/C,KAAK,IAAI,SAASA,EAAO,KAAK,YAAY,CAC5C,CAAC,EAGG,KAAK,OAASd,EAAY,UAAW,CACvC,QAAS0B,EAAI,EAAGA,EAAI,KAAK,UAAWA,IAClC,KAAK,IAAI,eAAe,QAAS,CAAE,UAAW,UAAW,CAAC,EAC1D,KAAK,IAAI,eAAe,QAAS,CAAE,UAAW,UAAW,CAAC,EAIvC,KAAK,IAAI,gBAAgB,EACjC,QAASb,GAAM,CACtBA,EAAE,OAAO,OAASA,EAAE,YAAc,aACpCA,EAAE,UAAY,WAElB,CAAC,EAED,IAAIc,EAAkB,EACtB,KAAK,IAAI,QAAWxB,GAAU,CAC5B,IAAMW,EAAQX,EAAM,MACpB,GAAIW,EAAM,OAAS,QAAS,CAC1B,IAAMc,EAAYD,IACdE,EAAS1B,EAAM,QAAQ,CAAC,EACvB0B,IACHA,EAAS,IAAI,YACbA,EAAO,SAASf,CAAK,GAEvB,IAAMgB,EAAO,KAAK,OAAO,IAAIF,CAAS,GAAK,CAAE,UAAAA,CAAU,EACvDE,EAAK,OAASD,EACd,KAAK,OAAO,IAAID,EAAWE,CAAI,EAC/B,KAAK,MAAM,SAAUD,EAAQD,CAAS,CACxC,CACF,CACF,CAEA,KAAK,IAAI,wBAA0B,IAAM,CACvC,IAAMP,EAAQ,KAAK,KAAK,gBACpBA,IAAU,aAAe,CAAC,KAAK,YACjC,KAAK,WAAa,GAClB,KAAK,MAAM,OAAO,IACTA,IAAU,UAAYA,IAAU,iBACzC,KAAK,MAAM,QAAS,IAAI,MAAM,kBAAkBA,CAAK,EAAE,CAAC,CAE5D,EAGA,IAAMN,EAAQ,MAAM,KAAK,IAAI,YAAY,EACzCA,EAAM,IAAMjB,EAAiB,aAAaiB,EAAM,GAAG,EACnD,MAAM,KAAK,IAAI,oBAAoBA,CAAK,EAGxC,MAAM,KAAK,qBAAqB,EAEhC,IAAMgB,EAAW,KAAK,IAAI,kBAAkB,IACxCA,GACF,KAAK,WAAW,UAAUA,CAAQ,CAEtC,CAGA,sBAAuB,CACrB,OAAO,IAAI,QAASf,GAAY,CAC9B,GAAI,KAAK,IAAI,oBAAsB,WACjC,OAAOA,EAAQ,EAEjB,IAAMgB,EAAQ,IAAM,CACd,KAAK,KAAK,oBAAsB,aAClC,KAAK,IAAI,oBAAoB,0BAA2BA,CAAK,EAC7DhB,EAAQ,EAEZ,EACA,KAAK,IAAI,iBAAiB,0BAA2BgB,CAAK,EAC1D,WAAW,IAAM,CACX,KAAK,KACP,KAAK,IAAI,oBAAoB,0BAA2BA,CAAK,EAE/DhB,EAAQ,CACV,EAAG,GAAG,CACR,CAAC,CACH,CAGA,OAAO,aAAaiB,EAAK,CACvB,IAAIC,EAAQD,EAAI,MAAM;AAAA,CAAM,EACtBE,EAAWD,EAAM,UAAWE,GAAMA,EAAE,WAAW,SAAS,CAAC,EAC/D,GAAID,IAAa,GAAI,CACnB,IAAME,EAAU,CAAC,EACXC,EAAa,IAAI,IAevB,GAdAJ,EAAM,QAASE,GAAM,CACnB,IAAMG,EAAIH,EAAE,MAAM,4BAA4B,EAC1CG,IAAKF,EAAQ,KAAKE,EAAE,CAAC,CAAC,EAAGD,EAAW,IAAIC,EAAE,CAAC,EAAG,CAAC,EACrD,CAAC,EACDL,EAAM,QAASE,GAAM,CACnB,GAAIA,EAAE,WAAW,SAAS,EAAG,CAC3B,IAAMI,EAAKJ,EAAE,MAAM,GAAG,EAAE,CAAC,EAAE,MAAM,GAAG,EAAE,CAAC,EACnCE,EAAW,IAAIE,CAAE,IACfJ,EAAE,SAAS,yBAAyB,EAAGE,EAAW,IAAIE,EAAI,GAAG,EACxDJ,EAAE,SAAS,yBAAyB,GAAGE,EAAW,IAAIE,EAAI,EAAE,EACjEJ,EAAE,SAAS,sBAAsB,GAAGE,EAAW,IAAIE,GAAKF,EAAW,IAAIE,CAAE,GAAK,GAAK,EAAE,EAE7F,CACF,CAAC,EACGH,EAAQ,OAAS,EAAG,CACtBA,EAAQ,KAAK,CAACI,EAAGC,IAAMJ,EAAW,IAAII,CAAC,EAAIJ,EAAW,IAAIG,CAAC,CAAC,EAC5D,IAAME,EAAQT,EAAMC,CAAQ,EAAE,MAAM,GAAG,EACjCS,EAAWD,EAAM,MAAM,CAAC,EAAE,OAAQH,GAAO,CAACH,EAAQ,SAASG,CAAE,CAAC,EACpEN,EAAMC,CAAQ,EAAI,CAAC,GAAGQ,EAAM,MAAM,EAAG,CAAC,EAAG,GAAGN,EAAS,GAAGO,CAAQ,EAAE,KAAK,GAAG,CAC5E,CACF,CACA,IAAMC,EAAWX,EAAM,UAAWE,GAAMA,EAAE,WAAW,SAAS,CAAC,EAC/D,GAAIS,IAAa,GAAI,CACnB,IAAMC,EAAU,CAAC,EAKjB,GAJAZ,EAAM,QAASE,GAAM,CACnB,IAAMG,EAAIH,EAAE,MAAM,4BAA4B,EAC1CG,GAAGO,EAAQ,KAAKP,EAAE,CAAC,CAAC,CAC1B,CAAC,EACGO,EAAQ,OAAS,EAAG,CACtB,IAAMH,EAAQT,EAAMW,CAAQ,EAAE,MAAM,GAAG,EACjCD,EAAWD,EAAM,MAAM,CAAC,EAAE,OAAQH,GAAO,CAACM,EAAQ,SAASN,CAAE,CAAC,EACpEN,EAAMW,CAAQ,EAAI,CAAC,GAAGF,EAAM,MAAM,EAAG,CAAC,EAAG,GAAGG,EAAS,GAAGF,CAAQ,EAAE,KAAK,GAAG,CAC5E,CACF,CACA,OAAOV,EAAM,KAAK;AAAA,CAAM,CAC1B,CAGA,oBAAoBX,EAAK,CACvB,OAAQA,EAAI,KAAM,CAChB,KAAKwB,EAAW,OACd,KAAK,cAAcxB,CAAG,EACtB,MACF,KAAKwB,EAAW,UACd,KAAK,iBAAiBxB,CAAG,EACzB,MACF,KAAKwB,EAAW,UACd,KAAK,gBAAgBxB,CAAG,EACxB,MACF,KAAKwB,EAAW,KACd,KAAK,MAAM,OAAQ,CACjB,OAAQxB,EAAI,QACZ,SAAUA,EAAI,SACd,KAAMA,EAAI,IACZ,CAAC,EACD,MACF,KAAKwB,EAAW,MACd,KAAK,MAAM,QAAS,IAAI,MAAMxB,EAAI,OAAO,CAAC,EAC1C,KACJ,CACF,CAGA,MAAM,cAAcA,EAAK,CACvB,GAAK,KAAK,IACV,GAAI,CACF,MAAM,KAAK,IAAI,qBACb,IAAI,sBAAsB,CAAE,KAAM,SAAU,IAAKA,EAAI,GAAI,CAAC,CAC5D,EACA,QAAWyB,KAAK,KAAK,gBACnB,MAAM,KAAK,IAAI,gBAAgBA,CAAC,EAElC,KAAK,gBAAkB,CAAC,CAC1B,OAASC,EAAG,CACV,KAAK,MAAM,QAASA,CAAC,CACvB,CACF,CAGA,MAAM,iBAAiB1B,EAAK,CAC1B,IAAM2B,EAAY,IAAI,gBAAgB,CACpC,UAAW3B,EAAI,UACf,OAAQA,EAAI,QACZ,cAAeA,EAAI,gBACrB,CAAC,EACD,GAAI,KAAK,KAAO,KAAK,IAAI,kBACvB,GAAI,CAAE,MAAM,KAAK,IAAI,gBAAgB2B,CAAS,CAAG,MAAY,CAAe,MAE5E,KAAK,gBAAgB,KAAKA,CAAS,CAEvC,CAGA,gBAAgB3B,EAAK,CACnB,IAAMK,EAAY,SAASL,EAAI,UAAU,QAAQ,UAAW,EAAE,EAAG,EAAE,EACnE,GAAI,CAACA,EAAI,UAAYA,EAAI,WAAa,GAAI,CACxC,KAAK,OAAO,OAAOK,CAAS,EAC5B,KAAK,MAAM,cAAe,CAAE,UAAAA,EAAW,SAAUL,EAAI,SAAU,CAAC,EAChE,MACF,CACA,IAAM4B,EAAW,KAAK,OAAO,IAAIvB,CAAS,GAAK,CAAC,EAChD,KAAK,OAAO,IAAIA,EAAW,CACzB,GAAGuB,EACH,UAAAvB,EACA,SAAUL,EAAI,UACd,SAAUA,EAAI,SACd,SAAUA,EAAI,SAChB,CAAC,EACD,KAAK,MAAM,OAAQ,KAAK,OAAO,IAAIK,CAAS,CAAC,CAC/C,CAGA,sBAAuB,CACjB,KAAK,MACP,KAAK,IAAI,MAAM,EACf,KAAK,IAAM,MAEb,KAAK,gBAAkB,CAAC,CAC1B,CACF",
6
+ "names": ["src_exports", "__export", "CODEC", "DEFAULT_ICE_SERVERS", "FuzionXPublisher", "FuzionXSignaling", "FuzionXViewer", "MAX_SLOTS", "RECONNECT", "SessionMode", "SignalType", "DEFAULT_ICE_SERVERS", "SignalType", "SessionMode", "MAX_SLOTS", "RECONNECT", "CODEC", "FuzionXSignaling", "opts", "event", "msg", "peerId", "channelId", "SignalType", "sdp", "candidate", "text", "nickname", "RECONNECT", "delay", "FuzionXViewer", "_FuzionXViewer", "opts", "SessionMode", "DEFAULT_ICE_SERVERS", "MAX_SLOTS", "event", "handler", "args", "fn", "res", "data", "isSecure", "wsProto", "FuzionXSignaling", "msg", "evt", "err", "text", "SignalType", "c", "e", "candidate", "slotIndex", "existing", "videoTrackCount", "track", "stream", "slot", "state", "i", "offer", "finalSdp", "resolve", "check", "sdp", "lines", "videoIdx", "l", "h264Pts", "h264Scores", "m", "pt", "a", "b", "parts", "otherPts", "audioIdx", "opusPts", "FuzionXPublisher", "_FuzionXPublisher", "opts", "SessionMode", "DEFAULT_ICE_SERVERS", "MAX_SLOTS", "event", "handler", "args", "fn", "createRes", "data", "isSecure", "wsProto", "baseUrl", "text", "t", "track", "offer", "resolve", "localDesc", "url", "response", "answerSdp", "state", "FuzionXSignaling", "msg", "evt", "err", "i", "videoTrackCount", "slotIndex", "stream", "slot", "finalSdp", "check", "sdp", "lines", "videoIdx", "l", "h264Pts", "h264Scores", "m", "pt", "a", "b", "parts", "otherPts", "audioIdx", "opusPts", "SignalType", "c", "e", "candidate", "existing"]
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fuzionx/player",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "FuzionX WebRTC Player SDK — Viewer & Publisher",
5
5
  "main": "dist/fuzionx-player.umd.js",
6
6
  "module": "dist/fuzionx-player.esm.js",
@@ -21,7 +21,14 @@
21
21
  "build": "node build.js",
22
22
  "prepublishOnly": "node build.js"
23
23
  },
24
- "keywords": ["webrtc", "player", "streaming", "whip", "videochat", "fuzionx"],
24
+ "keywords": [
25
+ "webrtc",
26
+ "player",
27
+ "streaming",
28
+ "whip",
29
+ "videochat",
30
+ "fuzionx"
31
+ ],
25
32
  "author": "FuzionX",
26
33
  "license": "MIT",
27
34
  "repository": {
@@ -45,6 +45,7 @@ export class FuzionXPublisher {
45
45
  // WHIP or WebSocket
46
46
  this.whipUrl = opts.whipUrl || null;
47
47
  this.url = opts.url || null;
48
+ this.hubUrl = opts.hubUrl || null;
48
49
  this.channelId = opts.channelId || null;
49
50
  this.mode = opts.mode || SessionMode.BROADCAST;
50
51
  this.nickname = opts.nickname || null;
@@ -91,6 +92,33 @@ export class FuzionXPublisher {
91
92
  /** 연결 시작 (미디어 획득 → WebSocket 또는 WHIP). */
92
93
  async connect() {
93
94
  try {
95
+ // Hub 라우팅: hubUrl이 있으면 Hub에서 채널 생성 (Origin 할당)
96
+ if (!this.url && !this.whipUrl && this.hubUrl && this.channelId) {
97
+ // Publisher는 POST 우선: 채널 생성 → Origin ws_url 반환
98
+ // GET은 Viewer용 (Edge 분산 트리거) → Publisher가 호출하면 안 됨
99
+ const createRes = await fetch(`${this.hubUrl}/api/channels`, {
100
+ method: 'POST',
101
+ headers: { 'Content-Type': 'application/json' },
102
+ body: JSON.stringify({
103
+ channel_id: this.channelId,
104
+ source_type: 'webrtc',
105
+ }),
106
+ });
107
+ if (createRes.status === 409) {
108
+ throw new Error(`Channel already exists: ${this.channelId}`);
109
+ }
110
+ if (!createRes.ok) throw new Error(`Failed to create channel: ${this.channelId}`);
111
+ const data = await createRes.json();
112
+ if (data.ws_url) {
113
+ const isSecure = this.hubUrl.startsWith('https');
114
+ this.url = data.ws_url.replace(/^ws(s?):/, isSecure ? 'wss:' : 'ws:');
115
+ } else {
116
+ const isSecure = this.hubUrl.startsWith('https');
117
+ const wsProto = isSecure ? 'wss' : 'ws';
118
+ this.url = `${wsProto}://${data.media_ip}:${data.webrtc_port}`;
119
+ }
120
+ }
121
+
94
122
  await this._acquireMedia();
95
123
 
96
124
  if (this.whipUrl) {
@@ -98,7 +126,7 @@ export class FuzionXPublisher {
98
126
  } else if (this.url) {
99
127
  this._connectWebSocket();
100
128
  } else {
101
- throw new Error('url 또는 whipUrl 중 하나를 지정해야 합니다.');
129
+ throw new Error('url, hubUrl, 또는 whipUrl 중 하나를 지정해야 합니다.');
102
130
  }
103
131
  } catch (e) {
104
132
  this._emit('error', e);
@@ -190,7 +218,7 @@ export class FuzionXPublisher {
190
218
  if (this._pc.iceGatheringState === 'complete') resolve();
191
219
  };
192
220
  // 안전 타임아웃
193
- setTimeout(resolve, 3000);
221
+ setTimeout(resolve, 150);
194
222
  }
195
223
  });
196
224
 
@@ -346,7 +374,7 @@ export class FuzionXPublisher {
346
374
  this._pc.removeEventListener('icegatheringstatechange', check);
347
375
  }
348
376
  resolve();
349
- }, 3000);
377
+ }, 150);
350
378
  });
351
379
  }
352
380
 
@@ -22,7 +22,8 @@ import { DEFAULT_ICE_SERVERS, SignalType, SessionMode, MAX_SLOTS } from './const
22
22
  export class FuzionXViewer {
23
23
  /**
24
24
  * @param {Object} opts
25
- * @param {string} opts.url - WebSocket URL
25
+ * @param {string} [opts.url] - WebSocket URL (직접 지정)
26
+ * @param {string} [opts.hubUrl] - Hub API URL (자동 라우팅: Hub에서 미디어 서버 조회)
26
27
  * @param {string} opts.channelId - 채널 ID
27
28
  * @param {string} [opts.mode='broadcast'] - 'broadcast' | 'videochat'
28
29
  * @param {string} [opts.nickname] - 닉네임
@@ -32,7 +33,8 @@ export class FuzionXViewer {
32
33
  * @param {boolean} [opts.autoReconnect=true]
33
34
  */
34
35
  constructor(opts) {
35
- this.url = opts.url;
36
+ this.url = opts.url || null;
37
+ this.hubUrl = opts.hubUrl || null;
36
38
  this.channelId = opts.channelId;
37
39
  this.mode = opts.mode || SessionMode.BROADCAST;
38
40
  this.nickname = opts.nickname || null;
@@ -77,7 +79,34 @@ export class FuzionXViewer {
77
79
  // ── Lifecycle ──
78
80
 
79
81
  /** 서버 연결 + WebRTC 세션 시작. */
80
- connect() {
82
+ async connect() {
83
+ // Hub 라우팅: hubUrl이 있으면 Hub에서 미디어 서버 조회
84
+ if (!this.url && this.hubUrl) {
85
+ try {
86
+ const res = await fetch(`${this.hubUrl}/api/channels/${this.channelId}`);
87
+ if (!res.ok) throw new Error(`Channel not found: ${this.channelId}`);
88
+ const data = await res.json();
89
+ // ws_url 사용 또는 ip:port에서 생성
90
+ if (data.ws_url) {
91
+ // Hub와 같은 프로토콜 사용 (https→wss, http→ws)
92
+ const isSecure = this.hubUrl.startsWith('https');
93
+ this.url = data.ws_url.replace(/^ws(s?):/, isSecure ? 'wss:' : 'ws:');
94
+ } else {
95
+ const isSecure = this.hubUrl.startsWith('https');
96
+ const wsProto = isSecure ? 'wss' : 'ws';
97
+ this.url = `${wsProto}://${data.media_ip}:${data.webrtc_port}`;
98
+ }
99
+ } catch (e) {
100
+ this._emit('error', e);
101
+ return;
102
+ }
103
+ }
104
+
105
+ if (!this.url) {
106
+ this._emit('error', new Error('url 또는 hubUrl을 지정해야 합니다.'));
107
+ return;
108
+ }
109
+
81
110
  this._signaling = new FuzionXSignaling({
82
111
  url: this.url,
83
112
  autoReconnect: this.autoReconnect,
@@ -258,6 +287,11 @@ export class FuzionXViewer {
258
287
  if (!this._connected) {
259
288
  this._connected = true;
260
289
  this._emit('connected');
290
+
291
+ // 즉시 키프레임 요청 (첫 프레임 빠르게)
292
+ if (this._signaling) {
293
+ this._signaling.sendPLI();
294
+ }
261
295
  }
262
296
  }
263
297
  };
@@ -306,13 +340,13 @@ export class FuzionXViewer {
306
340
  }
307
341
  };
308
342
  this._pc.addEventListener('icegatheringstatechange', check);
309
- // Timeout 3s (Safari 호환)
343
+ // 500ms gathering 안 끝나면 현재 SDP로 진행
310
344
  setTimeout(() => {
311
345
  if (this._pc) {
312
346
  this._pc.removeEventListener('icegatheringstatechange', check);
313
347
  }
314
348
  resolve();
315
- }, 3000);
349
+ }, 150);
316
350
  });
317
351
  }
318
352