simple-peer-light
Version:
Simple, light-weight WebRTC video/voice and data channels
1 lines • 18.1 kB
JavaScript
/*! simple-peer. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */const MAX_BUFFERED_AMOUNT=65536,ICECOMPLETE_TIMEOUT=5000,CHANNEL_CLOSING_TIMEOUT=5000;function randombytes(a){const b=new Uint8Array(a);for(let c=0;c<a;c++)b[c]=0|256*Math.random();return b}function getBrowserRTC(){if("undefined"==typeof globalThis)return null;const a={RTCPeerConnection:globalThis.RTCPeerConnection||globalThis.mozRTCPeerConnection||globalThis.webkitRTCPeerConnection,RTCSessionDescription:globalThis.RTCSessionDescription||globalThis.mozRTCSessionDescription||globalThis.webkitRTCSessionDescription,RTCIceCandidate:globalThis.RTCIceCandidate||globalThis.mozRTCIceCandidate||globalThis.webkitRTCIceCandidate};return a.RTCPeerConnection?a:null}function errCode(a,b){return Object.defineProperty(a,"code",{value:b,enumerable:!0,configurable:!0}),a}function filterTrickle(a){return a.replace(/a=ice-options:trickle\s\n/g,"")}function warn(a){console.warn(a)}class Peer{constructor(a={}){if(this._map=new Map,this._id=randombytes(4).toString("hex").slice(0,7),this._doDebug=a.debug,this._debug("new peer %o",a),this.channelName=a.initiator?a.channelName||randombytes(20).toString("hex"):null,this.initiator=a.initiator||!1,this.channelConfig=a.channelConfig||Peer.channelConfig,this.channelNegotiated=this.channelConfig.negotiated,this.config=Object.assign({},Peer.config,a.config),this.offerOptions=a.offerOptions||{},this.answerOptions=a.answerOptions||{},this.sdpTransform=a.sdpTransform||(a=>a),this.streams=a.streams||(a.stream?[a.stream]:[]),this.trickle=void 0===a.trickle||a.trickle,this.allowHalfTrickle=void 0!==a.allowHalfTrickle&&a.allowHalfTrickle,this.iceCompleteTimeout=a.iceCompleteTimeout||ICECOMPLETE_TIMEOUT,this.destroyed=!1,this.destroying=!1,this._connected=!1,this.remoteAddress=void 0,this.remoteFamily=void 0,this.remotePort=void 0,this.localAddress=void 0,this.localFamily=void 0,this.localPort=void 0,this._wrtc=a.wrtc&&"object"==typeof a.wrtc?a.wrtc:getBrowserRTC(),!this._wrtc)if("undefined"==typeof window)throw errCode(new Error("No WebRTC support: Specify `opts.wrtc` option in this environment"),"ERR_WEBRTC_SUPPORT");else throw errCode(new Error("No WebRTC support: Not a supported browser"),"ERR_WEBRTC_SUPPORT");this._pcReady=!1,this._channelReady=!1,this._iceComplete=!1,this._iceCompleteTimer=null,this._channel=null,this._pendingCandidates=[],this._isNegotiating=!1,this._firstNegotiation=!0,this._batchedNegotiation=!1,this._queuedNegotiation=!1,this._sendersAwaitingStable=[],this._senderMap=new Map,this._closingInterval=null,this._remoteTracks=[],this._remoteStreams=[],this._chunk=null,this._cb=null,this._interval=null;try{this._pc=new this._wrtc.RTCPeerConnection(this.config)}catch(a){return void this.destroy(errCode(a,"ERR_PC_CONSTRUCTOR"))}this._isReactNativeWebrtc="number"==typeof this._pc._peerConnectionId,this._pc.oniceconnectionstatechange=()=>{this._onIceStateChange()},this._pc.onicegatheringstatechange=()=>{this._onIceStateChange()},this._pc.onconnectionstatechange=()=>{this._onConnectionStateChange()},this._pc.onsignalingstatechange=()=>{this._onSignalingStateChange()},this._pc.onicecandidate=a=>{this._onIceCandidate(a)},"object"==typeof this._pc.peerIdentity&&this._pc.peerIdentity.catch(a=>{this.destroy(errCode(a,"ERR_PC_PEER_IDENTITY"))}),this.initiator||this.channelNegotiated?this._setupData({channel:this._pc.createDataChannel(this.channelName,this.channelConfig)}):this._pc.ondatachannel=a=>{this._setupData(a)},this.streams&&this.streams.forEach(a=>{this.addStream(a)}),this._pc.ontrack=a=>{this._onTrack(a)},this._debug("initial negotiation"),this._needsNegotiation()}get bufferSize(){return this._channel&&this._channel.bufferedAmount||0}get connected(){return this._connected&&"open"===this._channel.readyState}address(){return{port:this.localPort,family:this.localFamily,address:this.localAddress}}signal(a){if(!this.destroying){if(this.destroyed)throw errCode(new Error("cannot signal after peer is destroyed"),"ERR_DESTROYED");if("string"==typeof a)try{a=JSON.parse(a)}catch(b){a={}}this._debug("signal()"),a.renegotiate&&this.initiator&&(this._debug("got request to renegotiate"),this._needsNegotiation()),a.transceiverRequest&&this.initiator&&(this._debug("got request for transceiver"),this.addTransceiver(a.transceiverRequest.kind,a.transceiverRequest.init)),a.candidate&&(this._pc.remoteDescription&&this._pc.remoteDescription.type?this._addIceCandidate(a.candidate):this._pendingCandidates.push(a.candidate)),a.sdp&&this._pc.setRemoteDescription(new this._wrtc.RTCSessionDescription(a)).then(()=>{this.destroyed||(this._pendingCandidates.forEach(a=>{this._addIceCandidate(a)}),this._pendingCandidates=[],"offer"===this._pc.remoteDescription.type&&this._createAnswer())}).catch(a=>{this.destroy(errCode(a,"ERR_SET_REMOTE_DESCRIPTION"))}),a.sdp||a.candidate||a.renegotiate||a.transceiverRequest||this.destroy(errCode(new Error("signal() called with invalid signal data"),"ERR_SIGNALING"))}}_addIceCandidate(a){const b=new this._wrtc.RTCIceCandidate(a);this._pc.addIceCandidate(b).catch(a=>{!b.address||b.address.endsWith(".local")?warn("Ignoring unsupported ICE candidate."):this.destroy(errCode(a,"ERR_ADD_ICE_CANDIDATE"))})}send(a){if(!this.destroying){if(this.destroyed)throw errCode(new Error("cannot send after peer is destroyed"),"ERR_DESTROYED");this._channel.send(a)}}addTransceiver(a,b){if(!this.destroying){if(this.destroyed)throw errCode(new Error("cannot addTransceiver after peer is destroyed"),"ERR_DESTROYED");if(this._debug("addTransceiver()"),this.initiator)try{this._pc.addTransceiver(a,b),this._needsNegotiation()}catch(a){this.destroy(errCode(a,"ERR_ADD_TRANSCEIVER"))}else this.emit("signal",{type:"transceiverRequest",transceiverRequest:{kind:a,init:b}})}}addStream(a){if(!this.destroying){if(this.destroyed)throw errCode(new Error("cannot addStream after peer is destroyed"),"ERR_DESTROYED");this._debug("addStream()"),a.getTracks().forEach(b=>{this.addTrack(b,a)})}}addTrack(a,b){if(this.destroying)return;if(this.destroyed)throw errCode(new Error("cannot addTrack after peer is destroyed"),"ERR_DESTROYED");this._debug("addTrack()");const c=this._senderMap.get(a)||new Map;let d=c.get(b);if(!d)d=this._pc.addTrack(a,b),c.set(b,d),this._senderMap.set(a,c),this._needsNegotiation();else if(d.removed)throw errCode(new Error("Track has been removed. You should enable/disable tracks that you want to re-add."),"ERR_SENDER_REMOVED");else throw errCode(new Error("Track has already been added to that stream."),"ERR_SENDER_ALREADY_ADDED")}replaceTrack(a,b,c){if(this.destroying)return;if(this.destroyed)throw errCode(new Error("cannot replaceTrack after peer is destroyed"),"ERR_DESTROYED");this._debug("replaceTrack()");const d=this._senderMap.get(a),e=d?d.get(c):null;if(!e)throw errCode(new Error("Cannot replace track that was never added."),"ERR_TRACK_NOT_ADDED");b&&this._senderMap.set(b,d),null==e.replaceTrack?this.destroy(errCode(new Error("replaceTrack is not supported in this browser"),"ERR_UNSUPPORTED_REPLACETRACK")):e.replaceTrack(b)}removeTrack(a,b){if(this.destroying)return;if(this.destroyed)throw errCode(new Error("cannot removeTrack after peer is destroyed"),"ERR_DESTROYED");this._debug("removeSender()");const c=this._senderMap.get(a),d=c?c.get(b):null;if(!d)throw errCode(new Error("Cannot remove track that was never added."),"ERR_TRACK_NOT_ADDED");try{d.removed=!0,this._pc.removeTrack(d)}catch(a){"NS_ERROR_UNEXPECTED"===a.name?this._sendersAwaitingStable.push(d):this.destroy(errCode(a,"ERR_REMOVE_TRACK"))}this._needsNegotiation()}removeStream(a){if(!this.destroying){if(this.destroyed)throw errCode(new Error("cannot removeStream after peer is destroyed"),"ERR_DESTROYED");this._debug("removeSenders()"),a.getTracks().forEach(b=>{this.removeTrack(b,a)})}}_needsNegotiation(){this._debug("_needsNegotiation");this._batchedNegotiation||(this._batchedNegotiation=!0,queueMicrotask(()=>{this._batchedNegotiation=!1,this.initiator||!this._firstNegotiation?(this._debug("starting batched negotiation"),this.negotiate()):this._debug("non-initiator initial negotiation request discarded"),this._firstNegotiation=!1}))}negotiate(){if(!this.destroying){if(this.destroyed)throw errCode(new Error("cannot negotiate after peer is destroyed"),"ERR_DESTROYED");this.initiator?this._isNegotiating?(this._queuedNegotiation=!0,this._debug("already negotiating, queueing")):(this._debug("start negotiation"),setTimeout(()=>{this._createOffer()},0)):this._isNegotiating?(this._queuedNegotiation=!0,this._debug("already negotiating, queueing")):(this._debug("requesting negotiation from initiator"),this.emit("signal",{type:"renegotiate",renegotiate:!0})),this._isNegotiating=!0}}destroy(a){this.destroyed||this.destroying||(this.destroying=!0,this._debug("destroying (error: %s)",a&&(a.message||a)),queueMicrotask(()=>{if(this.destroyed=!0,this.destroying=!1,this._debug("destroy (error: %s)",a&&(a.message||a)),this._connected=!1,this._pcReady=!1,this._channelReady=!1,this._remoteTracks=null,this._remoteStreams=null,this._senderMap=null,clearInterval(this._closingInterval),this._closingInterval=null,clearInterval(this._interval),this._interval=null,this._chunk=null,this._cb=null,this._channel){try{this._channel.close()}catch(a){}this._channel.onmessage=null,this._channel.onopen=null,this._channel.onclose=null,this._channel.onerror=null}if(this._pc){try{this._pc.close()}catch(a){}this._pc.oniceconnectionstatechange=null,this._pc.onicegatheringstatechange=null,this._pc.onsignalingstatechange=null,this._pc.onicecandidate=null,this._pc.ontrack=null,this._pc.ondatachannel=null}this._pc=null,this._channel=null,a&&this.emit("error",a),this.emit("close")}))}_setupData(a){if(!a.channel)return this.destroy(errCode(new Error("Data channel event is missing `channel` property"),"ERR_DATA_CHANNEL"));this._channel=a.channel,this._channel.binaryType="arraybuffer","number"==typeof this._channel.bufferedAmountLowThreshold&&(this._channel.bufferedAmountLowThreshold=MAX_BUFFERED_AMOUNT),this.channelName=this._channel.label,this._channel.onmessage=a=>{this._onChannelMessage(a)},this._channel.onbufferedamountlow=()=>{this._onChannelBufferedAmountLow()},this._channel.onopen=()=>{this._onChannelOpen()},this._channel.onclose=()=>{this._onChannelClose()},this._channel.onerror=a=>{this.destroy(errCode(a,"ERR_DATA_CHANNEL"))};let b=!1;this._closingInterval=setInterval(()=>{this._channel&&"closing"===this._channel.readyState?(b&&this._onChannelClose(),b=!0):b=!1},CHANNEL_CLOSING_TIMEOUT)}_startIceCompleteTimeout(){this.destroyed||this._iceCompleteTimer||(this._debug("started iceComplete timeout"),this._iceCompleteTimer=setTimeout(()=>{this._iceComplete||(this._iceComplete=!0,this._debug("iceComplete timeout completed"),this.emit("iceTimeout"),this.emit("_iceComplete"))},this.iceCompleteTimeout))}_createOffer(){this.destroyed||this._pc.createOffer(this.offerOptions).then(a=>{if(this.destroyed)return;this.trickle||this.allowHalfTrickle||(a.sdp=filterTrickle(a.sdp)),a.sdp=this.sdpTransform(a.sdp);const b=()=>{if(!this.destroyed){const b=this._pc.localDescription||a;this._debug("signal"),this.emit("signal",{type:b.type,sdp:b.sdp})}};this._pc.setLocalDescription(a).then(()=>{this._debug("createOffer success");this.destroyed||(this.trickle||this._iceComplete?b():this.once("_iceComplete",b))}).catch(a=>{this.destroy(errCode(a,"ERR_SET_LOCAL_DESCRIPTION"))})}).catch(a=>{this.destroy(errCode(a,"ERR_CREATE_OFFER"))})}_requestMissingTransceivers(){this._pc.getTransceivers&&this._pc.getTransceivers().forEach(a=>{a.mid||!a.sender.track||a.requested||(a.requested=!0,this.addTransceiver(a.sender.track.kind))})}_createAnswer(){this.destroyed||this._pc.createAnswer(this.answerOptions).then(a=>{if(this.destroyed)return;this.trickle||this.allowHalfTrickle||(a.sdp=filterTrickle(a.sdp)),a.sdp=this.sdpTransform(a.sdp);const b=()=>{if(!this.destroyed){const b=this._pc.localDescription||a;this._debug("signal"),this.emit("signal",{type:b.type,sdp:b.sdp}),this.initiator||this._requestMissingTransceivers()}};this._pc.setLocalDescription(a).then(()=>{this.destroyed||(this.trickle||this._iceComplete?b():this.once("_iceComplete",b))}).catch(a=>{this.destroy(errCode(a,"ERR_SET_LOCAL_DESCRIPTION"))})}).catch(a=>{this.destroy(errCode(a,"ERR_CREATE_ANSWER"))})}_onConnectionStateChange(){this.destroyed||"failed"===this._pc.connectionState&&this.destroy(errCode(new Error("Connection failed."),"ERR_CONNECTION_FAILURE"))}_onIceStateChange(){if(this.destroyed)return;const a=this._pc.iceConnectionState,b=this._pc.iceGatheringState;this._debug("iceStateChange (connection: %s) (gathering: %s)",a,b),this.emit("iceStateChange",a,b),("connected"===a||"completed"===a)&&(this._pcReady=!0,this._maybeReady()),"failed"===a&&this.destroy(errCode(new Error("Ice connection failed."),"ERR_ICE_CONNECTION_FAILURE")),"closed"===a&&this.destroy(errCode(new Error("Ice connection closed."),"ERR_ICE_CONNECTION_CLOSED"))}getStats(a){const b=a=>("[object Array]"===Object.prototype.toString.call(a.values)&&a.values.forEach(b=>{Object.assign(a,b)}),a);0===this._pc.getStats.length||this._isReactNativeWebrtc?this._pc.getStats().then(c=>{const d=[];c.forEach(a=>{d.push(b(a))}),a(null,d)},b=>a(b)):0<this._pc.getStats.length?this._pc.getStats(c=>{if(this.destroyed)return;const d=[];c.result().forEach(a=>{const c={};a.names().forEach(b=>{c[b]=a.stat(b)}),c.id=a.id,c.type=a.type,c.timestamp=a.timestamp,d.push(b(c))}),a(null,d)},b=>a(b)):a(null,[])}_maybeReady(){if(this._debug("maybeReady pc %s channel %s",this._pcReady,this._channelReady),this._connected||this._connecting||!this._pcReady||!this._channelReady)return;this._connecting=!0;const a=()=>{this.destroyed||this.getStats((b,c)=>{if(this.destroyed)return;b&&(c=[]);const d={},e={},f={};let g=!1;c.forEach(a=>{("remotecandidate"===a.type||"remote-candidate"===a.type)&&(d[a.id]=a),("localcandidate"===a.type||"local-candidate"===a.type)&&(e[a.id]=a),("candidatepair"===a.type||"candidate-pair"===a.type)&&(f[a.id]=a)});const h=a=>{g=!0;let b=e[a.localCandidateId];b&&(b.ip||b.address)?(this.localAddress=b.ip||b.address,this.localPort=+b.port):b&&b.ipAddress?(this.localAddress=b.ipAddress,this.localPort=+b.portNumber):"string"==typeof a.googLocalAddress&&(b=a.googLocalAddress.split(":"),this.localAddress=b[0],this.localPort=+b[1]),this.localAddress&&(this.localFamily=this.localAddress.includes(":")?"IPv6":"IPv4");let c=d[a.remoteCandidateId];c&&(c.ip||c.address)?(this.remoteAddress=c.ip||c.address,this.remotePort=+c.port):c&&c.ipAddress?(this.remoteAddress=c.ipAddress,this.remotePort=+c.portNumber):"string"==typeof a.googRemoteAddress&&(c=a.googRemoteAddress.split(":"),this.remoteAddress=c[0],this.remotePort=+c[1]),this.remoteAddress&&(this.remoteFamily=this.remoteAddress.includes(":")?"IPv6":"IPv4"),this._debug("connect local: %s:%s remote: %s:%s",this.localAddress,this.localPort,this.remoteAddress,this.remotePort)};if(c.forEach(a=>{"transport"===a.type&&a.selectedCandidatePairId&&h(f[a.selectedCandidatePairId]),("googCandidatePair"===a.type&&"true"===a.googActiveConnection||("candidatepair"===a.type||"candidate-pair"===a.type)&&a.selected)&&h(a)}),!g&&(!Object.keys(f).length||Object.keys(e).length))return void setTimeout(a,100);if(this._connecting=!1,this._connected=!0,this._chunk){try{this.send(this._chunk)}catch(a){return this.destroy(errCode(a,"ERR_DATA_CHANNEL"))}this._chunk=null,this._debug("sent chunk from \"write before connect\"");const a=this._cb;this._cb=null,a(null)}"number"!=typeof this._channel.bufferedAmountLowThreshold&&(this._interval=setInterval(()=>this._onInterval(),150),this._interval.unref&&this._interval.unref()),this._debug("connect"),this.emit("connect")})};a()}_onInterval(){this._cb&&this._channel&&!(this._channel.bufferedAmount>MAX_BUFFERED_AMOUNT)&&this._onChannelBufferedAmountLow()}_onSignalingStateChange(){this.destroyed||("stable"===this._pc.signalingState&&(this._isNegotiating=!1,this._debug("flushing sender queue",this._sendersAwaitingStable),this._sendersAwaitingStable.forEach(a=>{this._pc.removeTrack(a),this._queuedNegotiation=!0}),this._sendersAwaitingStable=[],this._queuedNegotiation?(this._debug("flushing negotiation queue"),this._queuedNegotiation=!1,this._needsNegotiation()):(this._debug("negotiated"),this.emit("negotiated"))),this._debug("signalingStateChange %s",this._pc.signalingState),this.emit("signalingStateChange",this._pc.signalingState))}_onIceCandidate(a){this.destroyed||(a.candidate&&this.trickle?this.emit("signal",{type:"candidate",candidate:{candidate:a.candidate.candidate,sdpMLineIndex:a.candidate.sdpMLineIndex,sdpMid:a.candidate.sdpMid}}):!a.candidate&&!this._iceComplete&&(this._iceComplete=!0,this.emit("_iceComplete")),a.candidate&&this._startIceCompleteTimeout())}_onChannelMessage(a){if(this.destroyed)return;let b=a.data;b instanceof ArrayBuffer&&(b=new Uint8Array(b)),this.emit("data",b)}_onChannelBufferedAmountLow(){if(!this.destroyed&&this._cb){this._debug("ending backpressure: bufferedAmount %d",this._channel.bufferedAmount);const a=this._cb;this._cb=null,a(null)}}_onChannelOpen(){this._connected||this.destroyed||(this._debug("on channel open"),this._channelReady=!0,this._maybeReady())}_onChannelClose(){this.destroyed||(this._debug("on channel close"),this.destroy())}_onTrack(a){this.destroyed||a.streams.forEach(b=>{this._debug("on track"),this.emit("track",a.track,b),this._remoteTracks.push({track:a.track,stream:b});this._remoteStreams.some(a=>a.id===b.id)||(this._remoteStreams.push(b),queueMicrotask(()=>{this._debug("on stream"),this.emit("stream",b)}))})}_debug(...a){this._doDebug&&(a[0]="["+this._id+"] "+a[0],console.log(...a))}on(a,b){const c=this._map;c.has(a)||c.set(a,new Set),c.get(a).add(b)}off(a,b){const c=this._map,d=c.get(a);d&&(d.delete(b),0===d.size&&c.delete(a))}once(a,b){const c=(...d)=>{this.off(a,c),b(...d)};this.on(a,c)}emit(a,...b){const c=this._map;if(c.has(a))for(const d of c.get(a))try{d(...b)}catch(a){console.error(a)}}}Peer.WEBRTC_SUPPORT=!!getBrowserRTC(),Peer.config={iceServers:[{urls:["stun:stun.l.google.com:19302","stun:global.stun.twilio.com:3478"]}],sdpSemantics:"unified-plan"},Peer.channelConfig={};export default Peer;