import { BroadcastChannel, cast, getBroadcastChannel } from "./arcast" export class ScreenSharing extends EventTarget { signaling: BroadcastChannel handlers: { [messageType: string]: (data: { type: string, data: any }) => void } constructor() { super() this.handlers = {} this.signaling = getBroadcastChannel("arcast.screen-sharing", this.onSignal.bind(this)) } onSignal(data: any) { let message: any try { message = JSON.parse(data) } catch (err) { console.warn(err) return } if (typeof message.type !== "string") return const handler = this.handlers[message.type] if (!handler) return handler(message.data) } send(type: string, data: any) { this.signaling.send(JSON.stringify({ type, data })); } } export class ScreenSharingClient extends ScreenSharing { sessionId: string clientId: string peerConnection: RTCPeerConnection | undefined constructor(sessionId: string) { super() this.sessionId = sessionId this.clientId = window.crypto.randomUUID() this.handlers['server-offer'] = this.onServerOffer.bind(this) this.handlers['ice-candidate'] = this.onIceCandidate.bind(this) } connect() { this.send("client-request", { clientId: this.clientId, sessionId: this.sessionId }) } close() { this.signaling.close() } async onServerOffer(message: any) { if (!this.checkMessage(message)) return const conn = new RTCPeerConnection() conn.onicecandidate = this.sendCandidate.bind(this) conn.ontrack = this.onTrack.bind(this) conn.onconnectionstatechange = (evt: Event) => { const state = conn.connectionState this.dispatchEvent(new StateChangedEvent(state)) } await conn.setRemoteDescription(new RTCSessionDescription(message.offer)) const answer = await conn.createAnswer() await conn.setLocalDescription(answer) this.peerConnection = conn this.send("client-answer", { clientId: this.clientId, sessionId: this.sessionId, answer }) } sendCandidate(evt: RTCPeerConnectionIceEvent) { this.send("ice-candidate", { clientId: this.clientId, sessionId: this.sessionId, candidate: evt.candidate }) } onTrack(evt: RTCTrackEvent) { if (!evt.streams || evt.streams.length < 1) return; const stream = evt.streams[0] this.dispatchEvent(new StreamChangedEvent(stream)) } async onIceCandidate(message: any) { if (!this.checkMessage(message)) return if (!this.peerConnection) return if (!message.candidate) return await this.peerConnection.addIceCandidate(message.candidate) } checkMessage(message: any) { const sessionId = message.sessionId; if (!this.sessionId || sessionId !== this.sessionId) return false; if (this.clientId !== message.clientId) return false; return true } } export enum ScreenSharingServerState { Idle = "idle", Sharing = "sharing" } export class ScreenSharingServer extends ScreenSharing { stream: MediaStream | undefined sessionId: string | undefined peerConnections: { [key: string]: RTCPeerConnection } state: ScreenSharingServerState constructor() { super() this.peerConnections = {} this.state = ScreenSharingServerState.Idle this.handlers['client-request'] = this.onClientRequest.bind(this) this.handlers['client-answer'] = this.onClientAnswer.bind(this) this.handlers['ice-candidate'] = this.onIceCandidate.bind(this) } async shareScreen(options?: DisplayMediaStreamOptions) { try { this.stream = await navigator.mediaDevices.getDisplayMedia(options) } catch (err) { console.warn(err) return } this.sessionId = window.crypto.randomUUID() const url = `http://127.0.0.1:45555/apps/main/#screen-sharing/sessions/${this.sessionId}` cast(url) if (process.env.NODE_ENV === "development") { window.open(`http://localhost:3000/apps/main/#screen-sharing/sessions/${this.sessionId}`, "_blank") } this.state = ScreenSharingServerState.Sharing this.dispatchEvent(new StateChangedEvent(ScreenSharingServerState.Sharing)) } onClientRequest(message: any) { if (!this.checkMessage(message)) return this.addNewClient(message.clientId) } async onIceCandidate(message: any) { if (!this.checkMessage(message)) return const conn = this.peerConnections[message.clientId] if (!conn) return if (!message.candidate) return await conn.addIceCandidate(message.candidate) } async onClientAnswer(message: any) { if (!this.checkMessage(message)) return const conn = this.peerConnections[message.clientId] if (!conn) return await conn.setRemoteDescription(new RTCSessionDescription(message.answer)); } checkMessage(message: any) { const sessionId = message.sessionId; if (!this.sessionId || sessionId !== this.sessionId) return false; const clientId = message.clientId; if (!clientId) return false; return true } async addNewClient(clientId: string) { if (!this.stream) return const conn = new RTCPeerConnection(); const tracks = this.stream?.getVideoTracks() if (!tracks || tracks.length === 0) { return } conn.addTrack(tracks[0], this.stream) const offer = await conn.createOffer() await conn.setLocalDescription(offer); conn.onicecandidate = this.sendCandidate.bind(this, clientId) conn.onconnectionstatechange = (evt: Event) => { const isClosed = conn.connectionState === "disconnected" || conn.connectionState === "failed" if (!isClosed) return; conn.onicecandidate = null; conn.onconnectionstatechange = null; conn.close(); console.log("Removing client", clientId); delete this.peerConnections[clientId]; } console.log("Adding client", clientId); this.peerConnections[clientId] = conn; this.send("server-offer", { sessionId: this.sessionId, clientId, offer }) } sendCandidate(clientId: string, evt: RTCPeerConnectionIceEvent) { this.send("ice-candidate", { clientId, sessionId: this.sessionId, candidate: evt.candidate }) } close() { this.signaling.close() Object.keys(this.peerConnections).forEach(clientId => { const conn = this.peerConnections[clientId]; conn.onicecandidate = null; conn.onconnectionstatechange = null; conn.close(); delete this.peerConnections[clientId] }) const tracks = this.stream?.getTracks() if (!tracks) return tracks.forEach(track => track.stop()) this.state = ScreenSharingServerState.Idle this.dispatchEvent(new StateChangedEvent(ScreenSharingServerState.Idle)) } } export const StreamChangedEventType = "stream-changed" export class StreamChangedEvent extends Event { stream: MediaStream constructor(stream: MediaStream) { super(StreamChangedEventType) this.stream = stream } } export const StateChangedEventType = "state-changed" export class StateChangedEvent extends Event { state: string constructor(state: string) { super(StateChangedEventType) this.state = state } }