import Peer from "./Peer"; import "webrtc-adapter"; interface TransferStreamInfo { senders : RTCRtpSender[]; recaivers : RTCRtpReceiver[]; stream:MediaStream | undefined; id:string; name:string; } export default class WebRTC { public static channels : Map = new Map(); public static requireGC : boolean = false; public id : any; public active : boolean = false; public connectionStatus : "closed" | "connected" | "connecting" | "disconnected" | "failed" | "new" = "new"; public iceStatus : "checking" | "closed" | "completed" | "connected" | "disconnected" | "failed" | "new" = "new"; public gatheringStatus : "complete" | "gathering" | "new" = "new"; public signalingStatus : "" | "closed" | "have-local-offer" | "have-local-pranswer" | "have-remote-offer" | "have-remote-pranswer" | "stable" = "" public rtc! : RTCPeerConnection; public recaivingStream : Map = new Map(); public sendingStream : Map = new Map(); public events : { [eventname:string]: Function[] } = {}; public channel : RTCDataChannel | undefined; public static defaultRTCConfig : RTCConfiguration = { iceCandidatePoolSize: 0, iceTransportPolicy:"all", rtcpMuxPolicy:"require", }; public static defaultICEServers : RTCIceServer[] = [{ urls: "stun:stun.l.google.com:19302" },{ urls: "stun:stun1.l.google.com:19302" },{ urls: "stun:stun2.l.google.com:19302" },{ urls: "stun:stun3.l.google.com:19302" },{ urls: "stun:stun4.l.google.com:19302" }]; public peer? : Peer; constructor( rtcConfig?: RTCConfiguration, rtcServers?: RTCIceServer[] ) { let config : any = {}; if(rtcConfig) { Object.assign( config, WebRTC.defaultRTCConfig, rtcConfig ) }else{ Object.assign( config, WebRTC.defaultRTCConfig ) } config.iceServers = rtcServers || WebRTC.defaultICEServers; this.rtc = new RTCPeerConnection(config as RTCConfiguration); this.rtc.addEventListener("connectionstatechange",()=>{ this.eventConnectionState(); }) this.rtc.addEventListener("icecandidate",(...args)=>{ this.eventIcecandidate(...args); }) this.rtc.addEventListener("iceconnectionstatechange",()=>{ this.eventICEConnectionState(); }) this.rtc.addEventListener("icegatheringstatechange",()=>{ this.eventICEGatherinState(); }) this.rtc.addEventListener("negotiationneeded",()=>{ this.eventNogationNeeded(); }) this.rtc.addEventListener("signalingstatechange",()=>{ this.eventSignalingState(); }) this.rtc.addEventListener("track",(...args)=>{ this.eventTrack(...args); }) this.rtc.addEventListener("datachannel",(...args)=>{ this.eventDatachannel(...args); }) this.on('input',async (data:{[key:string]:any})=>{ switch(data.type) { case "icecandidate":{ await this.rtc.addIceCandidate(new RTCIceCandidate(data.value)); break; } case "offer":{ await this.rtc.setRemoteDescription(new RTCSessionDescription(data.value)); let answer = await this.rtc.createAnswer({ offerToReceiveAudio: true, offerToReceiveVideo: true }) await this.rtc.setLocalDescription(answer); this.send({ type: 'answer', value: answer }); break; } case "answer":{ await this.rtc.setRemoteDescription(new RTCSessionDescription(data.value)) break; } case "streamInfo":{ let {id,value} = data; let streamInfo = this.recaivingStream.get(id); if(!streamInfo) { this.recaivingStream.set(id,value as TransferStreamInfo); }else{ this.recaivingStream.set(id,{ ...streamInfo, ...value } as TransferStreamInfo); } this.send({ type:'streamAccept', id }) break; } case "streamRemoved":{ let {id} = data; this.emit('stream:stopped', this.recaivingStream.get(id)); this.sendingStream.delete(id); break; } case "streamAccept":{ let {id} = data; let sendingStream = this.sendingStream.get(id) as TransferStreamInfo; let senders = []; if(sendingStream && sendingStream.stream) { for (const track of sendingStream.stream.getTracks()) { senders.push(this.rtc.addTrack(track, sendingStream.stream)); }; sendingStream.senders = senders; } break; } case "message":{ this.emit('message', data.payload); break; } } }) } public addEventListener(event:string,callback: Function){ (this.events[event] || (this.events[event]=[])).push(callback); }; public on(event:string,callback: Function){ this.addEventListener(event, callback) }; public async dispatch(event:string,...args:any[]) : Promise { if(this.events[event]) { for (const callback of this.events[event]) { await callback(...args) } } } public async emit(event:string,...args:any[]) : Promise { await this.dispatch(event, ...args) } public connect() { if(!this.channel) { this.createDefaultDataChannel(); } } public sendMessage(data: any) { this.send({ type: 'message', payload: data }); } public createDefaultDataChannel() { let dt = this.rtc.createDataChannel(':default:',{ ordered: true }); dt.addEventListener("open",()=>{ this.channel = dt; WebRTC.channels.set(this.id, this); }); dt.addEventListener("message",({data})=>{ let pack = JSON.parse(data); this.emit('input', pack); }) dt.addEventListener("close",()=>{ this.channel = undefined; }) } public destroy() { this.active = false; if(this.channel) { this.channel.close(); this.channel = undefined; } if(this.rtc) { this.rtc.close(); //this.rtc = undefined; }; this.emit('disconnected'); WebRTC.channels.delete(this.id); } public eventDatachannel(event: RTCDataChannelEvent) { if(event.channel.label == ':default:'){ WebRTC.channels.set(this.id, this); this.channel = event.channel; event.channel.addEventListener("message",({data})=>{ let pack = JSON.parse(data); this.emit('input', pack); }) event.channel.addEventListener("close",()=>{ this.channel = undefined; WebRTC.channels.delete(this.id); WebRTC.requireGC = true; }) }else{ this.emit('datachannel', event.channel); } } public send(data:object) { if(this.channel?.readyState == "open") { this.channel.send(JSON.stringify(data)); }else{ this.emit('output', data); } } public eventConnectionState() { this.connectionStatus = this.rtc.connectionState; if(this.connectionStatus == 'connected') { if(this.active == false) { this.emit('connected'); this.active = true; } }; if(this.connectionStatus == 'failed' || this.connectionStatus == "disconnected" || this.connectionStatus == "closed") { if(this.active) { this.destroy(); } } } public eventIcecandidate(event: RTCPeerConnectionIceEvent) { if(event.candidate) { this.send({ type:'icecandidate', value: event.candidate }) } } public eventICEConnectionState() { this.iceStatus = this.rtc.iceConnectionState; } public eventICEGatherinState() { this.gatheringStatus = this.rtc.iceGatheringState; } public async eventNogationNeeded() { let offer = await this.rtc.createOffer({ iceRestart: true, offerToReceiveAudio: true, offerToReceiveVideo: true }); await this.rtc.setLocalDescription(offer); this.send({ type: 'offer', value: offer }); } public eventSignalingState() { this.signalingStatus = this.rtc.signalingState; } public eventTrack(event: RTCTrackEvent) { let rtpRecaiver = event.receiver; if(event.streams.length) { for (const stream of event.streams) { let streamInfo = this.recaivingStream.get(stream.id) as TransferStreamInfo; (streamInfo.recaivers || (streamInfo.recaivers = [])).push(rtpRecaiver); if((this.recaivingStream.get(stream.id) as {stream : MediaStream | undefined}).stream == null) { streamInfo.stream = stream; this.emit('stream:added', this.recaivingStream.get(stream.id)); }else{ streamInfo.stream = stream; } } } } public sendStream(stream:MediaStream,name:string,info:{[key:string]:any}){ this.send({ type: 'streamInfo', id: stream.id, value: { ...info, name: name } }); this.sendingStream.set(stream.id,{ ...info, id:stream.id, name: name, stream } as TransferStreamInfo); }; public stopStream(_stream:MediaStream){ if(this.connectionStatus != 'connected'){ return } if(this.sendingStream.has(_stream.id)) { let {stream} = this.sendingStream.get(_stream.id) as {stream:MediaStream}; for (const track of stream.getTracks()) { for (const RTCPSender of this.rtc.getSenders()) { if(RTCPSender.track?.id == track.id) { this.rtc.removeTrack(RTCPSender); } } } this.send({ type: 'streamRemoved', id: stream.id }); this.sendingStream.delete(_stream.id) } } public stopAllStreams() { if(this.connectionStatus != 'connected'){ return } for (const [, {stream}] of this.sendingStream) { if(stream == undefined) { continue; } for (const track of stream.getTracks()) { for (const RTCPSender of this.rtc.getSenders()) { if(RTCPSender.track?.id == track.id) { this.rtc.removeTrack(RTCPSender); } } } this.send({ type: 'streamRemoved', id: stream.id }); }; this.sendingStream.clear(); } } WebRTC.requireGC = false; setInterval(()=>{ if(WebRTC.requireGC == false) return; let img = document.createElement("img"); img.src = window.URL.createObjectURL(new Blob([new ArrayBuffer(5e+7)])); img.onerror = function() { window.URL.revokeObjectURL(this.src); }; WebRTC.requireGC = false; }, 3000) declare global { interface MediaStream { senders : RTCRtpSender[]; } }