WebRTC and WebSocket merged
This commit is contained in:
parent
d3142c3ee4
commit
7dd4797f00
|
@ -1,34 +1,79 @@
|
|||
## Kurulum
|
||||
|
||||
Proje ortamına kurulumu
|
||||
### Proje ortamına kurulumu
|
||||
|
||||
```html
|
||||
<script src="https://ws.saqut.com/script"></script>
|
||||
```
|
||||
|
||||
Geliştirme ortamına kurulumu
|
||||
### Geliştirme ortamına kurulumu
|
||||
|
||||
```javascript
|
||||
const wsjs = new MWSE({
|
||||
endpoint: "https://ws.saqut.com/" // Bağlanılacak sunucuyu belirtir
|
||||
endpoint: "https://ws.saqut.com/" // MSWS kurulu sunucu adresi
|
||||
});
|
||||
wsjs.scope(async () => {
|
||||
// Bağlantı sağlandığında burası tetiklenir
|
||||
})
|
||||
```
|
||||
|
||||
Kendi bağlantı kimliğini öğrenme
|
||||
### Kendi bağlantı kimliğini öğrenme
|
||||
|
||||
```
|
||||
```javascript
|
||||
wsjs.scope(async () => {
|
||||
let me = wsjs.peer('me'); // kendimizden bahsederken 'me' anahtar kelimesini kullanırız
|
||||
console.log(me.socketId); // her peerin socketId'si olması gerekir
|
||||
let me = wsjs.peer('me'); // Kendi bağlantınız üzerinde işlem yaparken `me` olarak bahsedersiniz
|
||||
console.log(me.socketId); // Her eşin tekil bir socketIdsi vardır
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
### Sanal Adres ayırma / yeniden ayırma / kaldırma
|
||||
```javascript
|
||||
wsjs.scope(async () => {
|
||||
let me = wsjs.peer('me');
|
||||
|
||||
/**
|
||||
* Sanal adresler size veri gönderilmek istendiğinde veya etkileşime
|
||||
* geçilmesi istendiğinde ona socketId gibi bir UUID yerine sizi temsil eden daha kısa
|
||||
* ip adresi, sayı veya kısa bir kod ile aynı şeyleri yapmanıza olanak tanır.
|
||||
* Aynı anda hem sanal ip adres, sayı ve kısa koduna sahip olabilirsiniz
|
||||
* ancak aynı türden temsil koduna (mesela kısa koddan) birden fazla sahip olamazsınız
|
||||
* Yeni bir bağlantı daha açmanız gerekir
|
||||
**/
|
||||
|
||||
// Bağlantınıze özel sanal tekil ip adresi kaynağı ayırın
|
||||
let ipadress = await me.virtualPressure.allocAPIPAddress();
|
||||
|
||||
// Bağlantınıze özel sanal tekil numara kaynağı ayırın
|
||||
let numberaddress = await me.virtualPressure.allocAPNumber();
|
||||
|
||||
// Bağlantınıze özel sanal kod kaynağı ayırın
|
||||
let shortcodeadress = await me.virtualPressure.allocAPShortCode();
|
||||
|
||||
// Bütün bu kaynakları yenileriyle değiştirmek için
|
||||
// her birinin ayrı ayrı yeniden alma işlevleri vardır
|
||||
// Bir adresi yenilediğinizde artık eski adres kullanılmaz olur
|
||||
me.virtualPressure.reallocAPIPAddress();
|
||||
me.virtualPressure.reallocAPNumber();
|
||||
me.virtualPressure.reallocAPShortCode();
|
||||
|
||||
// Bütün bu kaynakları kaldırmak için her birinin ayrı ayrı
|
||||
// bırakma işlevi vardır
|
||||
// Bir adresi kullanmadığınızda artık bu adreslerden size
|
||||
// ulaşılamaz olursunuz
|
||||
await me.virtualPressure.releaseAPIPAddress();
|
||||
await me.virtualPressure.releaseAPNumber();
|
||||
await me.virtualPressure.releaseAPShortCode();
|
||||
|
||||
await me.virtualPressure.queryAPIPAddress();
|
||||
await me.virtualPressure.queryAPNumber();
|
||||
await me.virtualPressure.queryAPShortCode();
|
||||
})
|
||||
```
|
||||
|
||||
Farklı bir eş ile iletişime geçme
|
||||
|
||||
```
|
||||
```javascript
|
||||
wsjs.scope(async () => {
|
||||
let peer = wsjs.peer('325a8f7f-eaaf-4c21-855e-9e965c0d5ac9') // Diğer eşin socketId'sini belirtiyoruz
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import EventTarget from "./EventTarget";
|
||||
import { PeerInfo } from "./PeerInfo";
|
||||
import WebRTC from "./WebRTC";
|
||||
import MWSE from "./index";
|
||||
|
||||
interface IPeerOptions{
|
||||
|
@ -15,12 +16,44 @@ export default class Peer extends EventTarget
|
|||
public selfSocket : boolean = false;
|
||||
public active : boolean = false;
|
||||
public info : PeerInfo;
|
||||
public rtc? : WebRTC;
|
||||
public peerConnection : boolean = false;
|
||||
public primaryChannel : "websocket" | "datachannel" = "datachannel";
|
||||
constructor(wsts:MWSE){
|
||||
super();
|
||||
this.mwse = wsts;
|
||||
this.info = new PeerInfo(this);
|
||||
}
|
||||
setPeerOptions(options: string | IPeerOptions){
|
||||
public createRTC() : WebRTC
|
||||
{
|
||||
this.rtc = new WebRTC();
|
||||
this.rtc.on("connected", () => {
|
||||
this.peerConnection = true;
|
||||
});
|
||||
this.rtc.on('disconnected', () => {
|
||||
this.peerConnection = false;
|
||||
})
|
||||
this.rtc.on("output",(payload:object) => {
|
||||
this.send({
|
||||
type: 'rtc:session',
|
||||
payload: payload
|
||||
})
|
||||
});
|
||||
this.on('message',(data:{type?:string,payload?:any}) => {
|
||||
if(data.type == 'rtc:session')
|
||||
{
|
||||
if(this.rtc)
|
||||
{
|
||||
this.rtc.emit("input", data.payload)
|
||||
}
|
||||
}else if(data.type == 'rtc:message')
|
||||
{
|
||||
this.emit("message", data.payload)
|
||||
}
|
||||
});
|
||||
return this.rtc;
|
||||
}
|
||||
public setPeerOptions(options: string | IPeerOptions){
|
||||
if(typeof options == "string")
|
||||
{
|
||||
this.setSocketId(options)
|
||||
|
@ -28,7 +61,7 @@ export default class Peer extends EventTarget
|
|||
this.options = options;
|
||||
}
|
||||
}
|
||||
setSocketId(uuid: string){
|
||||
public setSocketId(uuid: string){
|
||||
this.socketId = uuid;
|
||||
}
|
||||
async metadata() : Promise<any>
|
||||
|
@ -77,11 +110,37 @@ export default class Peer extends EventTarget
|
|||
});
|
||||
}
|
||||
async send(pack: any){
|
||||
await this.mwse.EventPooling.request({
|
||||
type:'pack/to',
|
||||
pack,
|
||||
to: this.socketId
|
||||
});
|
||||
let isOpenedP2P = this.peerConnection;
|
||||
let isOpenedServer = this.mwse.server.connected;
|
||||
let sendChannel : "websocket" | "datachannel";
|
||||
if(isOpenedP2P && isOpenedServer)
|
||||
{
|
||||
if(this.primaryChannel == "websocket")
|
||||
{
|
||||
sendChannel = "websocket"
|
||||
}else
|
||||
{
|
||||
sendChannel = "datachannel"
|
||||
}
|
||||
}else if(isOpenedServer){
|
||||
sendChannel = "websocket"
|
||||
}else{
|
||||
sendChannel = "datachannel"
|
||||
}
|
||||
|
||||
if(sendChannel == "websocket")
|
||||
{
|
||||
await this.mwse.EventPooling.request({
|
||||
type:'pack/to',
|
||||
pack,
|
||||
to: this.socketId
|
||||
});
|
||||
}else{
|
||||
this.rtc?.send({
|
||||
type: 'rtc:message',
|
||||
payload: pack
|
||||
})
|
||||
}
|
||||
}
|
||||
async forget(){
|
||||
this.mwse.peers.delete(this.socketId as string);
|
||||
|
|
|
@ -0,0 +1,378 @@
|
|||
interface TransferStreamInfo
|
||||
{
|
||||
senders : RTCRtpSender[];
|
||||
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 WebRTC()
|
||||
{
|
||||
this.rtc = new RTCPeerConnection({
|
||||
iceCandidatePoolSize: 0,
|
||||
iceTransportPolicy:"all",
|
||||
rtcpMuxPolicy:"require",
|
||||
iceServers:[{
|
||||
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"
|
||||
}]
|
||||
});
|
||||
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 {stream} = this.sendingStream.get(id) as {stream:MediaStream};
|
||||
let senders = [];
|
||||
for (const track of stream.getTracks()) {
|
||||
senders.push(this.rtc.addTrack(track, stream));
|
||||
};
|
||||
stream.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);
|
||||
});
|
||||
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;
|
||||
})
|
||||
}
|
||||
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)
|
||||
{
|
||||
if(event.streams.length)
|
||||
{
|
||||
for (const stream of event.streams) {
|
||||
if((this.recaivingStream.get(stream.id) as {stream : MediaStream | undefined}).stream == null)
|
||||
{
|
||||
(
|
||||
this.recaivingStream.get(stream.id) as {stream : MediaStream | undefined}
|
||||
).stream = stream;
|
||||
this.emit('stream:added', this.recaivingStream.get(stream.id));
|
||||
}else{
|
||||
(
|
||||
this.recaivingStream.get(stream.id) as {stream : MediaStream | undefined}
|
||||
).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 [id, {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[];
|
||||
}
|
||||
}
|
|
@ -5,7 +5,9 @@ import { IPPressure } from "./IPPressure";
|
|||
import Peer from "./Peer";
|
||||
import Room, { IRoomOptions } from "./Room";
|
||||
import WSTSProtocol, { Message } from "./WSTSProtocol";
|
||||
import WebRTC from "./WebRTC";
|
||||
export default class MWSE extends EventTarget {
|
||||
public static rtc : WebRTC;
|
||||
public server! : Connection;
|
||||
public WSTSProtocol! : WSTSProtocol;
|
||||
public EventPooling! : EventPool;
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,62 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Document</title>
|
||||
</head>
|
||||
<body>
|
||||
<script src="http://127.0.0.1:7707/script"></script>
|
||||
<script>
|
||||
const wsjs = new MWSE({
|
||||
endpoint: "ws://127.0.0.1:7707"
|
||||
});
|
||||
wsjs.scope(async () => {
|
||||
let me = wsjs.peer('me');
|
||||
|
||||
await me.info.set("name","Abdussamed");
|
||||
await me.info.set("surname","ULUTAŞ");
|
||||
await me.info.set("age","25");
|
||||
await me.info.set("date",1);
|
||||
|
||||
let t = 2;
|
||||
setInterval(()=>{
|
||||
me.info.set("date", t);
|
||||
t++;
|
||||
},2000)
|
||||
|
||||
|
||||
let room = wsjs.room({
|
||||
name: "saqut.com",
|
||||
description: "saqut.com try",
|
||||
joinType: "free",
|
||||
ifexistsJoin: true,
|
||||
accessType: "private",
|
||||
notifyActionInvite: false,
|
||||
notifyActionJoined: true,
|
||||
notifyActionEjected: true
|
||||
});
|
||||
|
||||
await room.createRoom();
|
||||
|
||||
let peers = await room.fetchPeers();
|
||||
for (const peer of peers) {
|
||||
await peer.info.fetch();
|
||||
console.log("Peer info fetched",peer.socketId,peer.info.get());
|
||||
peer.on('info', (name, value) => {
|
||||
console.log("Peer info changed", peer.socketId, name, value, peer.info.get());
|
||||
})
|
||||
}
|
||||
|
||||
room.on('join', async peer => {
|
||||
await peer.info.fetch();
|
||||
console.log("Peer info fetched",peer.socketId,peer.info.get());
|
||||
peer.on('info', (name, value) => {
|
||||
console.log("Peer info changed", peer.socketId, name, value, peer.info.get());
|
||||
})
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
Loading…
Reference in New Issue