feat: rewrite app system
Some checks reported warnings
arcad/arcast/pipeline/head This commit is unstable
Some checks reported warnings
arcad/arcast/pipeline/head This commit is unstable
This commit is contained in:
275
apps/main/src/api/webrtc.ts
Normal file
275
apps/main/src/api/webrtc.ts
Normal file
@ -0,0 +1,275 @@
|
||||
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
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user