521 lines
16 KiB
TypeScript
521 lines
16 KiB
TypeScript
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[];
|
||
}
|
||
} |