MWSE/frontend/WebRTC.ts

457 lines
14 KiB
TypeScript

import P2PFileSender from "./P2PFileSender";
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<any,any> = 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<string, TransferStreamInfo> = new Map();
public sendingStream : Map<string, TransferStreamInfo> = 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;
public FileTransportChannel? : P2PFileSender;
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.recaivingStream.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<any> {
if(this.events[event])
{
for (const callback of this.events[event])
{
await callback(...args)
}
}
}
public async emit(event:string,...args:any[]) : Promise<any> {
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);
this.active = true;
});
dt.addEventListener("message",({data})=>{
let pack = JSON.parse(data);
this.emit('input', pack);
})
dt.addEventListener("close",()=>{
this.channel = undefined;
this.active = false;
})
}
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;
this.active = true;
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');
}
};
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();
}
public async SendFile(file:File, meta: object)
{
if(!this.peer)
{
throw new Error("Peer is not ready");
}
this.FileTransportChannel = new P2PFileSender(this, this.peer);
await this.FileTransportChannel.SendFile(file, meta);
}
public async RecaiveFile(
chnlCount:number,
filemeta: {
name: string;
type: string;
},
totalSize: number
) : Promise<File>
{
if(!this.peer)
{
throw new Error("Peer is not ready");
}
this.FileTransportChannel = new P2PFileSender(this, this.peer);
return await new Promise(recaivedFile => {
if(this.FileTransportChannel)
{
this.FileTransportChannel.RecaiveFile(
this.rtc,
filemeta,
chnlCount,
totalSize,
(file: File) => {
recaivedFile(file)
}
);
}
})
}
}
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[];
}
}