wip webrtc

This commit is contained in:
Martti Malmi
2023-12-21 11:56:49 +02:00
parent 0c2ed147b0
commit 1309937869
11 changed files with 569 additions and 7 deletions

View File

@ -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(

View File

@ -1,6 +1,7 @@
import "./index.css";
import "@szhsin/react-menu/dist/index.css";
import "./fonts/inter.css";
import "./webrtc";
import {
compress,

View 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();
}
}

View 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;

View 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;
}