wip webrtc
This commit is contained in:
@ -1,5 +1,6 @@
|
||||
import { NostrEvent, OkResponse, SystemInterface } from "@snort/system";
|
||||
import { removeUndefined } from "@snort/shared";
|
||||
import {getWebRtcPool} from "@/webrtc";
|
||||
|
||||
export async function sendEventToRelays(
|
||||
system: SystemInterface,
|
||||
@ -8,6 +9,7 @@ export async function sendEventToRelays(
|
||||
setResults?: (x: Array<OkResponse>) => void,
|
||||
) {
|
||||
console.log("sendEventToRelays", ev, customRelays);
|
||||
getWebRtcPool()?.send(ev);
|
||||
if (customRelays) {
|
||||
return removeUndefined(
|
||||
await Promise.all(
|
||||
|
@ -1,6 +1,7 @@
|
||||
import "./index.css";
|
||||
import "@szhsin/react-menu/dist/index.css";
|
||||
import "./fonts/inter.css";
|
||||
import "./webrtc";
|
||||
|
||||
import {
|
||||
compress,
|
||||
|
106
packages/app/src/webrtc/WebRTCConnection.ts
Normal file
106
packages/app/src/webrtc/WebRTCConnection.ts
Normal file
@ -0,0 +1,106 @@
|
||||
import { Socket } from "socket.io-client";
|
||||
import EventEmitter from "eventemitter3";
|
||||
|
||||
export class WebRTCConnection extends EventEmitter {
|
||||
private peerConnection: RTCPeerConnection;
|
||||
private dataChannel: RTCDataChannel;
|
||||
|
||||
constructor(private socket: Socket, configuration: RTCConfiguration, public peerId: string) {
|
||||
super();
|
||||
this.peerConnection = new RTCPeerConnection(configuration);
|
||||
this.dataChannel = this.peerConnection.createDataChannel("data");
|
||||
this.registerPeerConnectionEvents();
|
||||
this.setupDataChannel();
|
||||
}
|
||||
|
||||
private log(...args: any[]): void {
|
||||
console.log(this.peerId, ...args);
|
||||
}
|
||||
|
||||
public async handleOffer(offer: RTCSessionDescriptionInit): Promise<void> {
|
||||
this.log('Received offer', offer);
|
||||
await this.peerConnection.setRemoteDescription(new RTCSessionDescription(offer));
|
||||
await this.sendLocalDescription('answer');
|
||||
}
|
||||
|
||||
public async handleAnswer(answer: RTCSessionDescriptionInit): Promise<void> {
|
||||
this.log('Received answer', answer);
|
||||
await this.peerConnection.setRemoteDescription(new RTCSessionDescription(answer));
|
||||
}
|
||||
|
||||
public handleCandidate(candidate: RTCIceCandidateInit): void {
|
||||
this.log('Received ICE candidate', candidate);
|
||||
this.peerConnection.addIceCandidate(new RTCIceCandidate(candidate));
|
||||
}
|
||||
|
||||
private async sendLocalDescription(type: 'offer' | 'answer'): Promise<void> {
|
||||
let description;
|
||||
if (type === 'offer') {
|
||||
description = await this.peerConnection.createOffer();
|
||||
} else {
|
||||
description = await this.peerConnection.createAnswer();
|
||||
}
|
||||
await this.peerConnection.setLocalDescription(description);
|
||||
this.socket.emit(type, { [type]: description, recipient: this.peerId });
|
||||
this.log(`Sent ${type}`, description);
|
||||
}
|
||||
|
||||
private setupDataChannel(): void {
|
||||
this.dataChannel.onopen = () => this.log('Data channel opened');
|
||||
this.dataChannel.onclose = () => this.log('Data channel closed');
|
||||
this.dataChannel.onmessage = event => this.handleDataChannelMessage(event);
|
||||
}
|
||||
|
||||
private handleDataChannelMessage(event: MessageEvent): void {
|
||||
this.log(`-> "${event.data}"`);
|
||||
if (event.data === 'ping') {
|
||||
this.send('pong');
|
||||
} else {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
this.emit('event', data);
|
||||
} catch (e) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public send(data: any): void {
|
||||
if (this.dataChannel.readyState === 'open') {
|
||||
this.log(`<- "${data}"`);
|
||||
this.dataChannel.send(data);
|
||||
}
|
||||
}
|
||||
|
||||
public async handleHello(): Promise<void> {
|
||||
if (this.peerConnection.connectionState === 'new') {
|
||||
await this.sendLocalDescription('offer');
|
||||
}
|
||||
}
|
||||
|
||||
private registerPeerConnectionEvents(): void {
|
||||
this.peerConnection.onicecandidate = event => {
|
||||
if (event.candidate) {
|
||||
this.log('Local ICE candidate:', event.candidate);
|
||||
this.socket.emit('candidate', { candidate: event.candidate.toJSON(), recipient: this.peerId });
|
||||
}
|
||||
};
|
||||
|
||||
this.peerConnection.oniceconnectionstatechange = () => {
|
||||
this.log('ICE Connection State Change:', this.peerConnection.iceConnectionState);
|
||||
};
|
||||
|
||||
this.peerConnection.onconnectionstatechange = () => {
|
||||
this.log('WebRTC Connection State Change:', this.peerConnection.connectionState);
|
||||
};
|
||||
|
||||
this.peerConnection.ondatachannel = event => {
|
||||
this.dataChannel = event.channel;
|
||||
this.setupDataChannel();
|
||||
};
|
||||
}
|
||||
|
||||
public close(): void {
|
||||
this.peerConnection.close();
|
||||
}
|
||||
}
|
86
packages/app/src/webrtc/WebRTCPool.ts
Normal file
86
packages/app/src/webrtc/WebRTCPool.ts
Normal file
@ -0,0 +1,86 @@
|
||||
import {io, Socket} from 'socket.io-client';
|
||||
import {WebRTCConnection} from '@/webrtc/WebRTCConnection';
|
||||
import EventEmitter from "eventemitter3";
|
||||
|
||||
const MAX_CONNECTIONS = 5;
|
||||
|
||||
class WebRTCPool extends EventEmitter {
|
||||
private signalingServer: Socket;
|
||||
private peers: Map<string, WebRTCConnection> = new Map();
|
||||
private configuration: RTCConfiguration;
|
||||
private peerId: string;
|
||||
|
||||
constructor(serverUrl: string, configuration: RTCConfiguration = {}, peerId: string) {
|
||||
super();
|
||||
this.signalingServer = io(serverUrl);
|
||||
this.configuration = configuration;
|
||||
this.peerId = peerId;
|
||||
this.registerSocketEvents();
|
||||
}
|
||||
|
||||
private sayHello(): void {
|
||||
this.signalingServer.emit('hello', this.peerId);
|
||||
}
|
||||
|
||||
public send(data: any, recipients?: string[]): void {
|
||||
this.peers.forEach(conn => {
|
||||
if (!recipients || recipients.includes(conn.peerId)) {
|
||||
try {
|
||||
conn.send(typeof data === 'string' ? data : JSON.stringify(data));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public createConnection(peerId: string): WebRTCConnection {
|
||||
if (this.peers.size >= MAX_CONNECTIONS) {
|
||||
throw new Error('Maximum connections reached');
|
||||
}
|
||||
const connection = new WebRTCConnection(this.signalingServer, this.configuration, peerId);
|
||||
connection.on('event', (event: any) => this.emit('event', event));
|
||||
this.peers.set(peerId, connection);
|
||||
return connection;
|
||||
}
|
||||
|
||||
private handleConnectionEvent(sender: string, action: (connection: WebRTCConnection) => Promise<void>): void {
|
||||
if (sender === this.peerId || this.peers.size >= MAX_CONNECTIONS) return;
|
||||
const connection = this.peers.get(sender) ?? this.createConnection(sender);
|
||||
action(connection);
|
||||
}
|
||||
|
||||
private registerSocketEvents(): void {
|
||||
this.signalingServer.on('connect', () => {
|
||||
console.log('Connected to signaling server');
|
||||
this.sayHello();
|
||||
});
|
||||
|
||||
this.signalingServer.on('offer', ({offer, sender}: { offer: RTCSessionDescriptionInit; sender: string }) => {
|
||||
this.handleConnectionEvent(sender, async conn => await conn.handleOffer(offer));
|
||||
});
|
||||
|
||||
this.signalingServer.on('answer', ({answer, sender}: { answer: RTCSessionDescriptionInit; sender: string }) => {
|
||||
this.handleConnectionEvent(sender, async conn => await conn.handleAnswer(answer));
|
||||
});
|
||||
|
||||
this.signalingServer.on('candidate', ({candidate, sender}: { candidate: RTCIceCandidateInit; sender: string }) => {
|
||||
this.handleConnectionEvent(sender, conn => conn.handleCandidate(candidate));
|
||||
});
|
||||
|
||||
this.signalingServer.on('hello', (sender: string) => {
|
||||
console.log('Received hello from', sender);
|
||||
this.handleConnectionEvent(sender, conn => conn.handleHello());
|
||||
});
|
||||
}
|
||||
|
||||
public close(): void {
|
||||
console.log('closing pool');
|
||||
this.signalingServer.close();
|
||||
for (const conn of this.peers.values()) {
|
||||
conn.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default WebRTCPool;
|
25
packages/app/src/webrtc/index.ts
Normal file
25
packages/app/src/webrtc/index.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import {LoginStore} from "@/Login";
|
||||
import WebRTCPool from "@/webrtc/WebRTCPool";
|
||||
|
||||
let publicKey: string | undefined;
|
||||
let pool: WebRTCPool | undefined;
|
||||
let interval: NodeJS.Timeout | undefined;
|
||||
|
||||
LoginStore.hook(() => {
|
||||
const login = LoginStore.takeSnapshot();
|
||||
if (login.publicKey && !login.readonly && login.publicKey !== publicKey) {
|
||||
publicKey = login.publicKey;
|
||||
if (location.hostname === 'localhost') {
|
||||
pool?.close();
|
||||
interval && clearInterval(interval);
|
||||
pool = new WebRTCPool('http://localhost:3000', {
|
||||
iceServers: [{ urls: 'stun:localhost:3478' }],
|
||||
}, login.publicKey);
|
||||
interval = setInterval(() => pool?.send('ping'), 10000);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export function getWebRtcPool(): WebRTCPool | undefined {
|
||||
return pool;
|
||||
}
|
Reference in New Issue
Block a user