create-fuzionx 0.1.54 → 0.1.55
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.
- package/package.json +1 -1
- package/templates/common/package.json.tpl +2 -2
- package/templates/spa/meta.json +1 -1
- package/templates/spa/views/default/spa/package.json +1 -1
- package/templates/ssr/public/js/fx-player.umd.js +7 -0
- package/templates/ssr/views/default/pages/live/room.html +1 -1
- package/templates/ssr/views/default/pages/live/watch.html +1 -1
package/package.json
CHANGED
package/templates/spa/meta.json
CHANGED
|
@@ -0,0 +1,7 @@
|
|
|
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:()=>c});var u=[{urls:"stun:stun.l.google.com:19302"},{urls:"stun:stun1.l.google.com:19302"}],c={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:c.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:c.OFFER,sdp:e})}sendAnswer(e){return this.send({type:c.ANSWER,sdp:e})}sendCandidate(e){return this.send({type:c.CANDIDATE,candidate:e.candidate,sdp_mid:e.sdpMid,sdp_m_line_index:e.sdpMLineIndex})}sendChat(e,t){return this.send({type:c.CHAT,text:e,nickname:t||null,peer_id:null})}sendPLI(){return this.send({type:c.PLI})}sendLeave(){return this.send({type:c.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(),this._beforeUnloadHandler=()=>{this._signaling&&this._signaling.connected&&this._signaling.sendLeave(),this._closePeerConnection()},window.addEventListener("beforeunload",this._beforeUnloadHandler)}async disconnect(){this._beforeUnloadHandler&&(window.removeEventListener("beforeunload",this._beforeUnloadHandler),this._beforeUnloadHandler=null),this._signaling&&(this._signaling.sendLeave(),await new Promise(e=>setTimeout(e,100)),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 c.ANSWER:this._handleAnswer(e);break;case c.CANDIDATE:this._handleCandidate(e);break;case c.SLOT_INFO:this._handleSlotInfo(e);break;case c.CHAT:this._emit("chat",{peerId:e.peer_id,nickname:e.nickname,text:e.text});break;case c.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===""){let o=this._slots.get(t);o&&this._slots.set(t,{slotIndex:t,stream:o.stream}),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 a=this._slots.get(s)||{slotIndex:s};a.stream=r,this._slots.set(s,a),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 a=r.match(/a=rtpmap:(\d+) H264\/90000/);a&&(n.push(a[1]),s.set(a[1],0))}),t.forEach(r=>{if(r.startsWith("a=fmtp:")){let a=r.split(" ")[0].split(":")[1];s.has(a)&&(r.includes("profile-level-id=42e01f")?s.set(a,100):r.includes("profile-level-id=42001f")&&s.set(a,80),r.includes("packetization-mode=1")&&s.set(a,(s.get(a)||0)+10))}}),n.length>0){n.sort((l,w)=>s.get(w)-s.get(l));let r=t[i].split(" "),a=r.slice(3).filter(l=>!n.includes(l));t[i]=[...r.slice(0,3),...n,...a].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(a=>!n.includes(a));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"})}),t;if(e.status===409){let i=await fetch(`${this.hubUrl}/api/channels/${this.channelId}`);if(!i.ok)throw new Error(`Channel not found: ${this.channelId}`);t=await i.json()}else if(e.ok)t=await e.json();else throw new Error(`Failed to create channel: ${this.channelId}`);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.");this._beforeUnloadHandler=()=>{this._signaling&&this._signaling.connected&&this._signaling.sendLeave(),this._closePeerConnection(),this._stopMedia()},window.addEventListener("beforeunload",this._beforeUnloadHandler)}catch(e){this._emit("error",e)}}async disconnect(){if(this._beforeUnloadHandler&&(window.removeEventListener("beforeunload",this._beforeUnloadHandler),this._beforeUnloadHandler=null),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(),await new Promise(e=>setTimeout(e,100)),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++,a=n.streams[0];a||(a=new MediaStream,a.addTrack(s));let l=this._slots.get(r)||{slotIndex:r};l.stream=a,this._slots.set(r,l),this._emit("stream",a,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 a=r.match(/a=rtpmap:(\d+) H264\/90000/);a&&(n.push(a[1]),s.set(a[1],0))}),t.forEach(r=>{if(r.startsWith("a=fmtp:")){let a=r.split(" ")[0].split(":")[1];s.has(a)&&(r.includes("profile-level-id=42e01f")?s.set(a,100):r.includes("profile-level-id=42001f")&&s.set(a,80),r.includes("packetization-mode=1")&&s.set(a,(s.get(a)||0)+10))}}),n.length>0){n.sort((l,w)=>s.get(w)-s.get(l));let r=t[i].split(" "),a=r.slice(3).filter(l=>!n.includes(l));t[i]=[...r.slice(0,3),...n,...a].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(a=>!n.includes(a));t[o]=[...s.slice(0,3),...n,...r].join(" ")}}return t.join(`\r
|
|
5
|
+
`)}_onSignalingMessage(e){switch(e.type){case c.ANSWER:this._handleAnswer(e);break;case c.CANDIDATE:this._handleCandidate(e);break;case c.SLOT_INFO:this._handleSlotInfo(e);break;case c.CHAT:this._emit("chat",{peerId:e.peer_id,nickname:e.nickname,text:e.text});break;case c.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){if(e.sender_id===this.peerId)return;let t=parseInt(e.stream_id.replace("stream_",""),10);if(!e.nickname||e.nickname===""){let o=this._slots.get(t);o&&this._slots.set(t,{slotIndex:t,stream:o.stream}),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
|
+
if(typeof module!=="undefined")module.exports=FuzionXPlayer;
|
|
7
|
+
//# sourceMappingURL=fx-player.umd.js.map
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
{% block description %}FuzionX WebRTC video chat room. Up to 9 participants.{% endblock %}
|
|
4
4
|
|
|
5
5
|
{% block head %}
|
|
6
|
-
<script src="/public/js/
|
|
6
|
+
<script src="/public/js/fx-player.umd.js"></script>
|
|
7
7
|
<style>
|
|
8
8
|
.live-role-selector{display:flex;gap:1rem;}
|
|
9
9
|
.live-role-btn{flex:1;display:flex;flex-direction:column;align-items:center;gap:.4rem;padding:1.25rem;border:2px solid var(--border-color,rgba(255,255,255,.08));border-radius:12px;background:transparent;cursor:pointer;transition:all .2s;}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
{% block description %}Watch live broadcast on FuzionX WebRTC.{% endblock %}
|
|
4
4
|
|
|
5
5
|
{% block head %}
|
|
6
|
-
<script src="/public/js/
|
|
6
|
+
<script src="/public/js/fx-player.umd.js"></script>
|
|
7
7
|
<style>
|
|
8
8
|
.live-watch-layout{display:flex;height:calc(100vh - 60px);overflow:hidden;}
|
|
9
9
|
.live-watch-video{flex:1;display:flex;flex-direction:column;background:#000;}
|