@vdoninja/sdk
Version:
AI-friendly P2P communication SDK for audio, video, and data
2 lines • 66.1 kB
JavaScript
// VDO.Ninja SDK v1.3.9
!function(e){"use strict";class t extends EventTarget{static get VERSION(){return"1.3.9"}_sanitizeStreamID(e){if(!e||"string"!=typeof e)return e=this._generateStreamID(),this._log("No streamID provided, generated:",e),e;if((e=e.trim()).length<1)return e=this._generateStreamID(),this._log("Empty streamID provided, generated:",e),e;const t=e.replace(/[\W]+/g,"_");if(e!==t&&this._log("StreamID contained non-alphanumeric characters, sanitized:",t),t.length>64){const e=t.substring(0,64);return this._log("StreamID too long, truncated to 64 characters:",e),e}return t}_sanitizeRoomName(e){if(!1===e||null==e)return!1;if("string"!=typeof e&&(e=String(e)),""===(e=e.trim()))return"";const t=e.replace(/[\W]+/g,"_");if(e!==t&&this._log("Room name contained non-alphanumeric characters, sanitized:",t),t.length>30){const e=t.substring(0,30);return this._log("Room name too long, truncated to 30 characters:",e),e}return t}_sanitizeLabel(e){if(!e||"string"!=typeof e)return"";const t=document.createElement("div");t.innerText=e;let s=t.textContent||t.innerText||"";return s=s.substring(0,Math.min(s.length,100)),s.trim()}_sanitizePassword(e){if(!1===e||null===e)return e;if(void 0===e||""===e)return"";if("string"!=typeof e&&(e=String(e)),(e=e.trim()).length<1)return this._log("Empty password provided"),"";return encodeURIComponent(e)}_getEffectivePassword(){if(!1===this.password||null===this.password)return null;if(void 0===this.password||""===this.password)return"someEncryptionKey123";let e=this.password;return"string"!=typeof e&&(e=String(e)),e=e.trim(),e.length<1?"someEncryptionKey123":e}constructor(e={}){if(super(),this.version=t.VERSION,this.host=e.host||e.wss||"wss://wss.vdo.ninja",this.room=this._sanitizeRoomName(e.room||null),!1===e.password?this.password=!1:void 0===e.password||null===e.password||""===e.password?this.password=this._sanitizePassword("someEncryptionKey123"):this.password=this._sanitizePassword(e.password),this.debug=e.debug||!1,this._pendingStreamID=e.streamID||null,this._pendingLabel=e.label||null,this._pendingInfo={},e.label&&(this._pendingInfo.label=this._sanitizeLabel(e.label)),e.meta&&(this._pendingInfo.meta=this._sanitizeLabel(e.meta)),e.order&&(this._pendingInfo.order=this._sanitizeLabel(e.order)),"boolean"==typeof e.broadcast&&(this._pendingInfo.broadcast=!!e.broadcast),"boolean"==typeof e.allowdrawing&&(this._pendingInfo.allowdrawing=!!e.allowdrawing),"boolean"==typeof e.iframe&&(this._pendingInfo.iframe=!!e.iframe),"boolean"==typeof e.widget&&(this._pendingInfo.widget=!!e.widget),"boolean"==typeof e.allowmidi&&(this._pendingInfo.allowmidi=!!e.allowmidi),"boolean"==typeof e.allowresources&&(this._pendingInfo.allowresources=!!e.allowresources),"boolean"!=typeof e.allowchunked&&"number"!=typeof e.allowchunked||(this._pendingInfo.allowchunked=e.allowchunked),e.info&&"object"==typeof e.info){const t=e.info;t.label&&(this._pendingInfo.label=this._sanitizeLabel(t.label),this._pendingLabel=this._pendingInfo.label),t.meta&&(this._pendingInfo.meta=this._sanitizeLabel(t.meta)),t.order&&(this._pendingInfo.order=this._sanitizeLabel(t.order)),"boolean"==typeof t.broadcast&&(this._pendingInfo.broadcast=!!t.broadcast),"boolean"==typeof t.allowdrawing&&(this._pendingInfo.allowdrawing=!!t.allowdrawing),"boolean"==typeof t.iframe&&(this._pendingInfo.iframe=!!t.iframe),"boolean"==typeof t.widget&&(this._pendingInfo.widget=!!t.widget),"boolean"==typeof t.allowmidi&&(this._pendingInfo.allowmidi=!!t.allowmidi),"boolean"==typeof t.allowresources&&(this._pendingInfo.allowresources=!!t.allowresources),"boolean"!=typeof t.allowchunked&&"number"!=typeof t.allowchunked||(this._pendingInfo.allowchunked=t.allowchunked)}this._pendingRoomID=e.roomid||e.roomID||null,this.state={connected:!1,room:null,streamID:null,uuid:null,roomJoined:!1,publishing:!1},this.signaling=null,this.connections=new Map,this._pendingViews=new Map,this._failedViewerConnections=new Map,this._intentionalDisconnect=!1,this._passwordHash=null,this._viewHandlers=new Map,this._sessionIDs={},this._remoteSessionIDs={},this._streamToUUID={},this.messageHandlers=new Map,this.streams=new Map,this.localStream=null,this.videoElement=e.videoElement||null,this.turnServers=void 0!==e.turnServers?e.turnServers:null,this.forceTURN=e.forceTURN||!1,this.turnCacheTTL=e.turnCacheTTL||5,this.stunServers=e.stunServers||[{urls:"stun:stun.l.google.com:19302"},{urls:"stun:stun.cloudflare.com:3478"}],this.configuration=e.configuration||{iceServers:this.stunServers.slice()},this._turnPromise=null,this._turnList=null,this._reconnectAttempts=0,this._maxReconnectAttempts=e.maxReconnectAttempts||5,this._reconnectDelay=e.reconnectDelay||1e3,this._reconnectTimer=null,this._isReconnecting=!1,this._intentionalDisconnect=!1,this._viewRetryTimers=new Map,this._viewRetryInterval=9e5,this.salt=e.salt||"vdo.ninja",this._saltProvidedViaOptions=!!e.salt,this.autoPingViewer=e.autoPingViewer||!1,this.autoPingInterval=e.autoPingInterval||1e4,this._setupCryptoUtils(),this._log("SDK initialized with host:",this.host),this._addPropertyHelpers()}_addPropertyHelpers(){Object.defineProperty(this,"streamID",{get:function(){return this.state.streamID},set:function(e){console.warn(`[VDONinja SDK] Setting streamID as a property is not recommended.\nPlease use the streamID option in publish() or announce() methods:\n await vdo.publish(stream, { streamID: '${e}' })\n await vdo.announce({ streamID: '${e}' })\nThe streamID has been stored and will be used if not specified in the method call.`),this._pendingStreamID=e},configurable:!0}),Object.defineProperty(this,"roomid",{get:function(){return this.room},set:function(e){console.warn(`[VDONinja SDK] Setting roomid as a property is not recommended.\nPlease use the room option in connect() or joinRoom() methods:\n await vdo.connect({ room: '${e}' })\n await vdo.joinRoom({ room: '${e}' })\nThe room has been stored and will be used if not specified in the method call.`),this._pendingRoomID=e,this.room=this._sanitizeRoomName(e)},configurable:!0}),Object.defineProperty(this,"label",{get:function(){return this._pendingLabel},set:function(e){console.warn(`[VDONinja SDK] Setting label as a property is not recommended.\nPlease use the label option in publish() or announce() methods:\n await vdo.publish(stream, { label: '${e}' })\n await vdo.announce({ label: '${e}' })\nThe label has been stored and will be used if not specified in the method call.`),this._pendingLabel=e},configurable:!0})}async connect(e={}){if(this.connections||(this.connections=new Map),this.state||(this.state={}),this._viewHandlers||(this._viewHandlers=new Map),this._sessionIDs||(this._sessionIDs={}),this._remoteSessionIDs||(this._remoteSessionIDs={}),this._streamToUUID||(this._streamToUUID={}),this._pendingViews||(this._pendingViews=new Map),this.messageHandlers||(this.messageHandlers=new Map),this.streams||(this.streams=new Map),this._viewRetryTimers||(this._viewRetryTimers=new Map),!this.state.connected)return this._intentionalDisconnect=!1,e.host&&(this.host=e.host),e.room?this.room=this._sanitizeRoomName(e.room):this._pendingRoomID&&(this.room=this._sanitizeRoomName(this._pendingRoomID)),void 0!==e.password&&(!1===e.password?this.password=!1:null===e.password||""===e.password?this.password=this._sanitizePassword("someEncryptionKey123"):this.password=this._sanitizePassword(e.password)),new Promise((e,t)=>{try{this.signaling=new WebSocket(this.host),this.signaling.onopen=()=>{this._log("WebSocket connected"),this.state.connected=!0,this._reconnectAttempts=0,this._emit("connected"),e()},this.signaling.onmessage=async e=>{try{const t=JSON.parse(e.data);this._logMessage("IN",t,"WebSocket"),await this._handleSignalingMessage(t)}catch(e){this._log("Error parsing message:",e)}},this.signaling.onerror=e=>{this._log("WebSocket error:",e),this._emit("error",{error:"WebSocket error",details:e}),t(e)},this.signaling.onclose=()=>{this._log("WebSocket closed"),this.state.connected=!1,this.state.roomJoined=!1,this.state.publishing=!1,this._emit("disconnected"),!this._intentionalDisconnect&&this._reconnectAttempts<this._maxReconnectAttempts&&this._attemptReconnect()}}catch(e){this._log("Connection error:",e),t(e)}});this._log("Already connected")}disconnect(){this._log("Disconnecting..."),this._intentionalDisconnect=!0,this._reconnectTimer&&(clearTimeout(this._reconnectTimer),this._reconnectTimer=null),this._log("Connections count:",this.connections.size);const e=[];for(const[t,s]of this.connections)for(const i of["viewer","publisher"]){const n=s[i];if(n&&(this._log(`Connection ${t}:${i}: dataChannel=${!!n.dataChannel}, state=${n.dataChannel?.readyState}`),n.dataChannel&&"open"===n.dataChannel.readyState))try{const s={bye:!0};n.dataChannel.send(JSON.stringify(s)),this._log("Sent bye message to:",t,"type:",i);const a=new Promise(e=>{const t=()=>{n.dataChannel&&"open"===n.dataChannel.readyState&&0!==n.dataChannel.bufferedAmount?setTimeout(t,10):e()};t()}),o=new Promise(e=>setTimeout(e,100));e.push(Promise.race([a,o]))}catch(e){this._log("Error sending bye message:",e)}}Promise.all(e).then(()=>{for(const[e,t]of this.connections)for(const e of["viewer","publisher"]){const s=t[e];s&&(this._stopPingMonitoring(s),s.pc&&s.pc.close())}this.connections.clear();for(const[e,t]of this._viewRetryTimers)clearTimeout(t);this._viewRetryTimers.clear(),this.signaling&&(this.signaling.close(),this.signaling=null),this.state={connected:!1,room:null,streamID:null,uuid:null,roomJoined:!1,publishing:!1},this._emit("disconnected")})}async _attemptReconnect(){if(this._isReconnecting)return;this._isReconnecting=!0,this._reconnectAttempts++;const e=Math.min(this._reconnectDelay*Math.pow(2,this._reconnectAttempts-1),3e4);this._log(`Attempting reconnection ${this._reconnectAttempts}/${this._maxReconnectAttempts} in ${e}ms`),this._emit("reconnecting",{attempt:this._reconnectAttempts,maxAttempts:this._maxReconnectAttempts}),this._reconnectTimer=setTimeout(async()=>{try{await this.connect(),this.state.room&&await this.joinRoom({room:this.state.room,password:this.password}),this.state.publishing&&this.localStream&&await this.publish(this.localStream,{streamID:this.state.streamID}),this._emit("reconnected"),this._isReconnecting=!1}catch(e){this._log("Reconnection failed:",e),this._isReconnecting=!1,this._reconnectAttempts>=this._maxReconnectAttempts?this._emit("reconnectFailed"):this._attemptReconnect()}},e)}getStreams(){const e=[],t=Date.now();for(const[s,i]of this.streams)e.push({streamID:s,firstSeen:i.firstSeen,lastSeen:i.lastSeen,timeSinceLastSeen:t-i.lastSeen,uuid:i.uuid,state:i.state,viewRequestTime:i.viewRequestTime,waitingTime:"pending"===i.state&&i.viewRequestTime?t-i.viewRequestTime:null});return e.sort((e,t)=>t.lastSeen-e.lastSeen)}getStreamInfo(e){const t=this.streams.get(e);if(!t)return null;const s=Date.now();return{streamID:e,firstSeen:t.firstSeen,lastSeen:t.lastSeen,timeSinceLastSeen:s-t.lastSeen,uuid:t.uuid,state:t.state,viewRequestTime:t.viewRequestTime,waitingTime:"pending"===t.state&&t.viewRequestTime?s-t.viewRequestTime:null}}async joinRoom(e={}){if(!this.state.connected)throw new Error("Not connected to signaling server");if(this.state.roomJoined)return this._log("Already joined a room on this connection"),Promise.resolve();const t=this._sanitizeRoomName(e.room||this.room),s=this._sanitizePassword(void 0!==e.password?e.password:this.password);if(!t)throw new Error("Room name is required");this.password=""===s?this._sanitizePassword("someEncryptionKey123"):s;let i=t;const n=this._getEffectivePassword();null!==n&&(i=await this._hashRoom(t,n)),this._log("Joining room:",t,"with hash:",i);const a={request:"joinroom",roomid:i};return e.claim&&(a.claim=!0),this._sendMessageWS(a),new Promise((e,s)=>{const i=setTimeout(()=>{s(new Error("Room join timeout"))},1e4),n=s=>{clearTimeout(i),this.removeEventListener("_roomJoined",n),this.state.room=t,this.state.roomJoined=!0,this._emit("roomJoined",{room:t}),e()};this.addEventListener("_roomJoined",n)})}leaveRoom(){if(!this.state.room)return void this._log("Not in a room");this._sendMessageWS({leave:!0});const e=this.state.room;this.state.room=null,this.state.roomJoined=!1,this._emit("roomLeft",{room:e})}async publish(e,t={}){if(!this.state.connected)throw new Error("Not connected to signaling server");if(!(e&&e instanceof MediaStream))throw new Error("Valid MediaStream required");if(this.state.publishing)throw new Error("Already publishing on this connection. Disconnect and reconnect to publish a different stream.");this.localStream=e;const s=this._sanitizeStreamID(t.streamID||this._pendingStreamID)||this._generateStreamID();if(t.label&&(this._pendingLabel=this._sanitizeLabel(t.label)),this._pendingInfo=this._pendingInfo||{},t.label&&(this._pendingInfo.label=this._sanitizeLabel(t.label)),t.meta&&(this._pendingInfo.meta=this._sanitizeLabel(t.meta)),t.order&&(this._pendingInfo.order=this._sanitizeLabel(t.order)),"boolean"==typeof t.broadcast&&(this._pendingInfo.broadcast=!!t.broadcast),"boolean"==typeof t.allowdrawing&&(this._pendingInfo.allowdrawing=!!t.allowdrawing),"boolean"==typeof t.iframe&&(this._pendingInfo.iframe=!!t.iframe),"boolean"==typeof t.widget&&(this._pendingInfo.widget=!!t.widget),"boolean"==typeof t.allowmidi&&(this._pendingInfo.allowmidi=!!t.allowmidi),"boolean"==typeof t.allowresources&&(this._pendingInfo.allowresources=!!t.allowresources),"boolean"!=typeof t.allowchunked&&"number"!=typeof t.allowchunked||(this._pendingInfo.allowchunked=t.allowchunked),t.info&&"object"==typeof t.info){const e=t.info;e.label&&(this._pendingInfo.label=this._sanitizeLabel(e.label),this._pendingLabel=this._pendingInfo.label),e.meta&&(this._pendingInfo.meta=this._sanitizeLabel(e.meta)),e.order&&(this._pendingInfo.order=this._sanitizeLabel(e.order)),"boolean"==typeof e.broadcast&&(this._pendingInfo.broadcast=!!e.broadcast),"boolean"==typeof e.allowdrawing&&(this._pendingInfo.allowdrawing=!!e.allowdrawing),"boolean"==typeof e.iframe&&(this._pendingInfo.iframe=!!e.iframe),"boolean"==typeof e.widget&&(this._pendingInfo.widget=!!e.widget),"boolean"==typeof e.allowmidi&&(this._pendingInfo.allowmidi=!!e.allowmidi),"boolean"==typeof e.allowresources&&(this._pendingInfo.allowresources=!!e.allowresources),"boolean"!=typeof e.allowchunked&&"number"!=typeof e.allowchunked||(this._pendingInfo.allowchunked=e.allowchunked)}!this.state.roomJoined&&t.room&&await this.joinRoom({room:t.room,password:void 0!==t.password?t.password:this.password});let i=s;{const e=this._getEffectivePassword();null!==e&&(i=await this._hashStreamID(s,e))}this._log("Publishing with streamID:",s,"as:",i),this.state.streamID=s,this.state.publishing=!0;const n={request:"seed",streamID:i};if(this._log("Sending seed message for streamID:",i),this._sendMessageWS(n),this._emit("publishing",{streamID:s,hashedStreamID:i}),this.streams){const e=Date.now();this.streams.set(s,{firstSeen:e,lastSeen:e,uuid:this.state.uuid,state:"connected"})}return s}async announce(e={}){if(!this.state.connected)throw new Error("Not connected to signaling server");if(e.label&&(this._pendingLabel=this._sanitizeLabel(e.label)),this._pendingInfo=this._pendingInfo||{},e.label&&(this._pendingInfo.label=this._sanitizeLabel(e.label)),e.meta&&(this._pendingInfo.meta=this._sanitizeLabel(e.meta)),e.order&&(this._pendingInfo.order=this._sanitizeLabel(e.order)),"boolean"==typeof e.broadcast&&(this._pendingInfo.broadcast=!!e.broadcast),"boolean"==typeof e.allowdrawing&&(this._pendingInfo.allowdrawing=!!e.allowdrawing),"boolean"==typeof e.iframe&&(this._pendingInfo.iframe=!!e.iframe),"boolean"==typeof e.widget&&(this._pendingInfo.widget=!!e.widget),"boolean"==typeof e.allowmidi&&(this._pendingInfo.allowmidi=!!e.allowmidi),"boolean"==typeof e.allowresources&&(this._pendingInfo.allowresources=!!e.allowresources),"boolean"!=typeof e.allowchunked&&"number"!=typeof e.allowchunked||(this._pendingInfo.allowchunked=e.allowchunked),e.info&&"object"==typeof e.info){const t=e.info;t.label&&(this._pendingInfo.label=this._sanitizeLabel(t.label),this._pendingLabel=this._pendingInfo.label),t.meta&&(this._pendingInfo.meta=this._sanitizeLabel(t.meta)),t.order&&(this._pendingInfo.order=this._sanitizeLabel(t.order)),"boolean"==typeof t.broadcast&&(this._pendingInfo.broadcast=!!t.broadcast),"boolean"==typeof t.allowdrawing&&(this._pendingInfo.allowdrawing=!!t.allowdrawing),"boolean"==typeof t.iframe&&(this._pendingInfo.iframe=!!t.iframe),"boolean"==typeof t.widget&&(this._pendingInfo.widget=!!t.widget),"boolean"==typeof t.allowmidi&&(this._pendingInfo.allowmidi=!!t.allowmidi),"boolean"==typeof t.allowresources&&(this._pendingInfo.allowresources=!!t.allowresources),"boolean"!=typeof t.allowchunked&&"number"!=typeof t.allowchunked||(this._pendingInfo.allowchunked=t.allowchunked)}const t=this._sanitizeStreamID(e.streamID||this._pendingStreamID)||this._generateStreamID();!this.state.roomJoined&&e.room&&await this.joinRoom({room:e.room,password:void 0!==e.password?e.password:this.password});let s=t;{const e=this._getEffectivePassword();null!==e&&(s=await this._hashStreamID(t,e))}this._log("Announcing availability with streamID:",t,"as:",s),this.state.streamID=t,this.state.publishing=!0;const i={request:"seed",streamID:s};return this._sendMessageWS(i),this._emit("publishing",{streamID:t,hashedStreamID:s,dataOnly:!0}),t}stopPublishing(){if(!this.state.publishing)return void this._log("Not currently publishing");const e=[];for(const[t,s]of this.connections){const i=s.publisher;if(i&&i.dataChannel&&"open"===i.dataChannel.readyState)try{const s={bye:!0};this._sendDataInternal(s,t,null,"publisher"),this._log("Sent bye message to viewer:",t);const n=new Promise(e=>{const t=()=>{i.dataChannel&&"open"===i.dataChannel.readyState&&0!==i.dataChannel.bufferedAmount?setTimeout(t,10):e()};t()}),a=new Promise(e=>setTimeout(e,100));e.push(Promise.race([n,a]))}catch(e){this._log("Error sending bye message:",e)}}Promise.all(e).then(()=>{for(const[e,t]of this.connections){const s=t.publisher;s&&(this._stopPingMonitoring(s),s.pc&&s.pc.close(),delete t.publisher,t.viewer||t.publisher||this.connections.delete(e))}this.localStream&&(this.localStream.getTracks().forEach(e=>e.stop()),this.localStream=null),this.state.publishing=!1,this.state.streamID=null,this._emit("publishingStopped")})}async view(e,t={}){if(!this.state.connected)throw new Error("Not connected to signaling server");e=this._sanitizeStreamID(e),t.label&&(t.label=this._sanitizeLabel(t.label)),this._log("View request for:",e,"with options:",t);const s=Date.now(),i=this.streams.get(e);this.streams.set(e,{firstSeen:i?.firstSeen||s,lastSeen:s,uuid:i?.uuid||null,state:"pending",viewRequestTime:s});try{let s=e;{const t=this._getEffectivePassword();null!==t&&(s=await this._hashStreamID(e,t))}this._pendingViews.set(e,{options:t,timestamp:Date.now(),hashedStreamID:s}),this._lastViewOptions=this._lastViewOptions||{},this._lastViewOptions[e]=t;const i={request:"play",streamID:s};if(!this.signaling||this.signaling.readyState!==WebSocket.OPEN)throw new Error("WebSocket not connected");return this._logMessage("OUT",i,"WebSocket"),this.signaling.send(JSON.stringify(i)),this._log("Sent view request for:",e,"as:",s),new Promise((s,i)=>{const n=setInterval(()=>{for(const[t,i]of this.connections){const t=i.viewer;if(t&&t.streamID===e&&t.pc)return clearInterval(n),clearTimeout(a),this._pendingViews.delete(e),void s(t.pc)}},100),a=setTimeout(()=>{clearInterval(n),this._log(`Stream ${e} not available yet, will retry in 15 minutes`),this._setupViewRetry(e,t),s(null)},15e3)})}catch(t){throw this._log("Error in view:",t.message),this._pendingViews.delete(e),t}}_setupViewRetry(e,t){this._viewRetryTimers.has(e)&&clearTimeout(this._viewRetryTimers.get(e));const s=setTimeout(()=>{this._log(`Retrying view for stream: ${e}`),this._viewRetryTimers.delete(e),this.state.connected&&!this._intentionalDisconnect&&this.view(e,t).catch(e=>{this._log("Retry view error:",e.message)})},this._viewRetryInterval);this._viewRetryTimers.set(e,s)}stopViewing(e){this._intentionalDisconnect=!0,this._viewRetryTimers.has(e)&&(clearTimeout(this._viewRetryTimers.get(e)),this._viewRetryTimers.delete(e)),this._pendingViews.delete(e),this._failedViewerConnections&&this._failedViewerConnections.delete(e);const t=[],s=this._getConnections({streamID:e,type:"viewer"});for(const e of s)if(e.dataChannel&&"open"===e.dataChannel.readyState)try{const s={bye:!0};e.dataChannel.send(JSON.stringify(s)),this._log("Sent bye message to publisher:",e.uuid);const i=new Promise(t=>{const s=()=>{e.dataChannel&&"open"===e.dataChannel.readyState&&0!==e.dataChannel.bufferedAmount?setTimeout(s,10):t()};s()}),n=new Promise(e=>setTimeout(e,100));t.push(Promise.race([i,n]))}catch(e){this._log("Error sending bye message:",e)}Promise.all(t).then(()=>{for(const[t,s]of this.connections){const i=s.viewer;i&&i.streamID===e&&(i.pc&&i.pc.close(),delete s.viewer,s.viewer||s.publisher||this.connections.delete(t))}this._emit("viewingStopped",{streamID:e}),this._intentionalDisconnect=!1})}async _createConnection(e,t="viewer"){let s,i;"string"==typeof e?(s=e,this._log("Creating connection to remote peer UUID:",s,"type:",t)):(i=e||{},s=i.uuid,t=i.type||t,this._log("Creating connection with options:",i));const n=await this._getICEConfiguration(),a={uuid:s,type:t,pc:new RTCPeerConnection(n),dataChannel:null,streamID:null,session:null,info:{label:i?.label||null},allowAudio:!0,allowVideo:!0,viewOptions:{},stats:{packetsReceived:0,packetsLost:0,bytesReceived:0,lastStatsTime:Date.now()},lastMessageTime:Date.now(),pingTimer:null,missedPings:0,pendingPing:null};if(i&&(i.streamID&&(a.streamID=i.streamID),i.viewPreferences&&(a.viewPreferences=i.viewPreferences)),a.iceTimer=null,a.iceBundle=[],a.iceBundleDelay=70,a.pc.onicecandidate=e=>{this._handleICECandidate(e,a)},a.pc.oniceconnectionstatechange=()=>{if(this._log(`ICE state for ${s}:`,a.pc.iceConnectionState),"connected"===a.pc.iceConnectionState){if(a.streamID&&this.streams.has(a.streamID)){const e=this.streams.get(a.streamID);e.state="connected",e.lastSeen=Date.now(),a.uuid&&(e.uuid=a.uuid)}this._emit("peerConnected",{uuid:s,connection:a})}else if("failed"===a.pc.iceConnectionState||"disconnected"===a.pc.iceConnectionState){if(a.streamID&&this.streams.has(a.streamID)){const e=this.streams.get(a.streamID);e.state="failed"===a.pc.iceConnectionState?"failed":"disconnected",e.lastSeen=Date.now()}this._handleConnectionFailed(a)}},a.pc.ontrack=e=>{this._log("Track received:",e.track.kind,"from:",s),this._emit("track",{track:e.track,streams:e.streams,uuid:s,streamID:a.streamID})},"publisher"===t){const e=a.pc.createDataChannel("sendChannel",{ordered:!0});a.dataChannel=e,this._setupDataChannel(a,e)}a.pc.ondatachannel=e=>{this._log("Data channel received from:",s),a.dataChannel=e.channel,this._setupDataChannel(a,e.channel)},this.connections.has(s)||this.connections.set(s,{});return this.connections.get(s)[t]=a,a}_getConnection(e,t=null){const s=this.connections.get(e);return s?t?s[t]||null:s.viewer||s.publisher||null:null}_getConnections(e={}){const t=[];for(const[s,i]of this.connections)if(!e.uuid||s===e.uuid)for(const s of["viewer","publisher"]){const n=i[s];n&&(e.type&&s!==e.type||e.streamID&&n.streamID!==e.streamID||t.push(n))}return t}_setupDataChannel(e,t){this._log(`Setting up data channel for ${e.uuid}, initial state: ${t.readyState}`),t.onopen=()=>{if(this._log(`Data channel opened for ${e.uuid}`),"viewer"===e.type&&e.viewPreferences)try{this._sendDataInternal(e.viewPreferences,e.uuid,null,"publisher"),this._log("Sent track preferences:",e.viewPreferences)}catch(e){this._log("Failed to send preferences:",e.message)}if("publisher"===e.type)try{const s=Object.assign({},this._pendingInfo||{},e.info||{});if(s.label&&(s.label=this._sanitizeLabel(s.label)),s.meta&&(s.meta=this._sanitizeLabel(s.meta)),s.order&&(s.order=this._sanitizeLabel(s.order)),Object.keys(s).length>0){const e={info:s};this._logMessage("OUT",e,"DataChannel"),t.send(JSON.stringify(e)),this._log("Sent publisher info to viewer:",s)}}catch(e){this._log("Failed to send publisher info:",e.message||e)}this._startPingMonitoring(e),this._emit("dataChannelOpen",{uuid:e.uuid,type:e.type,streamID:e.streamID})},t.onerror=t=>{this._log(`Data channel error for ${e.uuid}:`,t)},t.onclose=()=>{this._log(`Data channel closed for ${e.uuid}, was in state: ${t.readyState}`),this._stopPingMonitoring(e)},t.onmessage=async s=>{e.lastMessageTime=Date.now();try{const i=JSON.parse(s.data);if(this._logMessage("IN",i,"DataChannel"),i.description&&i.vector&&"string"==typeof i.description)try{const e=await this._decryptMessage(i.description,i.vector);i.description=JSON.parse(e),this._log("Decrypted SDP description from data channel")}catch(e){return void this._log("Failed to decrypt SDP description from data channel:",e)}if(i.candidates&&i.vector&&"string"==typeof i.candidates)try{const e=await this._decryptMessage(i.candidates,i.vector);i.candidates=JSON.parse(e),this._log("Decrypted ICE candidates from data channel")}catch(e){return void this._log("Failed to decrypt ICE candidates from data channel:",e)}if(i.candidate&&i.vector&&"string"==typeof i.candidate)try{const e=await this._decryptMessage(i.candidate,i.vector);i.candidate=JSON.parse(e),this._log("Decrypted ICE candidate from data channel")}catch(e){return void this._log("Failed to decrypt ICE candidate from data channel:",e)}if(i.description)this._log("Received SDP via data channel"),i.UUID=e.uuid,i.session=i.session||e.session,await this._handleSDP(i);else if(i.candidate)this._log("Received ICE candidate via data channel"),i.UUID=e.uuid,await this._handleRemoteICECandidate(i);else if(i.candidates)this._log("Received ICE candidates via data channel"),i.UUID=e.uuid,await this._handleRemoteICECandidates(i);else if("boolean"==typeof i.audio||"boolean"==typeof i.video)this._log("Received track preferences:",i),e.allowAudio=!1!==i.audio,e.allowVideo=!1!==i.video,this.localStream&&this._updateTracksForConnection&&await this._updateTracksForConnection(e);else if(i.info&&"object"==typeof i.info)e.info=Object.assign(e.info||{},i.info),this._emit("peerInfo",{uuid:e.uuid,streamID:e.streamID,info:e.info});else if(i.ping)try{this._sendDataInternal({pong:i.ping},e.uuid,null,"any"),this._log("Sent pong response")}catch(e){this._log("Failed to send pong:",e.message)}else if(i.pong){const t=Date.now()-i.pong;e.pendingPing===i.pong&&(e.pendingPing=null,e.missedPings=0),this._emit("peerLatency",{uuid:e.uuid,latency:t,streamID:e.streamID}),this._log(`Latency to ${"publisher"===e.type?"viewer":"publisher"} ${e.uuid}: ${t}ms`)}else if(i.bye)this._log("Received bye message via data channel"),this._handleBye({UUID:e.uuid});else if(i.pipe)this._log("Received generic data via pipe"),!i.pipe||"object"!=typeof i.pipe||"subscribe"!==i.pipe.type&&"unsubscribe"!==i.pipe.type&&"channelMessage"!==i.pipe.type&&"request"!==i.pipe.type&&"response"!==i.pipe.type?this._emit("dataReceived",{data:i.pipe,uuid:e.uuid,streamID:e.streamID}):this._handleDataChannelMessage(i.pipe,e.uuid);else if(i.iceRestartRequest)if(this._log("Received ICE restart request via data channel"),e.pc&&e.pc.restartIce)e.pc.restartIce();else{const s=await e.pc.createOffer({iceRestart:!0});await e.pc.setLocalDescription(s);e.uuid,e.session,e.streamID;if(null!==this._getEffectivePassword())try{const[i,n]=await this._encryptMessage(JSON.stringify(s)),a={UUID:e.uuid,description:i,vector:n,session:e.session};this._logMessage("OUT",a,"DataChannel"),t.send(JSON.stringify(a))}catch(e){this._log("Failed to encrypt offer for ICE restart:",e)}else{const i={UUID:e.uuid,description:s,session:e.session};this._logMessage("OUT",i,"DataChannel"),t.send(JSON.stringify(i))}}}catch(t){this._emit("data",{data:s.data,uuid:e.uuid,streamID:e.streamID})}},t.onerror=e=>{this._log("Data channel error:",e)},t.onclose=()=>{this._log("Data channel closed")}}async _updateTracksForConnection(e){if(!this.localStream||!e.pc)return;const t=e.pc.getSenders(),s=this.localStream.getAudioTracks(),i=t.find(e=>e.track&&"audio"===e.track.kind);i&&(e.allowAudio?s.length>0&&await i.replaceTrack(s[0]):await i.replaceTrack(null));const n=this.localStream.getVideoTracks(),a=t.find(e=>e.track&&"video"===e.track.kind);a&&(e.allowVideo?n.length>0&&await a.replaceTrack(n[0]):await a.replaceTrack(null))}async _createOffer(e){if(!e)throw new Error("No connection provided to _createOffer");if(!e.pc)throw this._log("ERROR: No peer connection in connection object"),new Error("No peer connection available");try{if(this.localStream&&"publisher"===e.type){const t=this.localStream.getAudioTracks(),s=this.localStream.getVideoTracks();e.allowAudio&&t.length>0&&t.forEach(t=>{e.pc.addTrack(t,this.localStream)}),e.allowVideo&&s.length>0&&s.forEach(t=>{e.pc.addTrack(t,this.localStream)})}const t=await e.pc.createOffer();return await e.pc.setLocalDescription(t),this._log("Created offer successfully"),t}catch(e){throw this._log("Error creating offer:",e.message),e}}async _createAnswer(e){if(!e.pc)throw new Error("No peer connection available");const t=await e.pc.createAnswer();return await e.pc.setLocalDescription(t),t}async _handleICECandidate(e,t){if(e.candidate){if(t.dataChannel&&"open"===t.dataChannel.readyState)try{const s={type:"viewer"===t.type?"remote":"local",UUID:t.uuid,candidates:null,session:t.session},i=[{candidate:e.candidate.candidate,sdpMLineIndex:e.candidate.sdpMLineIndex,sdpMid:e.candidate.sdpMid}];if(null!==this._getEffectivePassword())try{const[e,t]=await this._encryptMessage(JSON.stringify(i));s.candidates=e,s.vector=t}catch(e){s.candidates=i}else s.candidates=i;return this._logMessage("OUT",s,"DataChannel"),t.dataChannel.send(JSON.stringify(s)),void this._log("Sent ICE via data channel")}catch(e){this._log("Failed to send ICE via data channel:",e.message)}t.iceBundle.push(e.candidate),null===t.iceTimer&&(t.iceTimer=setTimeout(async()=>{t.iceTimer=null;const e={UUID:t.uuid,type:"viewer"===t.type?"remote":"local",candidates:t.iceBundle,session:t.session},s=t.iceBundle;if(t.iceBundle=[],t.iceBundleDelay=Math.min(1e3,2*t.iceBundleDelay),null!==this._getEffectivePassword())try{const[t,i]=await this._encryptMessage(JSON.stringify(s));e.candidates=t,e.vector=i,this._log("Encrypted ICE candidates bundle")}catch(t){this._log("Failed to encrypt ICE candidates:",t),e.candidates=s}this._sendMessageWS(e),this._log(`Sent ICE bundle with ${s.length} candidates`)},t.iceBundleDelay))}else this._log("Empty ICE candidate, gathering complete")}_handleConnectionFailed(e){this._log("Connection failed:",e.uuid,"type:",e.type),this._stopPingMonitoring(e),e.pc&&e.pc.close(),"viewer"===e.type&&e.streamID&&!this._intentionalDisconnect&&(this._log("Viewer connection failed for stream:",e.streamID),this._failedViewerConnections||(this._failedViewerConnections=new Map),this._failedViewerConnections.set(e.streamID,{uuid:e.uuid,viewOptions:e.viewOptions||{},retryCount:0,lastRetry:Date.now()}),setTimeout(()=>{this._retryFailedViewerConnection(e.streamID)},2e3));const t=this.connections.get(e.uuid);t&&(delete t[e.type],t.viewer||t.publisher||this.connections.delete(e.uuid)),this._emit("connectionFailed",{uuid:e.uuid,type:e.type,streamID:e.streamID})}async _retryFailedViewerConnection(e){const t=this._failedViewerConnections.get(e);if(t)if(this._intentionalDisconnect)this._failedViewerConnections.delete(e);else{this._log("Retrying viewer connection for stream:",e),t.retryCount++;try{await this.view(e,t.viewOptions),this._failedViewerConnections.delete(e),this._log("Successfully reconnected to stream:",e)}catch(s){this._log("Retry failed for stream:",e,s.message);const i=Math.min(3e4,2e3*Math.pow(2,Math.min(t.retryCount-1,5)));setTimeout(()=>{this._retryFailedViewerConnection(e)},i)}}}_generateSession(){const e="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";let t="";for(let s=0;s<8;s++)t+=e.charAt(Math.floor(62*Math.random()));return t}async _handleSignalingMessage(e){e&&e.id&&!this.state.uuid&&(this.state.uuid=e.id,this._log("Captured UUID:",e.id)),(e.request||e.description||e.candidate||e.candidates)&&this._log("Incoming message type:",e.request||(e.description?"description":"other")),e.description?await this._handleSDP(e):e.candidate?await this._handleRemoteICECandidate(e):e.candidates?await this._handleRemoteICECandidates(e):"joinroom"===e.request?await this._handleJoinRoom(e):"play"===e.request?await this._handlePlayRequest(e):"listing"===e.request?this._handleListing(e):"videoaddedtoroom"===e.request?this._handleVideoAddedToRoom(e):"someonejoined"===e.request?this._handleSomeoneJoined(e):"error"===e.request?this._handleError(e):"alert"===e.request?this._handleAlert(e):"transferred"===e.request?this._handleTransferred(e):"offerSDP"===e.request?this._handleOfferSDPRequest(e):e.rejected?this._handleRejected(e):void 0!==e.approved?this._handleApproved(e):e.bye?this._handleBye(e):e.hangup?this._handleHangup(e):void 0!==e.pipe&&e.__fallback&&this._handleFallbackData(e)}async _handleSDP(e){if(null!==this._getEffectivePassword()&&e.vector&&"string"==typeof e.description)try{const t=await this._decryptMessage(e.description,e.vector);e.description=JSON.parse(t),this._log("Decrypted SDP description")}catch(e){return void this._log("Failed to decrypt SDP description:",e)}"offer"===e.description.type?await this._handleOfferSDP(e):"answer"===e.description.type&&await this._handleAnswerSDP(e)}async _handleOfferSDP(e){this._log("Handling offer from:",e.UUID,"session:",e.session);const t=this._stripHashFromStreamID(e.streamID),s=this.connections.get(e.UUID);if(s&&s.viewer){const i=s.viewer;i.streamID===t&&i.session&&i.session!==e.session&&(this._log("Found existing connection with different session:",i.session,"vs",e.session),this._log("Closing old connection due to session mismatch"),i.pc&&i.pc.close(),delete s.viewer)}const i=await this._createConnection(e.UUID,"viewer");i.streamID=t,i.session=e.session;const n=this._pendingViews.get(t);n&&n.options?(i.viewPreferences={audio:!1!==n.options.audio,video:!1!==n.options.video},n.options.label&&(i.viewPreferences.info={label:n.options.label}),i.viewOptions=n.options,this._log("Attached view preferences to connection:",i.viewPreferences)):(i.viewPreferences={audio:!0,video:!0},i.viewOptions={audio:!0,video:!0},this._log("No pending view found, using default preferences:",i.viewPreferences)),this._log(`Created viewer connection for offer - UUID: ${e.UUID}, streamID: ${t}, session: ${e.session}`);try{await i.pc.setRemoteDescription(new RTCSessionDescription(e.description));const t=await this._createAnswer(i),s={UUID:e.UUID,session:e.session,streamID:e.streamID};if(null!==this._getEffectivePassword())try{const[e,i]=await this._encryptMessage(JSON.stringify(t));s.description=e,s.vector=i,this._log("Encrypted answer SDP")}catch(e){this._log("Failed to encrypt answer:",e),s.description=t}else s.description=t;if(i.dataChannel&&"open"===i.dataChannel.readyState){if(null!==this._getEffectivePassword()){const e={description:s.description,vector:s.vector};this._logMessage("OUT",e,"DataChannel"),i.dataChannel.send(JSON.stringify(e))}else{const e={description:t};this._logMessage("OUT",e,"DataChannel"),i.dataChannel.send(JSON.stringify(e))}this._log("Sent answer via data channel")}else this._sendMessageWS(s),this._log("Sent answer via WebSocket with session:",e.session)}catch(e){this._log("Error handling offer:",e),this._emit("error",{error:"Failed to handle offer",details:e.message})}}async _handleAnswerSDP(e){this._log("Handling answer from:",e.UUID,"session:",e.session);let t=this._getConnection(e.UUID,"publisher");if(t||(t=this._findConnection(e.UUID),t))if(e.session&&t.session&&e.session!==t.session)this._log("Session mismatch - ignoring answer. Expected:",t.session,"Got:",e.session);else try{await t.pc.setRemoteDescription(new RTCSessionDescription(e.description)),this._log("Remote description set successfully")}catch(e){this._log("Error handling answer:",e),this._emit("error",{error:"Failed to handle answer",details:e.message})}else this._log("No connection found for answer")}async _handleRemoteICECandidate(e){if(null!==this._getEffectivePassword()&&e.vector&&"string"==typeof e.candidate)try{const t=await this._decryptMessage(e.candidate,e.vector);e.candidate=JSON.parse(t),this._log("Decrypted ICE candidate")}catch(e){return void this._log("Failed to decrypt ICE candidate:",e)}let t=null;if(t="remote"===e.type?this._getConnection(e.UUID,"publisher"):"local"===e.type?this._getConnection(e.UUID,"viewer"):this._getConnection(e.UUID),!t||!t.pc)return this._log(`No connection found for ICE candidate with type: ${e.type}, UUID: ${e.UUID}`),void this._log("Available connections:",Array.from(this.connections.entries()).map(([e,t])=>`UUID: ${t.uuid}, type: ${t.type}, streamID: ${t.streamID}`));if(e.session&&t.session&&e.session!==t.session)this._log("Session mismatch - ignoring ICE candidate. Expected:",t.session,"Got:",e.session);else try{e.candidate&&!e.candidate.type&&(e.candidate.type="host"),await t.pc.addIceCandidate(new RTCIceCandidate(e.candidate)),this._log("Added ICE candidate")}catch(e){this._log("Error adding ICE candidate:",e)}}async _handleRemoteICECandidates(e){if(null!==this._getEffectivePassword()&&e.vector&&"string"==typeof e.candidates)try{const t=await this._decryptMessage(e.candidates,e.vector);e.candidates=JSON.parse(t),this._log("Decrypted ICE candidates bundle")}catch(e){return void this._log("Failed to decrypt ICE candidates:",e)}this._log(`Received ICE candidates bundle - type: ${e.type}, UUID: ${e.UUID}, session: ${e.session}`),this._log(`Candidates count: ${e.candidates?e.candidates.length:0}`);let t=null;if(t="remote"===e.type?this._getConnection(e.UUID,"publisher"):"local"===e.type?this._getConnection(e.UUID,"viewer"):this._getConnection(e.UUID),!t||!t.pc)return this._log(`No connection found for ICE candidates bundle with type: ${e.type}, UUID: ${e.UUID}`),void this._log("Available connections:",Array.from(this.connections.entries()).map(([e,t])=>`UUID: ${t.uuid}, type: ${t.type}, streamID: ${t.streamID}`));if(e.session&&t.session&&e.session!==t.session)this._log("Session mismatch - ignoring ICE candidates. Expected:",t.session,"Got:",e.session);else for(const s of e.candidates)try{s.candidate&&await t.pc.addIceCandidate(new RTCIceCandidate(s))}catch(e){this._log("Error adding ICE candidate:",e)}}async _handleJoinRoom(e){if(this._log("Received join room request:",e),!this.state.publishing)return void this._log("Not publishing, ignoring join request");const t=await this._createConnection(e.UUID,"publisher");this._pendingLabel&&"string"==typeof this._pendingLabel&&(t.info=t.info||{},t.info.label=this._sanitizeLabel(this._pendingLabel)),this._pendingInfo&&"object"==typeof this._pendingInfo&&(t.info=Object.assign(t.info||{},this._pendingInfo)),t.streamID=this.state.streamID,void 0===e.audio&&void 0===e.video||(t.allowAudio=!1!==e.audio,t.allowVideo=!1!==e.video);try{const s=await this._createOffer(t);let i=this.state.streamID;{const e=this._getEffectivePassword();null!==e&&(this._passwordHash||(this._passwordHash=await this._generateHash(e+this.salt,6)),i=this.state.streamID+this._passwordHash)}const n={UUID:this.state.uuid,session:e.session||t.uuid,streamID:i};if(e.session&&(this._sessionIDs[e.UUID]=e.session,this._remoteSessionIDs[e.UUID]=e.session),null!==this._getEffectivePassword())try{const[e,t]=await this._encryptMessage(JSON.stringify(s));n.description=e,n.vector=t,this._log("Encrypted offer SDP")}catch(e){this._log("Failed to encrypt offer:",e),n.description=s}else n.description=s;this._sendMessageWS(n),this._log("Sent offer to viewer")}catch(e){this._log("Error creating offer for viewer:",e)}}async _handlePlayRequest(e){this._log("Received play request for:",e.streamID,"from:",e.UUID);const t=this._stripHashFromStreamID(e.streamID);if(!this.state.publishing||this.state.streamID!==t)return void this._log("Not publishing this stream");const s=await this._createConnection(e.UUID,"publisher");this._pendingLabel&&"string"==typeof this._pendingLabel&&(s.info=s.info||{},s.info.label=this._sanitizeLabel(this._pendingLabel)),this._pendingInfo&&"object"==typeof this._pendingInfo&&(s.info=Object.assign(s.info||{},this._pendingInfo)),s.streamID=this.state.streamID,s.session=this._generateSession(),void 0===e.audio&&void 0===e.video||(s.allowAudio=!1!==e.audio,s.allowVideo=!1!==e.video);try{const t=await this._createOffer(s);let i=this.state.streamID;{const e=this._getEffectivePassword();null!==e&&(this._passwordHash||(this._passwordHash=await this._generateHash(e+this.salt,6)),i=this.state.streamID+this._passwordHash)}const n={UUID:e.UUID,session:s.session,streamID:i};if(null!==this._getEffectivePassword())try{const[e,s]=await this._encryptMessage(JSON.stringify(t));n.description=e,n.vector=s,this._log("Encrypted offer SDP")}catch(e){this._log("Failed to encrypt offer:",e),n.description=t}else n.description=t;this._sendMessageWS(n),this._log("Sent offer to viewer with session:",s.session)}catch(e){this._log("Error creating offer for play request:",e)}}_stripHashFromStreamID(e){if(!e||"string"!=typeof e)return e;if(e.length>6){const t=e.slice(-6);if(/^[a-f0-9]{6}$/.test(t))return e.slice(0,-6)}return e}_handleListing(e){if(this._log("Processing listing"),this._emit("_roomJoined"),e.list&&Array.isArray(e.list)){const t=e.list.map(e=>"string"==typeof e?this._stripHashFromStreamID(e):e&&e.streamID?{...e,streamID:this._stripHashFromStreamID(e.streamID)}:e),s=Date.now();t.forEach(e=>{const t="string"==typeof e?e:e.streamID;if(t){const i=this.streams.get(t);this.streams.set(t,{firstSeen:i?.firstSeen||s,lastSeen:s,uuid:e.UUID||e.uuid||i?.uuid||null,state:"available"})}}),this._emit("listing",{list:t,raw:e}),t.forEach((s,i)=>{const n=e.list[i];s&&(s.streamID||"string"==typeof s)&&this._emit("listing",{streamID:s.streamID||s,uuid:n&&n.UUID||n&&n.uuid,label:s.label,list:t})})}else this._emit("listing",{streamID:this._stripHashFromStreamID(e.streamID),uuid:e.UUID,label:e.label,raw:e});this._emit("peerListing",e)}_handleVideoAddedToRoom(e){const t=this._stripHashFromStreamID(e.streamID);if(this._log("Video added to room:",t),this._pendingViews.has(t)){this._log("Found pending view for newly available stream:",t);const e=this._pendingViews.get(t);this._log("Re-requesting stream that just became available:",t),this._pendingViews.delete(t),setTimeout(async()=>{try{await this.view(t,e.options),this._log("Successfully connected to newly available stream:",t)}catch(e){this._log("Failed to connect to newly available stream:",e.message)}},100)}if(this._emit("videoaddedtoroom",{streamID:t,uuid:e.UUID||e.uuid,raw:e}),this._emit("streamAdded",{streamID:t,uuid:e.UUID||e.uuid}),t&&this.streams){const s=Date.now(),i=this.streams.get(t);this.streams.set(t,{firstSeen:i?.firstSeen||s,lastSeen:s,uuid:e.UUID||e.uuid||i?.uuid||null,state:i?.state||"available"})}}_handleSomeoneJoined(e){const t=e.streamID?this._stripHashFromStreamID(e.streamID):null;this._log("Someone joined:",t||e.UUID),this._emit("videoaddedtoroom",{streamID:t,uuid:e.UUID||e.uuid,raw:e}),this._emit("userJoined",e)}_handleUserLeft(e){this._log("User left:",e.UUID);const t=this.connections.get(e.UUID);if(t){for(const e of["viewer","publisher"]){const s=t[e];s&&s.pc&&s.pc.close()}this.connections.delete(e.UUID)}this._emit("userLeft",e)}_handleError(e){this._log("Error from server:",e.message,"Code:",e.code),this._emit("error",{error:e.message,code:e.code,details:e})}_handleAlert(e){this._log("Alert from server:",e.message),e.message&&e.message.includes("Stream ID is already in use")&&(this.state.publishing=!1,this.state.streamID=null,this._log("Publishing failed due to stream ID conflict")),this._emit("alert",{message:e.message,raw:e})}_handleTransferred(e){this._log("Transferred to new room"),this._emit("transferred",{list:e.list,director:e.director,raw:e}),this._handleListing(e)}async _handleOfferSDPRequest(e){if(this._log("Server requesting offer SDP for viewer:",e.UUID),!this.state.publishing)return void this._log("Not publishing, ignoring offerSDP request");const t=await this._createConnection(e.UUID,"publisher");this._pendingLabel&&"string"==typeof this._pendingLabel&&(t.info=t.info||{},t.info.label=this._sanitizeLabel(this._pendingLabel)),this._pendingInfo&&"object"==typeof this._pendingInfo&&(t.info=Object.assign(t.info||{},this._pendingInfo)),t.streamID=this.state.streamID,t.session=this._generateSession();try{const s=await this._createOffer(t);let i=this.state.streamID;{const e=this._getEffectivePassword();null!==e&&(this._passwordHash||(this._passwordHash=await this._generateHash(e+this.salt,6)),i=this.state.streamID+this._passwordHash)}const n={UUID:e.UUID,session:t.session,streamID:i};if(null!==this._getEffectivePassword())try{const[e,t]=await this._encryptMessage(JSON.stringify(s));n.description=e,n.vector=t,this._log("Encrypted offer SDP")}catch(e){this._log("Failed to encrypt offer:",e),n.description=s}else n.description=s;this._sendMessageWS(n),this._log("Sent offer to viewer with session:",t.session)}catch(e){this._log("Error creating offer for viewer:",e)}}_handleRejected(e){this._log("Connection rejected:",e.rejected),this._emit("rejected",e)}_handleApproved(e){this._log("Connection approved"),this._emit("approved",e)}_handleBye(e){this._log("Received bye from:",e.UUID);const t=this.connections.get(e.UUID);if(t){const s=t.viewer;let i=!1,n=null,a=null;s&&s.streamID&&!this._intentionalDisconnect&&(i=!0,n=s.streamID,a=s.viewOptions||{audio:!0,video:!0},this._log("Viewer connection will be retried after bye from publisher:",n));for(const e of["viewer","publisher"]){const s=t[e];s&&(this._stopPingMonitoring(s),s.pc&&s.pc.close())}this.connections.delete(e.UUID),i&&(this._failedViewerConnections||(this._failedViewerConnections=new Map),this._failedViewerConnections.set(n,{uuid:e.UUID,viewOptions:a,retryCount:0,lastRetry:Date.now()}),this._log("Scheduling viewer reconnection after bye message"),setTimeout(()=>{this._retryFailedViewerConnection(n)},2e3))}this._emit("bye",e)}_handleHangup(e){this._log("Received hangup from:",e.UUID);const t=this.connections.get(e.UUID);if(t){for(const e of["viewer","publisher"]){const s=t[e];s&&(this._stopPingMonitoring(s),s.pc&&s.pc.close())}this.connections.delete(e.UUID)}this._emit("hangup",e)}_handleFallbackData(e){this._log("Received fallback data via WebSocket from:",e.UUID);const t={...e};delete t.__fallback,this._emit("dataReceived",{data:t.pipe,uuid:e.UUID,fallback:!0}),this._emit("data",{UUID:e.UUID,data:t})}_sendMessageWS(e){this.signaling&&this.signaling.readyState===WebSocket.OPEN?(this._logMessage("OUT",e,"WebSocket"),this.signaling.send(JSON.stringify(e))):this._log("WebSocket not ready, queuing message")}_emit(e,t={}){this.dispatchEvent(new CustomEvent(e,{detail:t}))}_log(...e){this.debug&&console.log("[VDONinjaSDK]",...e)}_logMessage(e,t,s){if(!this.debug)return;const i=JSON.parse(JSON.stringify(t));i.description&&("string"==typeof i.description?i.description=i.description.substring(0,10)+"...":i.description.sdp&&(i.description.sdp=i.description.sdp.substring(0,10)+"...")),i.candidate&&("string"==typeof i.candidate?i.candidate=i.candidate.substring(0,10)+"...":i.candidate.candidate&&(i.candidate.candidate=i.candidate.candidate.substring(0,10)+"...")),i.candidates&&("string"==typeof i.candidates?i.candidates=i.candidates.substring(0,10)+"...":Array.isArray(i.candidates)&&(i.candidates=i.candidates.map(e=>"string"==typeof e?e.substring(0,10)+"...":e.candidate?{...e,candidate:e.candidate.substring(0,10)+"..."}:e)));const n="IN"===e?"color: green":"color: blue",a="IN"===e?"← INBOUND":"→ OUTBOUND";console.log(`%c${a} [${s}]:`,n,JSON.stringify(i,null,2))}_generateStreamID(){return Math.random().toString(36).substring(2,15)}_generateSession(){return Math.random().toString(36).substring(2,10)}_generateUUID(){return"undefined"!=typeof crypto&&crypto.randomUUID?crypto.randomUUID():"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,function(e){const t=16*Math.random()|0;return("x"===e?t:3&t|8).toString(16)})}_setupCryptoUtils(){if(this._encoder=new TextEncoder,this._decoder=new TextDecoder,!this._saltProvidedViaOptions&&"undefined"!=typeof window&&window.location){const e=window.location.hostname;if(this._log("Setting salt based on hostname:",e),"vdo.ninja"===e||"steveseguin.github.io"===e)this.salt="vdo.ninja";else if(["vdo.ninja","rtc.ninja","versus.cam","socialstream.ninja"].includes(e.split(".").slice(-2).join(".")))this.salt=e.split(".").slice(-2).join(".");else{/^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$|^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/.test(e)||"localhost"===e||""===e?this.salt="vdo.ninja":this.salt=e}}else this._saltProvidedViaOptions||(this.salt="vdo.ninja");this._log("Salt set to:",this.salt)}_convertStringToArrayBufferView(e){return this._encoder.encode(e)}_toHexString(e){return Array.from(e,e=>("0"+(255&e).toString(16)).slice(-2)).join("")}_toByteArray(e){const t=new Uint8Array(e.length/2);for(let s=0;s<e.length;s+=2)t[s/2]=parseInt(e.substr(s,2),16);return t}async _encryptMessage(e,t=null){if(!t){const e=this._getEffectivePassword();if(null===e)throw new Error("Password not set for encryption");t=e+this.salt}const s=crypto.getRandomValues(new Uint8Array(16));try{const i=await crypto.subtle.digest({name:"SHA-256"},this._convertStringToArrayBufferView(t)),n=await crypto.subtle.importKey("raw",i,{name:"AES-CBC"},!1,["encrypt","decrypt"]),a=await crypto.subtle.encrypt({name:"AES-CBC",iv:s},n,this._convertStringToArrayBufferView(e)),o=new Uint8Array(a);return[this._toHexString(o),this._toHexString(s)]}catch(e){throw this._log("Encryption error:",e),e}}async _decryptMessage(e,t,s=null){if(!s){const e=this._getEffectivePassword();if(null===e)throw new Error("Password not set for decryption");s=e+this.salt}const i=this._toByteArray(e),n=this._toByteArray(t);try{const e=await crypto.subtle.digest({name:"SHA-256"},this._convertStringToArrayBufferView(s)),t=await crypto.subtle.importKey("raw",e,{name:"AES-CBC"},!1,["encrypt","decrypt"]),a=await crypto.subtle.decrypt({name:"AES-CBC",iv:n},t,i);return this._decoder.de