275 lines
7.7 KiB
TypeScript
275 lines
7.7 KiB
TypeScript
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
|
|
}
|
|
} |