import EventEmitter from 'eventemitter3' ; import shortid from 'shortid' ; import StrictEventEmitter from 'strict-event-emitter-types' ; declare global { interface Window { RTCIceCandidate : typeof RTCIceCandidate ; RTCPeerConnection : typeof RTCPeerConnection ; RTCSessionDescription : typeof RTCSessionDescription ; } interface RTCConfiguration { sdpSemantics ? : 'plan-b' | 'unified-plan' | string | undefined ; } interface RTCDataChannel { send ( data ) : void ; } interface RTCRtpTransceiver { name ? : string ; } } export type PayloadToServer = CandidatePayload | OfferPayload | AnswerPayload ; export interface FastRTCPeerEvents { signal : ( payload : PayloadToServer , peer : FastRTCPeer ) => void ; data : ( data : DataPayload , peer : FastRTCPeer ) => void ; open : ( peer : FastRTCPeer ) => void ; close : ( peer : FastRTCPeer ) => void ; error : ( error : Error , peer : FastRTCPeer ) => void ; stream : ( stream : MediaStream , name : string , peer : FastRTCPeer ) => void ; connection : ( state : RTCIceConnectionState , peer : FastRTCPeer ) => void ; } export type DataPayload = string | Blob | ArrayBuffer | ArrayBufferView ; export interface PeerConfig { readonly id ? : string ; readonly isOfferer ? : boolean ; readonly userId ? : string ; readonly streams ? : StreamDictInput ; readonly wrtc ? : WRTC ; readonly rtcConfig ? : RTCConfiguration ; } export interface WRTC { RTCIceCandidate : typeof RTCIceCandidate ; RTCPeerConnection : typeof RTCPeerConnection ; RTCSessionDescription : typeof RTCSessionDescription ; } export interface OfferPayload { readonly type : 'offer' ; readonly sdp : string ; } export interface CandidatePayload { readonly type : 'candidate' ; readonly candidate : RTCIceCandidateInit | null ; } export interface AnswerPayload { readonly type : 'answer' ; readonly sdp : string ; } export type PayloadFromServer = OfferPayload | CandidatePayload | AnswerPayload ; export type FastRTCPeerEmitter = { new ( ) : StrictEventEmitter < EventEmitter , FastRTCPeerEvents > ; } ; export type TrackKind = 'audio' | 'video' ; interface TrackConfig { readonly track : MediaStreamTrack ; readonly setParameters : ( sender : RTCRtpSender ) => Promise < void > ; } type TrackConfigOrKind = TrackConfig | TrackKind ; interface TrackDict { [ trackName ] : TrackConfigOrKind | null ; } export interface StreamDict { [ streamName ] : TrackDict ; } export interface StreamDictInput { [ streamName ] : TrackDict | MediaStream | undefined ; } interface RemoteStreams { [ streamName ] : MediaStream ; } interface StreamConfigEntry { readonly streamName : string ; readonly transceiverName : string ; trackConfigOrKind : TrackConfigOrKind | null ; } interface ClosePayload { readonly type : 'close' ; } interface MidNamePayload { readonly type : 'midOffer' ; readonly transceiverName : string ; readonly mid : string ; } interface RequestTransceiverPayload { readonly type : 'transceiverRequest' ; readonly transceiverName : string ; readonly kind : TrackKind ; } interface StreamPayload { readonly type : 'stream' ; readonly streamName : string ; readonly transceiverNames : string [ ] ; } type InternalPayload = ClosePayload | MidNamePayload | RequestTransceiverPayload | StreamPayload ; const replyWithTrack = async ( transceiver , trackConfigOrKind ) => { if ( trackConfigOrKind && typeof trackConfigOrKind !== 'string' ) { const { track , setParameters } = trackConfigOrKind ; transceiver . direction = 'sendrecv' ; if ( setParameters ) { await setParameters ( transceiver . sender ) ; } await transceiver . sender . replaceTrack ( track ) ; } } ; class FastRTCPeer extends ( EventEmitter as FastRTCPeerEmitter ) { static defaultICEServers : Array < RTCIceServer > = [ { urls : 'stun:stun.l.google.com:19302' } , { urls : 'stun:global.stun.twilio.com:3478?transport=udp' } ] ; static defaultConfig = { iceServers : FastRTCPeer . defaultICEServers , sdpSemantics : 'unified-plan' } ; static generateID = ( ) => { return shortid . generate ( ) ; } ; static fromStreamShorthand = ( streams ) => { const returnStreams = { } as StreamDict ; if ( streams ) { Object . keys ( streams ) . forEach ( ( streamName ) => { const streamOrConfig = streams [ streamName ] ; returnStreams [ streamName ] = streamOrConfig instanceof MediaStream ? { audio : { track : streamOrConfig . getAudioTracks ( ) [ 0 ] } as TrackConfig , video : { track : streamOrConfig . getVideoTracks ( ) [ 0 ] } as TrackConfig } : streamOrConfig || { } ; } ) ; } return returnStreams ; } ; private readonly dataChannelQueue : InternalPayload [ ] = [ ] ; private readonly isOfferer : boolean ; private readonly wrtc : WRTC | Window ; readonly remoteStreams : RemoteStreams = { } ; private readonly streamConfig : StreamConfigEntry [ ] = [ ] ; private readonly midsWithoutNames = new Set < string > ( ) ; private readonly pendingTransceivers : Array < { transceiver : RTCRtpTransceiver ; transceiverName : string ; } > = [ ] ; private negotiationCount = 0 ; private midLookup : { [ transceiverName ] : string ; } = { } ; private dataChannel : RTCDataChannel | null = null ; readonly id : string ; readonly peerConnection : RTCPeerConnection ; userId : string | null ; constructor ( userConfig = { } ) { super ( ) ; const { id , isOfferer , userId , streams , wrtc , rtcConfig = { } } = userConfig ; this . id = id || FastRTCPeer . generateID ( ) ; this . isOfferer = isOfferer || false ; this . userId = userId || null ; this . wrtc = wrtc || window ; const { RTCPeerConnection } = this . wrtc ; if ( ! RTCPeerConnection ) throw new Error ( 'Client does not support WebRTC' ) ; this . peerConnection = new RTCPeerConnection ( { ... FastRTCPeer . defaultConfig , ... rtcConfig } ) ; this . setupPeer ( ) ; this . addStreams ( FastRTCPeer . fromStreamShorthand ( streams ) ) ; } private setupPeer ( ) { if ( ! this . peerConnection ) return ; this . peerConnection . onicecandidate = this . onIceCandidate ; this . peerConnection . oniceconnectionstatechange = this . onIceConnectionStateChange ; this . peerConnection . onnegotiationneeded = this . onNegotiationNeeded ; this . peerConnection . ontrack = this . onTrack ; this . addDataChannel ( 'fast' ) ; } private onIceCandidate = ( event ) => { const payload = { type : 'candidate' , candidate : event . candidate } as CandidatePayload ; this . emit ( 'signal' , payload , this ) ; } ; private onIceConnectionStateChange = ( ) => { const { iceConnectionState } = this . peerConnection ; switch ( iceConnectionState ) { case 'closed' : case 'failed' : this . close ( ) ; break ; } this . emit ( 'connection' , iceConnectionState , this ) ; } ; private onNegotiationNeeded = async ( ) => { const neg = ++ this . negotiationCount ; const offer = await this . peerConnection . createOffer ( ) ; if ( neg !== this . negotiationCount ) return ; await this . peerConnection . setLocalDescription ( offer ) . catch ( ( e ) => { this . emit ( 'error' , e , this ) ; } ) ; this . pendingTransceivers . slice ( ) . forEach ( ( { transceiver : { mid } , transceiverName } , idx ) => { if ( ! mid ) return ; this . midLookup [ transceiverName ] = mid ; this . sendInternal ( { type : 'midOffer' , mid , transceiverName } ) ; this . pendingTransceivers . splice ( idx , 1 ) ; } ) ; const { sdp , type } = offer ; this . emit ( 'signal' , { sdp , type } as OfferPayload , this ) ; } ; private addTrackToStream = ( transceiverName , track ) => { const streamNames = new Set ( this . streamConfig . filter ( ( config ) => config . transceiverName === transceiverName ) . map ( ( { streamName } ) => streamName ) ) ; streamNames . forEach ( ( streamName ) => { let stream = this . remoteStreams [ streamName ] ; if ( ! stream ) { stream = this . remoteStreams [ streamName ] = new MediaStream ( [ track ] ) ; } else { stream . addTrack ( track ) ; } const streamCount = this . streamConfig . filter ( ( config ) => config . streamName === streamName ) . length ; if ( streamCount === stream . getTracks ( ) . length ) { this . emit ( 'stream' , stream , streamName , this ) ; } } ) ; } ; private onTrack = async ( e ) => { const { track , transceiver } = e ; const transceiverName = Object . keys ( this . midLookup ) . find ( ( name ) => this . midLookup [ name ] === transceiver . mid ) ; if ( transceiverName ) { this . addTrackToStream ( transceiverName , track ) ; } else { if ( ! transceiver . mid ) throw new Error ( 'No mid in onTrack' ) ; this . midsWithoutNames . add ( transceiver . mid ) ; } } ; private handleAnswer ( initSDP ) { const desc = new this . wrtc . RTCSessionDescription ( initSDP ) as RTCSessionDescriptionInit ; this . peerConnection . setRemoteDescription ( desc ) . catch ( ( e ) => { this . emit ( 'error' , e , this ) ; } ) ; } private handleCandidate ( candidateObj ) { if ( ! candidateObj ) return ; const candidate = new this . wrtc . RTCIceCandidate ( candidateObj ) ; this . peerConnection . addIceCandidate ( candidate ) . catch ( ( e ) => { this . emit ( 'error' , e , this ) ; } ) ; } private handleOffer = async ( initSdp ) => { const remoteSdp = new this . wrtc . RTCSessionDescription ( initSdp ) ; await this . peerConnection . setRemoteDescription ( remoteSdp ) . catch ( ( e ) => { this . emit ( 'error' , e , this ) ; } ) ; const answer = await this . peerConnection . createAnswer ( ) ; const { sdp , type } = answer ; this . emit ( 'signal' , { type , sdp } as AnswerPayload , this ) ; await this . peerConnection . setLocalDescription ( answer ) . catch ( ( e ) => { this . emit ( 'error' , e , this ) ; } ) ; } ; private handleInternalMessage = ( data ) => { if ( typeof data !== 'string' || ! data . startsWith ( '@fast' ) ) return false ; const payload = JSON . parse ( data . substring ( 6 ) ) as InternalPayload ; switch ( payload . type ) { case 'close' : this . close ( ) ; break ; case 'midOffer' : this . midLookup [ payload . transceiverName ] = payload . mid ; if ( this . midsWithoutNames . has ( payload . mid ) ) { this . midsWithoutNames . delete ( payload . mid ) ; const transceiver = this . peerConnection . getTransceivers ( ) . find ( ( { mid } ) => mid === payload . mid ) ; if ( ! transceiver ) throw new Error ( `No transceiver exists with mid: ${ payload . mid }` ) ; this . addTrackToStream ( payload . transceiverName , transceiver . receiver . track ) ; const { trackConfigOrKind } = this . streamConfig . find ( ( { transceiverName } ) => transceiverName === payload . transceiverName ) ! ; replyWithTrack ( transceiver , trackConfigOrKind ) . catch ( ) ; } break ; case 'transceiverRequest' : if ( ! this . midLookup [ payload . transceiverName ] && ! this . pendingTransceivers . some ( ( { transceiverName } ) => transceiverName === payload . transceiverName ) ) { this . setupTransceiver ( payload . transceiverName , payload . kind ) ; } break ; case 'stream' : const { streamName , transceiverNames } = payload ; transceiverNames . forEach ( ( transceiverName ) => { const existingConfig = this . streamConfig . find ( ( config ) => config . streamName === streamName && config . transceiverName === transceiverName ) ; if ( ! existingConfig ) { this . streamConfig . push ( { streamName , transceiverName , trackConfigOrKind : null } ) ; } } ) ; break ; } return true ; } ; private sendInternal ( payload ) { if ( ! this . dataChannel || this . dataChannel . readyState !== 'open' ) { this . dataChannelQueue . push ( payload ) ; } else { try { this . dataChannel . send ( `@fast/${ JSON . stringify ( payload ) }` ) ; } catch ( e ) { this . dataChannelQueue . push ( payload ) ; } } } private setChannelEvents = ( channel ) => { channel . onmessage = ( event ) => { if ( ! this . handleInternalMessage ( event . data ) ) { this . emit ( 'data' , event . data , this ) ; } } ; channel . onopen = ( ) => { this . dataChannel = channel ; this . dataChannelQueue . forEach ( ( payload ) => this . sendInternal ( payload ) ) ; this . dataChannelQueue . length = 0 ; this . emit ( 'open' , this ) ; } ; channel . onclose = ( ) => { this . emit ( 'close' , this ) ; } ; } ; private addDataChannel ( label , dataChannelDict ? ) { if ( this . isOfferer ) { const dataChannel = this . peerConnection . createDataChannel ( label , dataChannelDict ) ; this . setChannelEvents ( dataChannel ) ; } else { this . peerConnection . ondatachannel = ( e ) => { this . peerConnection . ondatachannel = null ; this . setChannelEvents ( e . channel ) ; } ; } } private setupTransceiver ( transceiverName , defaultKind = 'video' ) { const { trackConfigOrKind } = this . streamConfig . find ( ( config ) => config . transceiverName === transceiverName ) ! ; const trackOrKind = typeof trackConfigOrKind === 'string' ? trackConfigOrKind : trackConfigOrKind ? trackConfigOrKind . track : defaultKind ; this . negotiationCount ++ ; const transceiver = this . peerConnection . addTransceiver ( trackOrKind ) ; this . pendingTransceivers . push ( { transceiver , transceiverName } ) ; } private getTransceiver ( transceiverName ) { if ( ! this . peerConnection ) return ; const mid = this . midLookup [ transceiverName ] ; return this . peerConnection . getTransceivers ( ) . find ( ( transceiver ) => transceiver . mid === mid ) ; } private setTrack ( transceiverName , trackConfigOrKind ) { const existingTransceiver = this . getTransceiver ( transceiverName ) ; if ( existingTransceiver ) { replyWithTrack ( existingTransceiver , trackConfigOrKind ) . catch ( ) ; } else { if ( this . isOfferer ) { this . setupTransceiver ( transceiverName ) ; } else { const kind = typeof trackConfigOrKind === 'string' ? trackConfigOrKind : ( trackConfigOrKind . track . kind as TrackKind ) ; this . sendInternal ( { type : 'transceiverRequest' , transceiverName , kind } ) ; } } } private upsertStreamConfig ( streamName , transceiverName , trackConfigOrKind ) { const existingConfig = this . streamConfig . find ( ( config ) => config . streamName === streamName && config . transceiverName === transceiverName ) ; if ( ! existingConfig ) { this . streamConfig . push ( { streamName , transceiverName , trackConfigOrKind } ) ; } else { existingConfig . trackConfigOrKind = trackConfigOrKind ; } } addStreams = ( streams ) => { if ( ! this . peerConnection ) return ; Object . keys ( streams ) . forEach ( ( streamName ) => { const trackDict = streams [ streamName ] ; const transceiverNames = Object . keys ( trackDict ) ; this . sendInternal ( { type : 'stream' , streamName , transceiverNames } ) ; transceiverNames . forEach ( ( transceiverName ) => { const trackConfigOrKind = trackDict [ transceiverName ] ; this . upsertStreamConfig ( streamName , transceiverName , trackConfigOrKind ) ; if ( trackConfigOrKind ) { this . setTrack ( transceiverName , trackConfigOrKind ) ; } } ) ; } ) ; } ; muteTrack ( transceiverName ) { const transceiver = this . getTransceiver ( transceiverName ) ; if ( ! transceiver ) { throw new Error ( `Invalid track name: ${ name }` ) ; } const { track } = transceiver . sender ; transceiver . sender . replaceTrack ( null ) . catch ( ) ; transceiver . direction = 'recvonly' ; if ( track ) { track . enabled = false ; track . stop ( ) ; } } close ( ) { if ( ! this . peerConnection ) return ; this . peerConnection . ontrack = null ; this . peerConnection . onicecandidate = null ; this . peerConnection . oniceconnectionstatechange = null ; this . peerConnection . onnegotiationneeded = null ; this . peerConnection . ondatachannel = null ; if ( this . dataChannel ) { this . sendInternal ( { type : 'close' } ) ; this . dataChannel . onmessage = null ; this . dataChannel = null ; } this . peerConnection . close ( ) ; ( this . peerConnection as any ) = null ; } send = ( data ) => { try { this . dataChannel ?. send ( data ) ; } catch ( e ) { this . emit ( 'error' , e , this ) ; } } ; dispatch ( payload ) { switch ( payload . type ) { case 'offer' : this . handleOffer ( payload ) . catch ( ) ; break ; case 'candidate' : this . handleCandidate ( payload . candidate ) ; break ; case 'answer' : this . handleAnswer ( payload ) ; } } } export default FastRTCPeer ;