MWSE/frontend/WebRTC.ts

521 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import P2PFileSender from "./P2PFileSender";
import Peer from "./Peer";
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",
};
private isPolite() : boolean
{
let myId = this.peer?.mwse.peer('me').socketId as string;
let peerId = this.peer?.socketId as string;
return myId < peerId;
}
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;
public makingOffer = false;
public ignoreOffer = false;
public isSettingRemoteAnswerPending = false;
candicatePack : RTCIceCandidate[] = [];
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":{
try{
if(this.rtc.remoteDescription){
await this.rtc.addIceCandidate(new RTCIceCandidate(data.value));
}else{
this.candicatePack.push(new RTCIceCandidate(data.value))
}
}catch(error){
debugger;
}finally{
console.log("ICE Canbet")
}
break;
}
case "offer":{
let readyForOffer = !this.makingOffer && (this.rtc.signalingState == "stable" || this.isSettingRemoteAnswerPending);
const offerCollision = !readyForOffer;
this.ignoreOffer = !this.isPolite() && offerCollision;
if(this.ignoreOffer){
return;
}
this.isSettingRemoteAnswerPending = false;
await this.rtc.setRemoteDescription(new RTCSessionDescription(data.value));
this.isSettingRemoteAnswerPending = false;
for (const candidate of this.candicatePack) {
await this.rtc.addIceCandidate(candidate);
}
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))
for (const candidate of this.candicatePack) {
await this.rtc.addIceCandidate(candidate);
}
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)
{
if(data.type == ':rtcpack:')
{
throw "WebRTC Kanalında Sızma";
}
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;
this.active = false;
})
}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.rtc.restartIce();
};
if(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()
{
try{
this.makingOffer = true;
let offer = await this.rtc.createOffer({
iceRestart: true,
offerToReceiveAudio: true,
offerToReceiveVideo: true
});
await this.rtc.setLocalDescription(offer);
this.send({
type: 'offer',
value: offer
});
}catch(error){
console.error(`Nogation Error:`, error)
}
finally{
this.makingOffer = false;
}
}
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[];
}
}