@vdoninja/sdk
Version:
AI-friendly P2P communication SDK for audio, video, and data. Includes WHIP/WHEP clients for publishing to Twitch, Meshcast, Cloudflare Stream
9 lines • 131 kB
JavaScript
/*! VDO.Ninja SDK v1.5.5
* Copyright (c) 2025-2026 Steve Seguin. All rights reserved.
* SPDX-License-Identifier: MPL-2.0
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
const MEDIA_STREAM_TRACK_ENABLED_DESCRIPTOR="undefined"!=typeof MediaStreamTrack&&MediaStreamTrack?.prototype?Object.getOwnPropertyDescriptor(MediaStreamTrack.prototype,"enabled"):null,OUTBOUND_VIDEO_STOP_MUTE_DELAY_MS=500,VDON_CHANNEL_CONTROL="sendChannel",VDON_CHANNEL_CHUNKED="chunked",VDON_CHANNEL_RESOURCES="resources",VDON_RESERVED_CHANNEL_PREFIX="x-",VDON_CHANNEL_BINARY="x-bin",VDON_DEFAULT_BUFFER_LOW=262144,VDON_DEFAULT_BUFFER_HIGH=1048576,VDON_FILE_CHUNK_SIZE=16384,VDON_FILE_EOF_COMPLETE="EOF1",VDON_FILE_EOF_CANCELLED="EOF2",VDON_RESOURCE_CHUNK_SIZE=16384;!function(e){"use strict";class t extends EventTarget{static get VERSION(){return"1.5.5"}_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}_sanitizeMeta(e,t=0){if("string"==typeof e)return this._sanitizeLabel(e);if(!e||"object"!=typeof e||Array.isArray(e))return null;if(t>1)return null;const i={};let n=0;for(const s of Object.keys(e)){if(n++>=32)break;const o=this._sanitizeLabel(s);if(!o)continue;const r=e[s];if("string"==typeof r)i[o]=this._sanitizeLabel(r);else if("number"==typeof r||"boolean"==typeof r)i[o]=r;else if(r&&"object"==typeof r&&!Array.isArray(r)){const e=this._sanitizeMeta(r,t+1);e&&"object"==typeof e&&(i[o]=e)}}return i}_sanitizeLabel(e){if(!e||"string"!=typeof e)return"";const t=document.createElement("div");t.innerText=e;let i=t.textContent||t.innerText||"";return i=i.substring(0,Math.min(i.length,100)),i.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._sanitizeMeta(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._sanitizeMeta(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._publishMediaConfig=null,this.on=(e,t)=>{try{this.addEventListener(e,t)}catch(e){}return this},this.off=(e,t)=>{try{this.removeEventListener(e,t)}catch(e){}return this},this.once=(e,t)=>{try{const i=n=>{this.removeEventListener(e,i),t(n)};this.addEventListener(e,i)}catch(e){}return this},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._teardownPromise=null,this._passwordHash=null,this._passwordHashPromise=null,this._passwordHashKey=null,this._passwordHashPromiseKey=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._signalingQueue=[],this._signalingQueueLimit=Number.isFinite(e.signalingQueueLimit)?Math.max(1,Math.floor(e.signalingQueueLimit)):30,this._pendingIceCandidates=new Map,this._pendingIceLastSweep=0,this._pendingIceSweepMinMs=2e3,this._pendingIceTTL=Number.isFinite(e.pendingIceTTL)?Math.max(1e3,e.pendingIceTTL):15e3,this._pendingIceMaxPerPeer=Number.isFinite(e.pendingIceMaxPerPeer)?Math.max(1,Math.floor(e.pendingIceMaxPerPeer)):100,this._pendingIceMaxKeys=Number.isFinite(e.pendingIceMaxKeys)?Math.max(1,Math.floor(e.pendingIceMaxKeys)):500,this._connectionIntent={room:null,publishing:null,views:new Map},this._stoppedViews=new Set,this._restoringIntent=!1,this.autoRecover=!1!==e.autoRecover,this.autoRelay=!1!==e.autoRelay,this.disconnectGracePeriod=Number.isFinite(e.disconnectGracePeriod)?Math.max(0,e.disconnectGracePeriod):5e3,this.recoveryTimeout=Number.isFinite(e.recoveryTimeout)?Math.max(1e3,e.recoveryTimeout):12e3,this.connectionTimeout=Number.isFinite(e.connectionTimeout)?Math.max(1e3,e.connectionTimeout):2e4,this.relayRestoreDelay=Number.isFinite(e.relayRestoreDelay)?Math.max(0,e.relayRestoreDelay):45e3,this._isReconnecting=!1,this._intentionalDisconnect=!1,this._viewRetryTimers=new Map,this._viewRetryInterval=9e5,this._outboundVideoMonitors=new Map,this._pendingVideoMuteFinalizers=new Set,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._teardownPromise){const e=this._teardownPromise;await e,this._teardownPromise===e&&(this._teardownPromise=null)}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._signalingQueue||(this._signalingQueue=[]),this._pendingIceCandidates||(this._pendingIceCandidates=new Map),!this.state.connected)return this._intentionalDisconnect=!1,(e.host||e.wss)&&(this.host=e.host||e.wss),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)),void 0!==e.datamode&&(console.warn("[VDONinja SDK] connect({ datamode }) is not used. Establish data channels via announce() (publisher) and/or view() (viewer)."),this._emit("alert",{message:"connect({ datamode }) has no effect. Use announce()/view() to create data channels."})),new Promise((e,t)=>{try{const i=new WebSocket(this.host);this.signaling=i,i.onopen=()=>{this.signaling===i&&(this._log("WebSocket connected"),this.state.connected=!0,this._isReconnecting||(this._reconnectAttempts=0),this._flushSignalingQueue(),this._emit("connected"),this._emitIframeCompatible("hss-connection","connected"),e())},i.onmessage=async e=>{if(this.signaling===i)try{const t=JSON.parse(e.data);this._logMessage("IN",t,"WebSocket"),await this._handleSignalingMessage(t)}catch(e){this._log("Error parsing message:",e)}},i.onerror=e=>{this.signaling===i&&(this._log("WebSocket error:",e),this._emit("error",{error:"WebSocket error",details:e}),t(e))},i.onclose=()=>{if(this.signaling!==i)return;this._log("WebSocket closed"),this.state.connected=!1,this.state.roomJoined=!1,this.state.publishing=!1;const e=!!this._intentionalDisconnect,t=!e&&this._reconnectAttempts<this._maxReconnectAttempts;this._emit("disconnected",{intentional:e,reason:e?"local-disconnect":"socket-closed",willReconnect:t,phase:"socket"}),this._emitIframeCompatible("hss-connection","closed"),t&&this._attemptReconnect()}}catch(e){this._log("Connection error:",e),t(e)}});this._log("Already connected")}disconnect(){if(this._log("Disconnecting..."),this._teardownPromise)return this._teardownPromise;if(this._intentionalDisconnect=!0,this._connectionIntent&&(this._connectionIntent.room=null,this._connectionIntent.publishing=null,this._connectionIntent.views&&this._connectionIntent.views.clear()),this._stoppedViews&&this._stoppedViews.clear(),this._signalingQueue&&(this._signalingQueue.length=0),this._pendingIceCandidates&&this._pendingIceCandidates.clear(),this._failedViewerConnections){for(const e of this._failedViewerConnections.values())e&&e.timer&&clearTimeout(e.timer);this._failedViewerConnections.clear()}this._reconnectTimer&&(clearTimeout(this._reconnectTimer),this._reconnectTimer=null),this._isReconnecting=!1,this._restoringIntent=!1,this._log("Connections count:",this.connections.size);const e=[];for(const[t,i]of this.connections)for(const n of["viewer","publisher"]){const s=i[n];if(s&&(this._log(`Connection ${t}:${n}: dataChannel=${!!s.dataChannel}, state=${s.dataChannel?.readyState}`),s.dataChannel&&"open"===s.dataChannel.readyState))try{const i={bye:!0};s.dataChannel.send(JSON.stringify(i)),this._log("Sent bye message to:",t,"type:",n);const o=new Promise(e=>{const t=()=>{s.dataChannel&&"open"===s.dataChannel.readyState&&0!==s.dataChannel.bufferedAmount?setTimeout(t,10):e()};t()}),r=new Promise(e=>setTimeout(e,100));e.push(Promise.race([o,r]))}catch(e){this._log("Error sending bye message:",e)}}const t=this.signaling;return this._teardownPromise=Promise.all(e).then(async()=>{for(const[e,t]of this.connections)for(const e of["viewer","publisher"]){const i=t[e];i&&(this._stopPingMonitoring(i),this._clearConnectionRecoveryTimers(i),i._finalized=!0,i.pc&&i.pc.close())}this.connections.clear(),this._clearAllVideoTrackMonitors();for(const[e,t]of this._viewRetryTimers)clearTimeout(t);this._viewRetryTimers.clear(),await this._closeSignalingSocket(t),this.signaling===t&&(this.signaling=null),this.state={connected:!1,room:null,streamID:null,uuid:null,roomJoined:!1,publishing:!1},this._emit("disconnected",{intentional:!0,reason:"teardown-complete",willReconnect:!1,phase:"teardown"}),this._emit("teardownComplete",{reason:"local-disconnect"})}),this._teardownPromise}_closeSignalingSocket(e){if(!e)return Promise.resolve();const t="undefined"!=typeof WebSocket&&"number"==typeof WebSocket.CLOSED?WebSocket.CLOSED:3;return e.readyState===t?Promise.resolve():new Promise(t=>{let i=!1,n=null,s=null;const o=()=>{if(!i){if(i=!0,s&&clearTimeout(s),n)try{n()}catch(e){}t()}};let r=!1;if("function"==typeof e.addEventListener){const t=()=>o();try{e.addEventListener("close",t),n=()=>e.removeEventListener("close",t),r=!0}catch(e){}}if(!r&&"function"==typeof e.once)try{e.once("close",o),n=()=>{"function"==typeof e.removeListener&&e.removeListener("close",o)},r=!0}catch(e){}if(!r&&"onclose"in e){const t=e.onclose,i=i=>{try{"function"==typeof t&&t.call(e,i)}finally{o()}};try{e.onclose=i,n=()=>{e.onclose===i&&(e.onclose=t)},r=!0}catch(e){}}try{e.close()}catch(e){return this._log("Error closing signaling socket:",e),void o()}r?"function"==typeof e.terminate&&(s=setTimeout(()=>{try{e.terminate()}catch(e){o()}},1e3)):o()})}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(),await this._restoreConnectionIntent(),this._emit("reconnected"),this._reconnectAttempts=0,this._isReconnecting=!1}catch(e){this._log("Reconnection failed:",e),this._isReconnecting=!1,this._reconnectAttempts>=this._maxReconnectAttempts?this._emit("reconnectFailed"):this._attemptReconnect()}},e)}async _restoreConnectionIntent(){if(!this._restoringIntent){this._restoringIntent=!0;try{const e=this._connectionIntent||{};if(e.room&&e.room.room&&await this.joinRoom({...e.room.options,room:e.room.room,password:e.room.password}),e.publishing&&e.publishing.active){const t=e.publishing,i={...t.options,streamID:t.streamID};t.dataOnly?await this.announce(i):t.stream&&await this.publish(t.stream,i)}if(e.views&&"function"==typeof e.views.entries)for(const[t,i]of e.views.entries())this._stoppedViews&&this._stoppedViews.has(t)||this.view(t,{...i,_intentReplay:!0}).catch(e=>{this._log("Failed to restore view intent for",t,e.message||e),this._setupViewRetry(t,i,2e3)})}finally{this._restoringIntent=!1}}}getStreams(){const e=[],t=Date.now();for(const[i,n]of this.streams)e.push({streamID:i,firstSeen:n.firstSeen,lastSeen:n.lastSeen,timeSinceLastSeen:t-n.lastSeen,uuid:n.uuid,state:n.state,viewRequestTime:n.viewRequestTime,waitingTime:"pending"===n.state&&n.viewRequestTime?t-n.viewRequestTime:null});return e.sort((e,t)=>t.lastSeen-e.lastSeen)}getStreamInfo(e){const t=this.streams.get(e);if(!t)return null;const i=Date.now();return{streamID:e,firstSeen:t.firstSeen,lastSeen:t.lastSeen,timeSinceLastSeen:i-t.lastSeen,uuid:t.uuid,state:t.state,viewRequestTime:t.viewRequestTime,waitingTime:"pending"===t.state&&t.viewRequestTime?i-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),i=this._sanitizePassword(void 0!==e.password?e.password:this.password);if(!t)throw new Error("Room name is required");this._connectionIntent.room={room:t,password:i,options:{claim:!!e.claim}},this.password=""===i?this._sanitizePassword("someEncryptionKey123"):i;let n=t;const s=this._getEffectivePassword();null!==s&&(n=await this._hashRoom(t,s)),this._log("Joining room:",t,"with hash:",n),void 0!==e.role&&(console.warn("[VDONinja SDK] joinRoom({ role }) is not used. Your role is determined by calling announce() (publisher) and/or view() (viewer)."),this._emit("alert",{message:"joinRoom({ role }) is ignored. Use announce() to publish and view() to view."}));const o={request:"joinroom",roomid:n};return e.claim&&(o.claim=!0),this._sendMessageWS(o),new Promise((e,i)=>{const n=setTimeout(()=>{i(new Error("Room join timeout"))},1e4),s=i=>{clearTimeout(n),this.removeEventListener("_roomJoined",s),this.state.room=t,this.state.roomJoined=!0,this._emit("roomJoined",{room:t}),e()};this.addEventListener("_roomJoined",s)})}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._connectionIntent&&(this._connectionIntent.room=null),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 i=await this._extractPublisherMediaOptions(t);i&&(this._publishMediaConfig=i),this._publishMediaConfig&&await this._applyLocalMediaPreferences(this.localStream,this._publishMediaConfig);const n=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._sanitizeMeta(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._sanitizeMeta(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 s=n;{const e=this._getEffectivePassword();null!==e&&(s=await this._hashStreamID(n,e))}if(this._log("Publishing with streamID:",n,"as:",s),this.state.streamID=n,this.state.publishing=!0,this._clearAllVideoTrackMonitors(),this.localStream){(this.localStream.getVideoTracks?this.localStream.getVideoTracks():[]).forEach(e=>this._monitorOutboundVideoTrack(e))}const o={request:"seed",streamID:s};if(this._log("Sending seed message for streamID:",s),this._sendMessageWS(o),this._connectionIntent.publishing={active:!0,dataOnly:!1,stream:this.localStream,streamID:n,options:{...t,streamID:n}},this._emit("publishing",{streamID:n,hashedStreamID:s}),this.streams){const e=Date.now();this.streams.set(n,{firstSeen:e,lastSeen:e,uuid:this.state.uuid,state:"connected"})}return n}async announce(e={}){if(!this.state.connected)throw new Error("Not connected to signaling server");if(void 0!==e.role&&(console.warn("[VDONinja SDK] announce({ role }) is not used. Remove role and just call announce()."),this._emit("alert",{message:"announce({ role }) is ignored. Remove role and call announce({ streamID })."})),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._sanitizeMeta(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._sanitizeMeta(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 i=t;{const e=this._getEffectivePassword();null!==e&&(i=await this._hashStreamID(t,e))}this._log("Announcing availability with streamID:",t,"as:",i),this.state.streamID=t,this.state.publishing=!0;const n={request:"seed",streamID:i};return this._sendMessageWS(n),this._connectionIntent.publishing={active:!0,dataOnly:!0,stream:null,streamID:t,options:{...e,streamID:t}},this._emit("publishing",{streamID:t,hashedStreamID:i,dataOnly:!0}),t}stopPublishing(){const e=!(!this._connectionIntent||!this._connectionIntent.publishing);if(this._connectionIntent&&(this._connectionIntent.publishing=null),!this.state.publishing&&!e)return void this._log("Not currently publishing");if(this._outboundVideoMonitors&&this._outboundVideoMonitors.size)for(const e of this._outboundVideoMonitors.values())this._sendVideoMutedState(e.track,!0,null,"stopPublishing");const t=[];for(const[e,i]of this.connections){const n=i.publisher;if(n&&n.dataChannel&&"open"===n.dataChannel.readyState)try{const i={bye:!0};this._sendDataInternal(i,e,null,"publisher"),this._log("Sent bye message to viewer:",e);const s=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));t.push(Promise.race([s,o]))}catch(e){this._log("Error sending bye message:",e)}}Promise.all(t).then(()=>{for(const[e,t]of this.connections){const i=t.publisher;i&&(this._stopPingMonitoring(i),this._clearConnectionRecoveryTimers(i),i._finalized=!0,i.pc&&i.pc.close(),delete t.publisher,t.viewer||t.publisher||this.connections.delete(e))}this._clearAllVideoTrackMonitors(),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 i=Date.now(),n=this.streams.get(e);this.streams.set(e,{firstSeen:n?.firstSeen||i,lastSeen:i,uuid:n?.uuid||null,state:"pending",viewRequestTime:i});try{let i=e;{const t=this._getEffectivePassword();null!==t&&(i=await this._hashStreamID(e,t))}const n=t&&!0===t.dataOnly?{...t,audio:!1,video:!1}:t||{},s={...n};delete s._intentReplay,this._connectionIntent.views.set(e,s),this._stoppedViews.delete(e),this._pendingViews.set(e,{options:n,timestamp:Date.now(),hashedStreamID:i}),this._lastViewOptions=this._lastViewOptions||{},this._lastViewOptions[e]=n;const o={request:"play",streamID:i};if(!this.signaling||this.signaling.readyState!==WebSocket.OPEN)throw new Error("WebSocket not connected");return this._logMessage("OUT",o,"WebSocket"),this.signaling.send(JSON.stringify(o)),this._log("Sent view request for:",e,"as:",i),new Promise((i,n)=>{const s=setInterval(()=>{for(const[t,n]of this.connections){const t=n.viewer;if(t&&t.streamID===e&&t.pc)return clearInterval(s),clearTimeout(o),this._pendingViews.delete(e),void i(t.pc)}},100),o=setTimeout(()=>{clearInterval(s),this._log(`Stream ${e} not available yet, will retry in 15 minutes`),this._setupViewRetry(e,t),i(null)},15e3)})}catch(t){throw this._log("Error in view:",t.message),this._pendingViews.delete(e),t}}_setupViewRetry(e,t,i=this._viewRetryInterval){this._viewRetryTimers.has(e)&&clearTimeout(this._viewRetryTimers.get(e));const n=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)})},i);this._viewRetryTimers.set(e,n)}stopViewing(e){if(e=this._sanitizeStreamID(e),this._connectionIntent&&this._connectionIntent.views&&this._connectionIntent.views.delete(e),this._stoppedViews&&this._stoppedViews.add(e),this._viewRetryTimers.has(e)&&(clearTimeout(this._viewRetryTimers.get(e)),this._viewRetryTimers.delete(e)),this._pendingViews.delete(e),this._failedViewerConnections){const t=this._failedViewerConnections.get(e);t&&t.timer&&clearTimeout(t.timer),this._failedViewerConnections.delete(e)}const t=[],i=this._getConnections({streamID:e,type:"viewer"});for(const e of i)if(e.dataChannel&&"open"===e.dataChannel.readyState)try{const i={bye:!0};e.dataChannel.send(JSON.stringify(i)),this._log("Sent bye message to publisher:",e.uuid);const n=new Promise(t=>{const i=()=>{e.dataChannel&&"open"===e.dataChannel.readyState&&0!==e.dataChannel.bufferedAmount?setTimeout(i,10):t()};i()}),s=new Promise(e=>setTimeout(e,100));t.push(Promise.race([n,s]))}catch(e){this._log("Error sending bye message:",e)}Promise.all(t).then(()=>{for(const[t,i]of this.connections){const n=i.viewer;n&&n.streamID===e&&(this._clearConnectionRecoveryTimers(n),n._finalized=!0,n.pc&&n.pc.close(),delete i.viewer,i.viewer||i.publisher||this.connections.delete(t))}this._emit("viewingStopped",{streamID:e})})}async _createConnection(e,t="viewer"){let i,n;"string"==typeof e?(i=e,this._log("Creating connection to remote peer UUID:",i,"type:",t)):(n=e||{},i=n.uuid,t=n.type||t,this._log("Creating connection with options:",n));const s=await this._getICEConfiguration(),o={uuid:i,type:t,pc:new RTCPeerConnection(s),dataChannel:null,channels:new Map,streamID:null,session:null,info:{label:n?.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,healthState:"connecting",recoveryStage:0,recovering:!1,disconnectTimer:null,recoveryTimer:null,connectTimer:null,relayRestoreTimer:null,relayEscalated:!1,originalConfiguration:null,_peerConnectedEmitted:!1,_finalized:!1};if(n&&(n.streamID&&(o.streamID=n.streamID),n.viewPreferences&&(o.viewPreferences=n.viewPreferences)),o.iceTimer=null,o.iceBundle=[],o.iceBundleDelay=70,o.pc.onicecandidate=e=>{this._handleICECandidate(e,o)},o.pc.oniceconnectionstatechange=()=>{this._handlePeerConnectionState(o,"ice")},o.pc.onconnectionstatechange=()=>{this._handlePeerConnectionState(o,"peer")},o.pc.ontrack=e=>{this._log("Track received:",e.track.kind,"from:",i),this._emit("track",{track:e.track,streams:e.streams,uuid:i,streamID:o.streamID})},"publisher"===t){const e=o.pc.createDataChannel("sendChannel",{ordered:!0});o.dataChannel=e,o.channels.set("sendChannel",e),this._setupDataChannel(o,e)}o.pc.ondatachannel=e=>{this._handleIncomingDataChannel(o,e.channel)},this.connections.has(i)||this.connections.set(i,{});return this.connections.get(i)[t]=o,o.connectTimer=setTimeout(()=>{if(o.connectTimer=null,!this._isCurrentConnection(o)||!o.pc)return;const e=o.pc.connectionState,t=o.pc.iceConnectionState;"connected"!==e&&"connected"!==t&&"completed"!==t&&this._recoverConnection(o,"connect-timeout")},this.connectionTimeout),o.connectTimer&&"function"==typeof o.connectTimer.unref&&o.connectTimer.unref(),o}_isTrackEffectivelyMuted(e){return!e||("ended"===e.readyState||(!1===e.enabled||!0===e.muted))}_sendVideoMutedState(e,t,i=null,n=null){if(!this.state||!this.state.publishing)return;const s={videoMuted:!!t};e&&"string"==typeof e.id&&(s.trackId=e.id),n&&this._log(`Broadcasting videoMuted:${s.videoMuted} (${n})`),i?this._sendDataInternal(s,i,null,"publisher"):this._sendDataInternal(s,null,"publisher","publisher")}_monitorOutboundVideoTrack(e){if(!e||"video"!==e.kind)return;if(this._pendingVideoMuteFinalizers&&this._pendingVideoMuteFinalizers.size&&this._cancelPendingVideoMuteFinalizers(),this._outboundVideoMonitors||(this._outboundVideoMonitors=new Map),this._outboundVideoMonitors.has(e)){const t=this._outboundVideoMonitors.get(e),i=this._isTrackEffectivelyMuted(e);return t&&(t.lastState=i),void(this.state?.publishing&&this._sendVideoMutedState(e,i,null,"refresh"))}const t={track:e,lastState:this._isTrackEffectivelyMuted(e),restoreEnabled:null,restoreStop:null,listeners:[],poller:null,pendingFinalizer:null,finalizing:!1},i=(t,i)=>{const n=i||"update";this._sendVideoMutedState(e,t,null,n),this._emit("publisherVideoMuteState",{track:e,muted:t,reason:n})},n=(n,s={})=>{const{force:o=!1,delay:r=0,cleanup:a=!1}=s,l=()=>this._isTrackEffectivelyMuted(e),c=()=>{t.pendingFinalizer&&(t.pendingFinalizer=null);const s=l();o||s!==t.lastState?(t.lastState=s,i(s,n||"update"),a&&this._unmonitorOutboundVideoTrack(e,{skipSend:!0})):a&&this._unmonitorOutboundVideoTrack(e,{skipSend:!0})};if(t.pendingFinalizer){try{t.pendingFinalizer.cancel()}catch(e){this._log("Error cancelling pending mute finalizer:",e)}t.pendingFinalizer=null}r>0?t.pendingFinalizer=this._scheduleVideoMuteFinalizer(r,c,()=>{t.pendingFinalizer=null}):c()},s=e=>{t.finalizing||(t.finalizing=!0,n(e,{force:!0,delay:500,cleanup:!0}))},o=()=>n("mute"),r=()=>n("unmute"),a=()=>s("ended");if("function"==typeof e.addEventListener&&(e.addEventListener("mute",o),e.addEventListener("unmute",r),e.addEventListener("ended",a),t.listeners.push(["mute",o],["unmute",r],["ended",a])),"function"==typeof e.stop){const i=e.stop.bind(e);try{e.stop=(...e)=>(s("stop"),i(...e)),t.restoreStop=()=>{try{e.stop=i}catch(e){this._log("Failed to restore track.stop:",e)}}}catch(e){this._log("Unable to wrap MediaStreamTrack.stop for monitoring:",e)}}let l=!1;try{l=Object.isExtensible(e)}catch(e){this._log("Unable to inspect MediaStreamTrack extensibility:",e)}if(!!MEDIA_STREAM_TRACK_ENABLED_DESCRIPTOR&&"function"==typeof MEDIA_STREAM_TRACK_ENABLED_DESCRIPTOR.get&&"function"==typeof MEDIA_STREAM_TRACK_ENABLED_DESCRIPTOR.set&&!1!==MEDIA_STREAM_TRACK_ENABLED_DESCRIPTOR.configurable&&l){const{get:i,set:s}=MEDIA_STREAM_TRACK_ENABLED_DESCRIPTOR;try{Object.defineProperty(e,"enabled",{configurable:!0,enumerable:!0,get:()=>i.call(e),set:t=>{const o=i.call(e);s.call(e,t);o!==i.call(e)&&n("enabled-toggle")}}),t.restoreEnabled=()=>{try{delete e.enabled}catch(e){this._log("Failed to restore track.enabled descriptor:",e)}}}catch(e){this._log("Unable to wrap MediaStreamTrack.enabled for monitoring:",e)}}else this._log("Skipping MediaStreamTrack.enabled override; falling back to polling.");t.restoreEnabled||(t.poller=setInterval(()=>n("poll"),250)),this._outboundVideoMonitors.set(e,t),this.state?.publishing?i(t.lastState,"initial"):this._emit("publisherVideoMuteState",{track:e,muted:t.lastState,reason:"initial"})}_unmonitorOutboundVideoTrack(e,t={}){if(!e||!this._outboundVideoMonitors||!this._outboundVideoMonitors.has(e))return;const i=this._outboundVideoMonitors.get(e);if(i){if(i.pendingFinalizer){try{i.pendingFinalizer.cancel()}catch(e){this._log("Error cancelling monitor pending finalizer:",e)}i.pendingFinalizer=null}if(!t.skipSend&&this.state?.publishing&&!i.finalizing){const n="boolean"!=typeof t.forceMuted||t.forceMuted,s="boolean"==typeof t.forceMuted?"force-muted":"monitor-stopped",o="number"==typeof t.delayMs?Math.max(0,t.delayMs):n?500:0,r=()=>{this._sendVideoMutedState(e,n,null,s),this._emit("publisherVideoMuteState",{track:e,muted:n,reason:s})};o>0?i.pendingFinalizer=this._scheduleVideoMuteFinalizer(o,r,()=>{i.pendingFinalizer=null}):r()}if(i.restoreEnabled)try{i.restoreEnabled()}catch(e){this._log("Failed to restore track.enabled descriptor:",e)}if(i.restoreStop)try{i.restoreStop()}catch(e){this._log("Failed to restore track.stop override:",e)}if(i.poller&&(clearInterval(i.poller),i.poller=null),i.listeners&&"function"==typeof e.removeEventListener)for(const[t,n]of i.listeners)try{e.removeEventListener(t,n)}catch(e){}}this._outboundVideoMonitors.delete(e)}_scheduleVideoMuteFinalizer(e,t,i){this._pendingVideoMuteFinalizers||(this._pendingVideoMuteFinalizers=new Set);const n=this,s={timerId:null,finished:!1,flush(){if(!s.finished){s.finished=!0,s.timerId&&(clearTimeout(s.timerId),s.timerId=null),n._pendingVideoMuteFinalizers.delete(s);try{t()}catch(e){n._log("Error dispatching videoMuted finalizer:",e)}if("function"==typeof i)try{i(!0)}catch(e){n._log("Error running mute finalizer onFinish handler:",e)}}},cancel(){if(!s.finished&&(s.finished=!0,s.timerId&&(clearTimeout(s.timerId),s.timerId=null),n._pendingVideoMuteFinalizers.delete(s),"function"==typeof i))try{i(!1)}catch(e){n._log("Error running mute finalizer cancel handler:",e)}}};return s.timerId=setTimeout(()=>s.flush(),e),this._pendingVideoMuteFinalizers.add(s),s}_cancelPendingVideoMuteFinalizers(){if(!this._pendingVideoMuteFinalizers||0===this._pendingVideoMuteFinalizers.size)return;const e=Array.from(this._pendingVideoMuteFinalizers);for(const t of e)try{t.flush()}catch(e){this._log("Error flushing pending mute finalizer:",e)}}_clearAllVideoTrackMonitors(){if(!this._outboundVideoMonitors||0===this._outboundVideoMonitors.size)return void(this._pendingVideoMuteFinalizers&&this._pendingVideoMuteFinalizers.size&&this._cancelPendingVideoMuteFinalizers());this._cancelPendingVideoMuteFinalizers();const e=Array.from(this._outboundVideoMonitors.keys());for(const t of e)this._unmonitorOutboundVideoTrack(t,{skipSend:!0});this._outboundVideoMonitors.clear(),this._pendingVideoMuteFinalizers&&this._pendingVideoMuteFinalizers.clear()}_syncVideoMuteStateToConnection(e){if(e&&"publisher"===e.type&&this.state&&this.state.publishing&&this._outboundVideoMonitors&&0!==this._outboundVideoMonitors.size)for(const t of this._outboundVideoMonitors.values()){const i=t.track,n="boolean"==typeof t.lastState?t.lastState:this._isTrackEffectivelyMuted(i);this._sendVideoMutedState(i,n,e.uuid,"sync")}}_getConnection(e,t=null){const i=this.connections.get(e);return i?t?i[t]||null:i.viewer||i.publisher||null:null}_getConnections(e={}){const t=[];for(const[i,n]of this.connections)if(!e.uuid||i===e.uuid)for(const i of["viewer","publisher"]){const s=n[i];s&&(e.type&&i!==e.type||e.streamID&&s.streamID!==e.streamID||t.push(s))}return t}_applyViewerCapabilities(e,t){e.downloads=!1!==t.downloads,!0===t.allowresources&&(e.allowresources=!0)}_handleIncomingDataChannel(e,t){if(!t)return;const i="string"==typeof t.label?t.label:"";if(this._log(`Data channel received from ${e.uuid}: "${i}"`),this._trackChannel(e,t),!i||"sendChannel"===i)return e.dataChannel=t,void this._setupDataChannel(e,t);this._setupAuxiliaryChannel(e,t,i)}_trackChannel(e,t){if(!e||!t)return;e.channels||(e.channels=new Map);const i="string"==typeof t.label&&t.label?t.label:"sendChannel";e.channels.set(i,t);const n=()=>{e.channels&&e.channels.get(i)===t&&e.channels.delete(i)};if("function"==typeof t.addEventListener)try{t.addEventListener("close",n)}catch(e){}}_isReservedChannelLabel(e){return"string"==typeof e&&e.startsWith("x-")}_connectionsFor(e){const t=this.connections.get(e);return t?[t.publisher,t.viewer].filter(Boolean):[]}_connectionFor(e){const t=this._connectionsFor(e);return t.find(e=>e.pc&&"closed"!==e.pc.connectionState)||t[0]||null}async openChannel(e,t,i={}){if("string"!=typeof t||!t)throw new Error("openChannel requires a label");if("number"==typeof i.maxRetransmits&&"number"==typeof i.maxPacketLifeTime)throw new Error("maxRetransmits and maxPacketLifeTime are mutually exclusive");const n=this._isReservedChannelLabel(t)?t:"x-"+t,s=this._connectionsFor(e);for(const e of s){const t=e.channels&&e.channels.get(n);if(t&&"closed"!==t.readyState&&"closing"!==t.readyState)return"open"===t.readyState?t:this._awaitChannelOpen(t,i.timeout)}const o=s.find(e=>e.pc&&"closed"!==e.pc.connectionState)||null;if(!o)throw new Error(`No connection to ${e}`);const r={ordered:!1!==i.ordered};"number"==typeof i.maxRetransmits&&(r.maxRetransmits=i.maxRetransmits),"number"==typeof i.maxPacketLifeTime&&(r.maxPacketLifeTime=i.maxPacketLifeTime),"string"==typeof i.protocol&&(r.protocol=i.protocol);const a=o.pc.createDataChannel(n,r);return a.binaryType="arraybuffer",this._trackChannel(o,a),this._attachBufferedAmountLow(o,a,n),this._log(`Opened channel "${n}" to ${e} (ordered=${r.ordered}`+(void 0!==r.maxRetransmits?", maxRetransmits="+r.maxRetransmits:"")+(void 0!==r.maxPacketLifeTime?", maxPacketLifeTime="+r.maxPacketLifeTime:"")+")"),this._awaitChannelOpen(a,i.timeout)}_awaitChannelOpen(e,t=15e3){return"open"===e.readyState?Promise.resolve(e):new Promise((i,n)=>{const s=setTimeout(()=>{o(),n(new Error(`Channel "${e.label}" did not open within ${t}ms`))},t);s&&"function"==typeof s.unref&&s.unref();const o=()=>{if(clearTimeout(s),"function"==typeof e.removeEventListener)try{e.removeEventListener("open",r),e.removeEventListener("close",a),e.removeEventListener("error",a)}catch(e){}},r=()=>{o(),i(e)},a=()=>{o(),n(new Error(`Channel "${e.label}" closed before opening`))};"function"==typeof e.addEventListener?(e.addEventListener("open",r),e.addEventListener("close",a),e.addEventListener("error",a)):e.onopen=r,"open"===e.readyState&&r()})}getChannel(e,t){const i=this._isReservedChannelLabel(t)?t:"x-"+t;let n=null;for(const t of this._connectionsFor(e)){if(!t.channels)continue;const e=t.channels.get(i);if(e){if("closed"!==e.readyState&&"closing"!==e.readyState)return e;n||(n=e)}}return n}_attachBufferedAmountLow(e,t,i){const n=262144;try{t.bufferedAmountLowThreshold=n}catch(e){}let s=!1,o=!0,r=null;const a=()=>{s=!1,o=!0,this._emit("bufferedAmountLow",{uuid:e.uuid,streamID:e.streamID,label:i,bufferedAmount:t.bufferedAmount})},l=()=>{"number"==typeof t.bufferedAmount&&(t.bufferedAmount>n?(s=!0,o=!1):s&&!o&&a())};try{Object.defineProperty(t,"_sdkObserveBufferedAmount",{value:l,configurable:!0})}catch(e){try{t._sdkObserveBufferedAmount=l}catch(e){}}const c=()=>{"number"!=typeof t.bufferedAmountLowThreshold||t.bufferedAmountLowThreshold===n?a():l()},d=()=>{if(r&&(clearInterval(r),r=null),"function"==typeof t.removeEventListener){try{t.removeEventListener("bufferedamountlow",c)}catch(e){}try{t.removeEventListener("close",d)}catch(e){}}if(t._sdkObserveBufferedAmount===l)try{delete t._sdkObserveBufferedAmount}catch(e){}};if("function"==typeof t.addEventListener){try{t.addEventListener("bufferedamountlow",c)}catch(e){}try{t.addEventListener("close",d)}catch(e){}}r=setInterval(l,25),r&&"function"==typeof r.unref&&r.unref(),l()}getBufferedAmount(e,t=null){const i=this._connectionFor(e);if(!i)return null;const n=t?this.getChannel(e,t):i.dataChannel;if(!n||"number"!=typeof n.bufferedAmount)return null;const s=n.bufferedAmount;if("function"==typeof n._sdkObserveBufferedAmount)try{n._sdkObserveBufferedAmount()}catch(e){}return s}getMaxMessageSize(e){const t=this._connectionFor(e);if(!t||!t.pc)return null;const i=t.pc.sctp;return i&&"number"==typeof i.maxMessageSize?!isFinite(i.maxMessageSize)||i.maxMessageSize<=0?null:i.maxMessageSize:null}async sendBinary(e,t,i={}){const n=this._toUint8Array(e);if(!n)throw new Error("sendBinary requires an ArrayBuffer or typed array");if(!t)throw new Error("sendBinary requires a target uuid");const s=await this.openChannel(t,"x-bin",{ordered:i.ordered,maxRetransmits:i.maxRetransmits,maxPacketLifeTime:i.maxPacketLifeTime,protocol:i.protocol,timeout:i.timeout});if("open"!==s.readyState)return!1;if(!1!==i.waitForDrain&&(await this._waitForChannelDrain(s,1048576),"open"!==s.readyState))return!1;if(s.send(n),"function"==typeof s._sdkObserveBufferedAmount)try{s._sdkObserveBufferedAmount()}catch(e){}return!0}_setupBinaryChannel(e,t){this._log(`Binary channel open from ${e.uuid}`),t.onmessage=t=>{const i=this._toUint8Array(t.data);i?this._emit("binaryReceived",{uuid:e.uuid,streamID:e.streamID,bytes:i,data:i}):this._emit("binaryReceived",{uuid:e.uuid,streamID:e.streamID,data:t.data,bytes:null})},t.onerror=t=>{this._log(`Binary channel error from ${e.uuid}:`,t)}}_setupAuxiliaryChannel(e,t,i){try{t.binaryType="arraybuffer"}catch(e){}return this._isReservedChannelLabel(i)?(this._attachBufferedAmountLow(e,t,i),"x-bin"===i?void this._setupBinaryChannel(e,t):(this._log(`Reserved channel "${i}" from ${e.uuid}`),void this._emit("channelOpen",{uuid:e.uuid,streamID:e.streamID,label:i,channel:t}))):"chunked"===i?(this._log(`Chunked media channel from ${e.uuid} is not supported yet; ignoring`),void this._emit("unsupportedChannel",{uuid:e.uuid,streamID:e.streamID,label:i})):void("resources"!==i?this._setupFileReceiveChannel(e,t,i):this._setupResourceReceiveChannel(e,t))}_normalizeFileSource(e){if("undefined"!=typeof Blob&&e instanceof Blob)return{size:e.size,read:async(t,i)=>{const n=e.slice(t,i);return"function"==typeof n.arrayBuffer?await n.arrayBuffer():await new Promise((e,t)=>{const i=new FileReader;i.onload=()=>e(i.result),i.onerror=()=>t(i.error),i.readAsArrayBuffer(n)})}};if(this._isArrayBuffer(e))return{size:e.byteLength,read:async(t,i)=>e.slice(t,i)};if(ArrayBuffer.isView(e)){const t=new Uint8Array(e.buffer,e.byteOffset,e.byteLength);return{size:t.byteLength,read:async(e,i)=>t.slice(e,i)}}throw new Error("hostFile requires a Blob, File, ArrayBuffer, or typed array")}hostFile(e,t={}){const i=this._normalizeFileSource(e),n=t.name||(e&&"string"==typeof e.name?e.name:null);if(!n)throw new Error("hostFile requires a name when the source is not a File");if(this._hostedFiles||(this._hostedFiles=new Map),this._isReservedChannelLabel(t.id))throw new Error(`File ID "${t.id}" uses the reserved "x-" prefix`);let s=t.id||this._generateStreamID();for(;"sendChannel"===s||"chunked"===s||"resources"===s||this._isReservedChannelLabel(s)||this._hostedFiles.has(s);)s=this._generateStreamID();const o={id:s,name:n,size:i.size,read:i.read,restricted:"string"==typeof t.restricted&&t.restricted};return this._hostedFiles.set(s,o),this._log(`Hosting file "${n}" (${i.size} bytes) as ${s}`),this._advertiseFiles([o]),{id:s,name:n,size:i.size}}unhostFile(e){if(!this._hostedFiles||!this._hostedFiles.has(e))return!1;if(this._hostedFiles.delete(e),this._outboundTransfers)for(const t of this._outboundTransfers.values())t.fileId===e&&(t.cancelled=!0);return!0}getHostedFiles(){return this._hostedFiles?Array.from(this._hostedFiles.values()).map(e=>({id:e.id,name:e.name,size:e.size,restricted:e.restricted})):[]}_fileListFor(e){if(!this._hostedFiles||!this._hostedFiles.size)return[];const t=[];for(const i of this._hostedFiles.values())!1!==i.restricted&&i.restricted!==e||t.push({id:i.id,name:i.name,size:i.size});return t}_sendFileEntries(e,t){if(!e||!0!==e.allowDownloads||!e.dataChannel||"open"!==e.dataChannel.readyState)return!1;if(!t||!t.length)return!1;e._advertisedFileIds||(e._advertisedFileIds=new Set);const i=t.filter(t=>!e._advertisedFileIds.has(t.id));if(!i.length)return!1;try{e.dataChannel.send(JSON.stringify({fileList:i}));for(const t of i)e._advertisedFileIds.add(t.id);return this._log(`Sent file list (${i.length}) to ${e.uuid}`),!0}catch(e){return this._log("Failed to send file list:",e.message||e),!1}}_sendFileList(e,t=null){const i=this._fileListFor(e);if(!i.length)return!1;const n=t||this.connections.get(e)&&this.connections.get(e).publisher||null;return this._sendFileEntries(n,i)}_advertiseFiles(e){if(this.connections&&e&&e.length)for(const[t,i]of this.connections){const n=i&&i.publisher;if(!n||!0!==n.allowDownloads)continue;const s=e.filter(e=>!1===e.restricted||e.restricted===t).map(e=>({id:e.id,name:e.name,size:e.size}));s.length&&this._sendFileEntries(n,s)}}requestFile(e,t,i={}){if(!e||!t)return Promise.reject(new Error("requestFile requires a uuid and a fileId"));this._inboundTransfers||(this._inboundTransfers=new Map);const n=`${e}:${t}`,s=this._inboundTransfers.get(n);if(s)return s.promise;const o=!0===i.stream,r="number"==typeof i.timeout?i.timeout:3e4,a={uuid:e,fileId:t,stream:o,chunks:[],received:0,details:null,started:!1};a.promise=new Promise((e,t)=>{a.resolve=e,a.reject=t}),a.startTimer=setTimeout(()=>{a.started||(this._inboundTransfers.delete(n),a.reject(new Error(`Peer ${e} did not start transfer of ${t}`)))},r),a.startTimer&&"function"==typeof a.startTimer.unref&&a.startTimer.unref(),this._inboundTransfers.set(n,a);try{this._sendDataInternal({requestFile:t},e,null,"any"),this._log(`Requested file ${t} from ${e}`)}catch(e){clearTimeout(a.startTimer),this._inboundTransfers.delete(n),a.reject(e)}return a.promise}async _handleFileRequest(e,t){const i=this._hostedFiles&&this._hostedFiles.get(t);if(!i)return void this._log(`Peer ${e.uuid} requested unknown file ${t}`);if(!1!==i.restricted&&i.restricted!==e.uuid)return void this._log(`Peer ${e.uuid} is not permitted to read ${t}`);if(!e.pc)return;this._outboundTransfers||(this._outboundTransfers=new Map);const n=`${e.uuid}:${t}`;if(this._outboundTransfers.has(n))return void this._log(`Transfer of ${t} to ${e.uuid} is already running`);let s;try{s=e.pc.createDataChannel(t,{ordered:!0}),this._log(`Opened file transfer channel "${t}" to ${e.uuid}, state: ${s.readyState}`)}catch(e){return void this._log("Failed to open file transfer channel:",e.message||e)}s.binaryType="arraybuffer",this._trackChannel(e,s);const o={fileId:t,uuid:e.uuid,cancelled:!1,channel:s};this._outboundTransfers.set(n,o);const r=()=>{this._outboundTransfers.delete(n)};s.onclose=r,s.onerror=e=>{this._log(`File transfer channel error for ${t}:`,e),r()};const a=async()=>{try{s.send(JSON.stringify({type:"filetransfer",size:i.size,filename:i.name,id:i.id})),await this._streamFileChunks(s,i,o,e)}catch(i){this._log(`File transfer of ${t} failed:`,i.message||i),this._emit("fileTransferError",{uuid:e.uuid,streamID:e.streamID,id:t,direction:"outbound",error:i});try{s.close()}catch(e){}r()}};"open"===s.readyState?a():s.onopen=a}async _streamFileChunks(e,t,i,n){let s=0;for(;s<t.size;){if(i.cancelled){try{e.send("EOF2")}catch(e){}try{e.close()}catch(e){}return void this._emit("fileTransferCancelled",{uuid:i.uuid,id:t.id,direction:"outbound",bytesSent:s})}if("open"!==e.readyState)return void this._log(`File transfer channel for ${t.id} closed mid-transfer`);if(await this._waitForChannelDrain(e),"open"!==e.readyState)return;const o=Math.min(s+16384,t.size),r=await t.read(s,o);e.send(r),s=o,th