it works!!!
This commit is contained in:
commit
1e47627c27
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
node_modules/
|
||||
build/
|
||||
package-lock.json
|
||||
config.json
|
||||
*.log
|
||||
1
.prettierignore
Normal file
1
.prettierignore
Normal file
@ -0,0 +1 @@
|
||||
package-lock.json
|
||||
7
.prettierrc
Normal file
7
.prettierrc
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"tabWidth": 4,
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "es5",
|
||||
"printWidth": 120
|
||||
}
|
||||
17
README.md
Normal file
17
README.md
Normal file
@ -0,0 +1,17 @@
|
||||
# screensy-unbloated
|
||||
|
||||
Screen sharing website.
|
||||
Based on https://github.com/screensy/screensy but without a ton of bloat
|
||||
|
||||
## Usage
|
||||
|
||||
1. ```sh
|
||||
cp default-config.json config.json
|
||||
```
|
||||
2. Edit `config.json`
|
||||
3.
|
||||
|
||||
```sh
|
||||
npm run build
|
||||
npm run start
|
||||
```
|
||||
4
default-config.json
Normal file
4
default-config.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"host": "127.0.0.1",
|
||||
"port": 4004
|
||||
}
|
||||
23
package.json
Normal file
23
package.json
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "screensy-unbloated",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"homepage": "https://github.com/screensy/screensy",
|
||||
"devDependencies": {
|
||||
"@types/minimist": "^1.2.5",
|
||||
"@types/node": "^22.18.6",
|
||||
"@types/serve-handler": "^6.1.4",
|
||||
"@types/ws": "^8.18.1",
|
||||
"@typescript/native-preview": "^7.0.0-dev.20250918.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"minimist": "^1.2.8",
|
||||
"serve-handler": "^6.1.6",
|
||||
"ws": "^8.18.3"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "npx tsgo -b src/client src/server",
|
||||
"postbuild": "cp -r public config.json build && cp -r src build/public",
|
||||
"start": "cd build && node ./server.js",
|
||||
"clean": "rm -rf build"
|
||||
}
|
||||
}
|
||||
82
public/index.html
Normal file
82
public/index.html
Normal file
@ -0,0 +1,82 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>screensy</title>
|
||||
<meta name="description" content="Simple peer-to-peer screen sharing" />
|
||||
<meta name="application-name" content="screensy" />
|
||||
<meta name="language" content="en" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link href="./styles.css" rel="stylesheet" />
|
||||
<script type="text/javascript" src="./screensy.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<div class="popup">
|
||||
<h1>JavaScript is not supported or not enabled.</h1>
|
||||
<p>
|
||||
Screensy needs JavaScript support, but your browser does not provide it. Use a browser that supports
|
||||
JavaScript and make sure it is enabled.
|
||||
</p>
|
||||
</div>
|
||||
</noscript>
|
||||
<div id="access-denied" class="popup hidden">
|
||||
<h1>You denied access to your screen.</h1>
|
||||
<p>
|
||||
Screensy needs access to your screen in order to share it, but you denied access. Try reloading this
|
||||
webpage and provide access when your browser asks you for it. If your browser does not ask you for
|
||||
access, check your browser settings.
|
||||
</p>
|
||||
</div>
|
||||
<div id="screensharing-not-supported" class="popup hidden">
|
||||
<h1>Screensharing is not supported or not enabled.</h1>
|
||||
<p>
|
||||
Screensy cannot share your screen, because your browser does not support it, or it is not enabled. Use a
|
||||
browser that supports screensharing and make sure it is enabled.
|
||||
</p>
|
||||
</div>
|
||||
<div id="websockets-not-supported" class="popup hidden">
|
||||
<h1>WebSocket is not supported or not enabled.</h1>
|
||||
<p>
|
||||
Screensy needs WebSocket support, but your browser does not provide it. Use a browser that supports
|
||||
WebSocket and make sure it is enabled.
|
||||
</p>
|
||||
</div>
|
||||
<div id="webrtc-not-supported" class="popup hidden">
|
||||
<h1>WebRTC is not supported or not enabled.</h1>
|
||||
<p>
|
||||
Screensy needs WebRTC support, but your browser does not provide it. Use a browser that supports WebRTC
|
||||
and make sure it is enabled.
|
||||
</p>
|
||||
</div>
|
||||
<div id="mediastream-not-supported" class="popup hidden">
|
||||
<h1>MediaStream is not supported or not enabled.</h1>
|
||||
<p>
|
||||
Screensy needs MediaStream support, but your browser does not provide it. Use a browser that supports
|
||||
MediaStream and make sure it is enabled.
|
||||
</p>
|
||||
</div>
|
||||
<div id="websocket-connect-failed" class="popup hidden">
|
||||
<h1>Could not establish a WebSocket connection.</h1>
|
||||
<p>
|
||||
Screensy needs to establish a WebSocket connection to connect to other users, but it was not able to.
|
||||
Try using a different device or contact the administrator of this website.
|
||||
</p>
|
||||
</div>
|
||||
<div id="broadcaster-disconnected" class="popup hidden">
|
||||
<h1>The broadcaster has disconnected.</h1>
|
||||
</div>
|
||||
<div id="click-to-share" class="popup clickable hidden">
|
||||
<h1>Click anywhere to share your screen.</h1>
|
||||
</div>
|
||||
<video id="stream" autoplay="autoplay" muted="muted" controls="controls" playsinline="playsinline">
|
||||
<div class="popup">
|
||||
<h1>Video tags are not supported.</h1>
|
||||
<p>
|
||||
Screensy needs video tag support, but your browser does not provide it. Use a browser that supports
|
||||
video tags.
|
||||
</p>
|
||||
</div>
|
||||
</video>
|
||||
</body>
|
||||
</html>
|
||||
67
public/styles.css
Normal file
67
public/styles.css
Normal file
@ -0,0 +1,67 @@
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
background-color: #333333;
|
||||
}
|
||||
|
||||
#counter {
|
||||
text-shadow: 1px 1px 3px #333333;
|
||||
color: white;
|
||||
position: absolute;
|
||||
padding: 10px;
|
||||
margin: 0;
|
||||
font-family: monospace;
|
||||
font-size: 1.5em;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
#stream {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.popup {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-family: system-ui, sans-serif;
|
||||
user-select: none;
|
||||
max-width: 65ch;
|
||||
margin: 0 auto;
|
||||
padding: 0 30px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.popup h1 {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.popup p {
|
||||
color: #e1e1e1;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 800px) {
|
||||
.popup h1 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.popup p {
|
||||
text-align: justify;
|
||||
}
|
||||
}
|
||||
802
src/client/screensy.ts
Normal file
802
src/client/screensy.ts
Normal file
@ -0,0 +1,802 @@
|
||||
/**
|
||||
* @source ./screensy.ts
|
||||
*
|
||||
* @licstart The following is the entire license notice for the JavaScript
|
||||
* code in this page.
|
||||
*
|
||||
* Copyright (C) 2021 Stef Gijsberts, Marijn van Wezel
|
||||
*
|
||||
* The JavaScript code in this page is free software: you can redistribute it
|
||||
* and/or modify it under the terms of the GNU General Public License (GNU GPL)
|
||||
* as published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version. The code is distributed
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU GPL for more details.
|
||||
*
|
||||
* As additional permission under GNU GPL version 3 section 7, you may
|
||||
* distribute non-source (e.g., minimized or compacted) forms of that code
|
||||
* without the copy of the GNU GPL normally required by section 4, provided you
|
||||
* include this license notice and a URL through which recipients can access
|
||||
* the Corresponding Source.
|
||||
*
|
||||
* @licend The above is the entire license notice for the JavaScript code in
|
||||
* this page.
|
||||
*/
|
||||
|
||||
interface MessageJoin {
|
||||
type: 'join'
|
||||
roomId: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells the broadcaster a viewer has connected
|
||||
*/
|
||||
interface MessageViewer {
|
||||
type: 'viewer'
|
||||
viewerId: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask the server to resend the VIEWER messages
|
||||
*/
|
||||
interface MessageRequestViewers {
|
||||
type: 'requestviewers'
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells the broadcaster a viewer has disconnected
|
||||
*/
|
||||
interface MessageViewerDisconnected {
|
||||
type: 'viewerdisconnected'
|
||||
viewerId: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells the viewer the broadcaster has disconnected.
|
||||
*/
|
||||
interface MessageBroadcasterDisconnected {
|
||||
type: 'broadcasterdisconnected'
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a WebRTC message between the viewer and the server
|
||||
*/
|
||||
interface MessageWebRTCViewer {
|
||||
type: 'webrtcviewer'
|
||||
kind: 'offer' | 'answer' | 'candidate'
|
||||
message: any
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a WebRTC message between the server and the broadcaster
|
||||
*/
|
||||
interface MessageWebRTCBroadcaster {
|
||||
type: 'webrtcbroadcaster'
|
||||
viewerId: string
|
||||
kind: 'offer' | 'answer' | 'candidate'
|
||||
message: any
|
||||
}
|
||||
|
||||
type Message =
|
||||
| MessageViewer
|
||||
| MessageViewerDisconnected
|
||||
| MessageBroadcasterDisconnected
|
||||
| MessageWebRTCViewer
|
||||
| MessageWebRTCBroadcaster
|
||||
| MessageRequestViewers
|
||||
| MessageJoin
|
||||
|
||||
interface MessageSender {
|
||||
(msg: Message): Promise<void>
|
||||
}
|
||||
|
||||
/**
|
||||
* Pause execution until the listener/event has fired on the given target.
|
||||
*
|
||||
* @see https://stackoverflow.com/a/63718685
|
||||
*/
|
||||
function wait(target: EventTarget, listenerName: string): Promise<Event> {
|
||||
// Lambda that returns a listener for the given resolve lambda
|
||||
const listener = (resolve: (value: Event | PromiseLike<Event>) => void) => (event: Event) => {
|
||||
target.removeEventListener(listenerName, listener(resolve))
|
||||
resolve(event)
|
||||
}
|
||||
|
||||
return new Promise((resolve, _reject) => {
|
||||
target.addEventListener(listenerName, listener(resolve))
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the popup with the given name. Does nothing if the popup does not
|
||||
* exist.
|
||||
*
|
||||
* @param name Name of the popup to display
|
||||
*/
|
||||
function showPopup(name: string): void {
|
||||
const element = document.getElementById(name)
|
||||
|
||||
if (element == null) {
|
||||
return
|
||||
}
|
||||
|
||||
element.classList.remove('hidden')
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides the popup with the given name. Does nothing if the popup is not visible or if
|
||||
* the popup does not exist.
|
||||
*
|
||||
* @param name Name of the popup to hide
|
||||
*/
|
||||
function hidePopup(name: string): void {
|
||||
const element = document.getElementById(name)
|
||||
|
||||
if (element == null) {
|
||||
return
|
||||
}
|
||||
|
||||
element.classList.add('hidden')
|
||||
}
|
||||
|
||||
interface Client {
|
||||
/**
|
||||
* Handles the messages received from the signaling server.
|
||||
*
|
||||
* @param msg
|
||||
*/
|
||||
handleMessage(msg: Message): void
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a broadcaster. The broadcaster is responsible for capturing and sending
|
||||
* their screen (and maybe audio) to all peers.
|
||||
*/
|
||||
class Broadcaster implements Client {
|
||||
public onviewerjoin: ((viewerId: string) => void) | null = null
|
||||
public onviewerleave: ((viewerId: string) => void) | null = null
|
||||
|
||||
private readonly sendMessage: MessageSender
|
||||
private readonly rtcConfig: RTCConfiguration
|
||||
private readonly mediaStream: MediaStream
|
||||
|
||||
private readonly viewers: { [id: string]: RTCPeerConnection } = {}
|
||||
|
||||
/**
|
||||
* Broadcaster constructor.
|
||||
*
|
||||
* @param sendMessage Function to send a message to the server
|
||||
* @param rtcConfig The WebRTC configuration to use for the WebRTC connection
|
||||
* @param mediaStream The MediaStream to broadcast
|
||||
*/
|
||||
constructor(sendMessage: MessageSender, rtcConfig: RTCConfiguration, mediaStream: MediaStream) {
|
||||
this.sendMessage = sendMessage
|
||||
this.rtcConfig = rtcConfig
|
||||
this.mediaStream = mediaStream
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
async handleMessage(msg: MessageViewer | MessageViewerDisconnected | MessageWebRTCBroadcaster): Promise<void> {
|
||||
switch (msg.type) {
|
||||
case 'viewer':
|
||||
await this.addViewer(msg.viewerId)
|
||||
break
|
||||
case 'viewerdisconnected':
|
||||
await this.removeViewer(msg.viewerId)
|
||||
break
|
||||
case 'webrtcbroadcaster':
|
||||
await this.handleWebRTCMessage(msg)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a viewer to the peer-to-peer connection.
|
||||
*
|
||||
* @param viewerId
|
||||
* @private
|
||||
*/
|
||||
private async addViewer(viewerId: string): Promise<void> {
|
||||
const viewerConnection = new RTCPeerConnection(this.rtcConfig)
|
||||
|
||||
for (const track of await this.mediaStream.getTracks()) {
|
||||
viewerConnection.addTrack(track, this.mediaStream)
|
||||
}
|
||||
|
||||
viewerConnection.onicecandidate = (event: RTCPeerConnectionIceEvent) => {
|
||||
if (!event.candidate) return
|
||||
|
||||
this.sendMessage({
|
||||
type: 'webrtcbroadcaster',
|
||||
kind: 'candidate',
|
||||
viewerId: viewerId,
|
||||
message: event.candidate,
|
||||
})
|
||||
}
|
||||
|
||||
viewerConnection.onicegatheringstatechange = async (_event: Event) => {
|
||||
if (viewerConnection.iceGatheringState !== 'complete') {
|
||||
return
|
||||
}
|
||||
|
||||
for (const sender of await viewerConnection.getSenders()) {
|
||||
if (sender.track == null) {
|
||||
continue
|
||||
}
|
||||
|
||||
const rtcSendParameters = sender.getParameters()
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/RTCRtpSender/setParameters#currently_compatible_implementation
|
||||
if (!rtcSendParameters.encodings) {
|
||||
rtcSendParameters.encodings = [{}]
|
||||
}
|
||||
|
||||
if (sender.track.kind === 'audio') {
|
||||
rtcSendParameters.encodings[0].maxBitrate = 960000 // 960 Kbps, does gek
|
||||
} else if (sender.track.kind === 'video') {
|
||||
// @ts-ignore
|
||||
rtcSendParameters.encodings[0].maxFramerate = 30
|
||||
rtcSendParameters.encodings[0].maxBitrate = 100000000 // 100 Mbps
|
||||
}
|
||||
|
||||
await sender.setParameters(rtcSendParameters)
|
||||
}
|
||||
}
|
||||
|
||||
const offer = await viewerConnection.createOffer()
|
||||
await viewerConnection.setLocalDescription(offer)
|
||||
const localDescription = viewerConnection.localDescription
|
||||
|
||||
if (localDescription == null) {
|
||||
throw 'No local description available.'
|
||||
}
|
||||
|
||||
await this.sendMessage({
|
||||
type: 'webrtcbroadcaster',
|
||||
kind: 'offer',
|
||||
viewerId: viewerId,
|
||||
message: localDescription,
|
||||
})
|
||||
|
||||
this.viewers[viewerId] = viewerConnection
|
||||
|
||||
if (this.onviewerjoin != null) {
|
||||
this.onviewerjoin(viewerId)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a viewer from the peer-to-peer connection.
|
||||
*
|
||||
* @param viewerId
|
||||
* @private
|
||||
*/
|
||||
private async removeViewer(viewerId: string): Promise<void> {
|
||||
if (this.viewers[viewerId] == null) {
|
||||
return
|
||||
}
|
||||
|
||||
this.viewers[viewerId].close()
|
||||
delete this.viewers[viewerId]
|
||||
|
||||
if (this.onviewerleave != null) {
|
||||
this.onviewerleave(viewerId)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles incoming WebRTC messages.
|
||||
*
|
||||
* @param msg
|
||||
* @private
|
||||
*/
|
||||
private async handleWebRTCMessage(msg: MessageWebRTCBroadcaster): Promise<void> {
|
||||
const kind = msg.kind
|
||||
|
||||
switch (kind) {
|
||||
case 'candidate':
|
||||
if (this.viewers[msg.viewerId] == null) {
|
||||
break
|
||||
}
|
||||
|
||||
await this.viewers[msg.viewerId].addIceCandidate(new RTCIceCandidate(msg.message))
|
||||
break
|
||||
case 'answer':
|
||||
if (this.viewers[msg.viewerId] == null) {
|
||||
break
|
||||
}
|
||||
|
||||
await this.viewers[msg.viewerId].setRemoteDescription(msg.message)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a viewer.
|
||||
*/
|
||||
class Viewer implements Client {
|
||||
private readonly sendMessage: MessageSender
|
||||
private readonly rtcConfig: RTCConfiguration
|
||||
private readonly videoElement: HTMLVideoElement
|
||||
|
||||
private broadcasterPeerConnection: RTCPeerConnection | null = null
|
||||
|
||||
/**
|
||||
* Viewer constructor.
|
||||
*
|
||||
* @param sendMessage Function to send a message to the server
|
||||
* @param rtcConfig The WebRTC configuration to use for the WebRTC connection
|
||||
* @param videoElement The element to project the received MediaStream onto
|
||||
*/
|
||||
constructor(sendMessage: MessageSender, rtcConfig: RTCConfiguration, videoElement: HTMLVideoElement) {
|
||||
this.sendMessage = sendMessage
|
||||
this.rtcConfig = rtcConfig
|
||||
this.videoElement = videoElement
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
async handleMessage(msg: MessageBroadcasterDisconnected | MessageWebRTCViewer): Promise<void> {
|
||||
switch (msg.type) {
|
||||
case 'broadcasterdisconnected':
|
||||
await this.handleBroadcasterDisconnect()
|
||||
break
|
||||
case 'webrtcviewer':
|
||||
await this.handleWebRTCMessage(msg)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a disconnect of the broadcaster.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
private async handleBroadcasterDisconnect(): Promise<void> {
|
||||
showPopup('broadcaster-disconnected')
|
||||
document.body.removeChild(this.videoElement)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles incoming WebRTC messages.
|
||||
*
|
||||
* @param msg
|
||||
* @private
|
||||
*/
|
||||
private async handleWebRTCMessage(msg: MessageWebRTCViewer): Promise<void> {
|
||||
const kind = msg.kind
|
||||
|
||||
switch (kind) {
|
||||
case 'candidate':
|
||||
if (this.broadcasterPeerConnection == null) {
|
||||
break
|
||||
}
|
||||
|
||||
await this.broadcasterPeerConnection.addIceCandidate(new RTCIceCandidate(msg.message))
|
||||
break
|
||||
case 'offer':
|
||||
await this.handleOffer(msg)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles incoming WebRTC offer.
|
||||
*
|
||||
* @param msg
|
||||
* @private
|
||||
*/
|
||||
private async handleOffer(msg: MessageWebRTCViewer): Promise<void> {
|
||||
this.broadcasterPeerConnection = new RTCPeerConnection(this.rtcConfig)
|
||||
|
||||
this.broadcasterPeerConnection.ontrack = (event: RTCTrackEvent) => {
|
||||
this.videoElement.srcObject = event.streams[0]
|
||||
}
|
||||
|
||||
this.broadcasterPeerConnection.onicecandidate = (event: RTCPeerConnectionIceEvent) => {
|
||||
if (event.candidate == null) return
|
||||
|
||||
this.sendMessage({
|
||||
type: 'webrtcviewer',
|
||||
kind: 'candidate',
|
||||
message: event.candidate,
|
||||
})
|
||||
}
|
||||
|
||||
await this.broadcasterPeerConnection.setRemoteDescription(msg.message)
|
||||
|
||||
const answer = await this.broadcasterPeerConnection.createAnswer()
|
||||
await this.broadcasterPeerConnection.setLocalDescription(answer)
|
||||
|
||||
if (this.broadcasterPeerConnection == null) {
|
||||
throw 'No local description available.'
|
||||
}
|
||||
|
||||
await this.sendMessage({
|
||||
type: 'webrtcviewer',
|
||||
kind: 'answer',
|
||||
message: this.broadcasterPeerConnection.localDescription,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
class Room {
|
||||
private readonly roomId: string
|
||||
private readonly videoElement: HTMLVideoElement
|
||||
private readonly webSocket: WebSocket
|
||||
private readonly sendMessage: MessageSender
|
||||
private readonly rtcConfig: RTCConfiguration
|
||||
|
||||
/**
|
||||
* Room constructor.
|
||||
*
|
||||
* @param roomId The ID of this room
|
||||
*/
|
||||
constructor(roomId: string) {
|
||||
this.roomId = roomId
|
||||
this.videoElement = <HTMLVideoElement>document.getElementById('stream')
|
||||
|
||||
const secureProtocol = window.location.protocol === 'https:'
|
||||
const protocol = secureProtocol ? 'wss' : 'ws'
|
||||
const webSocketUrl = `${protocol}://${location.host}`
|
||||
console.log('connecting to websocket', webSocketUrl)
|
||||
this.webSocket = new WebSocket(webSocketUrl)
|
||||
this.webSocket.onerror = () => showPopup('websocket-connect-failed')
|
||||
|
||||
this.sendMessage = async (message: Message) => this.webSocket.send(JSON.stringify(message))
|
||||
this.rtcConfig = {
|
||||
iceServers: [
|
||||
{ urls: 'stun:' + location.hostname },
|
||||
{
|
||||
urls: 'turn:' + location.hostname,
|
||||
username: 'screensy',
|
||||
credential: 'screensy',
|
||||
},
|
||||
],
|
||||
iceCandidatePoolSize: 8,
|
||||
}
|
||||
|
||||
this.videoElement.onpause = (_event: Event) => this.videoElement.play()
|
||||
window.onunload = window.onbeforeunload = () => this.webSocket.close()
|
||||
}
|
||||
|
||||
/**
|
||||
* Joins the room.
|
||||
*/
|
||||
async join() {
|
||||
// Wait until the socket opens
|
||||
await wait(this.webSocket, 'open')
|
||||
|
||||
this.webSocket.onmessage = async (event: MessageEvent) => {
|
||||
const messageData = JSON.parse(event.data)
|
||||
const isBroadcaster = messageData.type === 'broadcast'
|
||||
|
||||
if (isBroadcaster && !('getDisplayMedia' in navigator.mediaDevices)) {
|
||||
showPopup('screensharing-not-supported')
|
||||
return
|
||||
}
|
||||
|
||||
const client = isBroadcaster ? await this.setupBroadcaster() : await this.setupViewer()
|
||||
|
||||
this.webSocket.onmessage = (event: MessageEvent) => client.handleMessage(JSON.parse(event.data))
|
||||
|
||||
if (isBroadcaster) {
|
||||
await this.sendMessage({ type: 'requestviewers' })
|
||||
}
|
||||
|
||||
this.setDocumentTitle()
|
||||
}
|
||||
|
||||
await this.sendMessage({
|
||||
type: 'join',
|
||||
roomId: this.roomId.toLowerCase(),
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the document's title to the room name.
|
||||
*/
|
||||
private setDocumentTitle() {
|
||||
const roomIdWords = this.roomId.split(/(?=[A-Z])/)
|
||||
document.title = roomIdWords.join(' ') + ' | screensy'
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up a Broadcaster instance.
|
||||
*/
|
||||
private async setupBroadcaster(): Promise<Broadcaster> {
|
||||
const mediaStream = await this.getDisplayMediaStream()
|
||||
const broadcaster = new Broadcaster(this.sendMessage, this.rtcConfig, mediaStream)
|
||||
const counterElement: HTMLParagraphElement = document.createElement('p')
|
||||
|
||||
counterElement.id = 'counter'
|
||||
counterElement.innerText = '0'
|
||||
|
||||
broadcaster.onviewerjoin = (_viewerId: string) => {
|
||||
const currentCounter = parseInt(counterElement.innerText)
|
||||
counterElement.innerText = (currentCounter + 1).toString()
|
||||
}
|
||||
|
||||
broadcaster.onviewerleave = (_viewerId: string) => {
|
||||
const currentCounter = parseInt(counterElement.innerText)
|
||||
counterElement.innerText = (currentCounter - 1).toString()
|
||||
}
|
||||
|
||||
document.body.prepend(counterElement)
|
||||
this.videoElement.srcObject = mediaStream
|
||||
|
||||
return broadcaster
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up a Viewer instance.
|
||||
*/
|
||||
private async setupViewer(): Promise<Viewer> {
|
||||
// The client is assigned the role of viewer
|
||||
return new Viewer(this.sendMessage, this.rtcConfig, this.videoElement)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the user's display MediaStream.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
private async getDisplayMediaStream(): Promise<MediaStream> {
|
||||
showPopup('click-to-share')
|
||||
|
||||
await wait(document, 'click')
|
||||
|
||||
const videoConstraints: MediaTrackConstraints | boolean = true
|
||||
const audioConstraints: MediaTrackConstraints | boolean = {
|
||||
channelCount: { ideal: 2 },
|
||||
sampleRate: { ideal: 192000 },
|
||||
// @ts-ignore
|
||||
noiseSuppression: { ideal: false },
|
||||
echoCancellation: { ideal: false },
|
||||
autoGainControl: { ideal: false },
|
||||
}
|
||||
|
||||
const mediaConstraints: MediaStreamConstraints = {
|
||||
video: videoConstraints,
|
||||
audio: audioConstraints,
|
||||
}
|
||||
|
||||
const mediaDevices: MediaDevices = window.navigator.mediaDevices
|
||||
|
||||
// @ts-ignore getDisplayMedia is not supported by TypeScript :(
|
||||
const displayMedia = mediaDevices.getDisplayMedia(mediaConstraints)
|
||||
|
||||
// If the promise is resolved, remove the popup from the screen
|
||||
displayMedia.then(() => {
|
||||
hidePopup('click-to-share')
|
||||
})
|
||||
|
||||
// If the promise is rejected, tell the user about the failure
|
||||
displayMedia.catch(() => {
|
||||
hidePopup('click-to-share')
|
||||
showPopup('access-denied')
|
||||
})
|
||||
|
||||
return displayMedia
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a random readable room name and returns the words as a string array.
|
||||
*
|
||||
* @source https://github.com/jitsi/js-utils/blob/master/random/roomNameGenerator.js
|
||||
*/
|
||||
function generateRoomName(): string {
|
||||
const adjectives = [
|
||||
'large',
|
||||
'small',
|
||||
'beautiful',
|
||||
'heavenly',
|
||||
'red',
|
||||
'yellow',
|
||||
'green',
|
||||
'orange',
|
||||
'purple',
|
||||
'massive',
|
||||
'tasty',
|
||||
'cheap',
|
||||
'fancy',
|
||||
'expensive',
|
||||
'crazy',
|
||||
'round',
|
||||
'triangular',
|
||||
'powered',
|
||||
'blue',
|
||||
'heavy',
|
||||
'square',
|
||||
'rectangular',
|
||||
'lit',
|
||||
'authentic',
|
||||
'broken',
|
||||
'busy',
|
||||
'original',
|
||||
'special',
|
||||
'thick',
|
||||
'thin',
|
||||
'pleasant',
|
||||
'sharp',
|
||||
'steady',
|
||||
'happy',
|
||||
'delighted',
|
||||
'stunning',
|
||||
]
|
||||
|
||||
const pluralNouns = [
|
||||
'monsters',
|
||||
'people',
|
||||
'cars',
|
||||
'buttons',
|
||||
'vegetables',
|
||||
'students',
|
||||
'computers',
|
||||
'robots',
|
||||
'lamps',
|
||||
'doors',
|
||||
'wizards',
|
||||
'books',
|
||||
'shirts',
|
||||
'pens',
|
||||
'guitars',
|
||||
'bottles',
|
||||
'microphones',
|
||||
'pants',
|
||||
'drums',
|
||||
'plants',
|
||||
'batteries',
|
||||
'barrels',
|
||||
'birds',
|
||||
'coins',
|
||||
'clothes',
|
||||
'deals',
|
||||
'crosses',
|
||||
'devices',
|
||||
'desktops',
|
||||
'diamonds',
|
||||
'fireworks',
|
||||
'funds',
|
||||
'guitars',
|
||||
'pianos',
|
||||
'harmonies',
|
||||
'levels',
|
||||
'mayors',
|
||||
'mechanics',
|
||||
'networks',
|
||||
'ponds',
|
||||
'trees',
|
||||
'proofs',
|
||||
'flowers',
|
||||
'houses',
|
||||
'speakers',
|
||||
'phones',
|
||||
'chargers',
|
||||
]
|
||||
|
||||
const verbs = [
|
||||
'break',
|
||||
'roll',
|
||||
'flip',
|
||||
'grow',
|
||||
'bake',
|
||||
'create',
|
||||
'cook',
|
||||
'smack',
|
||||
'drink',
|
||||
'close',
|
||||
'display',
|
||||
'run',
|
||||
'move',
|
||||
'flop',
|
||||
'wrap',
|
||||
'enter',
|
||||
'dig',
|
||||
'fly',
|
||||
'swim',
|
||||
'draw',
|
||||
'celebrate',
|
||||
'communicate',
|
||||
'encompass',
|
||||
'forgive',
|
||||
'negotiate',
|
||||
'pioneer',
|
||||
'photograph',
|
||||
'play',
|
||||
'scratch',
|
||||
'stabilize',
|
||||
'weigh',
|
||||
'wrap',
|
||||
'yield',
|
||||
'return',
|
||||
'update',
|
||||
'understand',
|
||||
'propose',
|
||||
'succeed',
|
||||
'stretch',
|
||||
'submit',
|
||||
]
|
||||
|
||||
const adverbs = [
|
||||
'gingerly',
|
||||
'thoroughly',
|
||||
'heavily',
|
||||
'crazily',
|
||||
'mostly',
|
||||
'fast',
|
||||
'slowly',
|
||||
'merrily',
|
||||
'quickly',
|
||||
'heavenly',
|
||||
'cheerfully',
|
||||
'honestly',
|
||||
'politely',
|
||||
'bravely',
|
||||
'vivaciously',
|
||||
'fortunately',
|
||||
'innocently',
|
||||
'kindly',
|
||||
'eagerly',
|
||||
'elegantly',
|
||||
'vividly',
|
||||
'reasonably',
|
||||
'rudely',
|
||||
'wisely',
|
||||
'thankfully',
|
||||
'wholly',
|
||||
'adorably',
|
||||
'happily',
|
||||
'firmly',
|
||||
'fast',
|
||||
'simply',
|
||||
'wickedly',
|
||||
]
|
||||
|
||||
const idxAdjective = Math.floor(Math.random() * adjectives.length)
|
||||
const idxPluralNoun = Math.floor(Math.random() * pluralNouns.length)
|
||||
const idxVerb = Math.floor(Math.random() * verbs.length)
|
||||
const idxAdverb = Math.floor(Math.random() * adverbs.length)
|
||||
|
||||
const words = [adjectives[idxAdjective], pluralNouns[idxPluralNoun], verbs[idxVerb], adverbs[idxAdverb]]
|
||||
|
||||
// @see https://flaviocopes.com/how-to-uppercase-first-letter-javascript/
|
||||
return words.map((word: string) => word.charAt(0).toUpperCase() + word.slice(1)).join('')
|
||||
}
|
||||
|
||||
async function main(_event: Event) {
|
||||
if (window.location.hash == '') {
|
||||
// Redirect the user to a room
|
||||
window.location.replace('#' + generateRoomName())
|
||||
}
|
||||
|
||||
// If the user manually changes the hash, force a reload
|
||||
window.onhashchange = (_event: Event) => {
|
||||
location.reload()
|
||||
}
|
||||
|
||||
if (!('WebSocket' in window)) {
|
||||
showPopup('websockets-not-supported')
|
||||
return
|
||||
}
|
||||
|
||||
if (!('mediaDevices' in navigator)) {
|
||||
showPopup('mediastream-not-supported')
|
||||
return
|
||||
}
|
||||
|
||||
if (!('RTCPeerConnection' in window)) {
|
||||
showPopup('webrtc-not-supported')
|
||||
return
|
||||
}
|
||||
|
||||
const room = new Room(window.location.hash.substring(1))
|
||||
await room.join()
|
||||
}
|
||||
|
||||
window.addEventListener('DOMContentLoaded', main)
|
||||
16
src/client/tsconfig.json
Normal file
16
src/client/tsconfig.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "ES2015",
|
||||
"target": "ES2015",
|
||||
"sourceMap": true,
|
||||
"noImplicitAny": true,
|
||||
"noImplicitThis": true,
|
||||
"strictNullChecks": true,
|
||||
"alwaysStrict": true,
|
||||
"strictFunctionTypes": true,
|
||||
"strict": true,
|
||||
"removeComments": false,
|
||||
"outDir": "../../build/public"
|
||||
},
|
||||
"files": ["screensy.ts"]
|
||||
}
|
||||
449
src/server/server.ts
Normal file
449
src/server/server.ts
Normal file
@ -0,0 +1,449 @@
|
||||
import * as websocket from 'ws'
|
||||
import * as http from 'http'
|
||||
import serveHandler = require('serve-handler')
|
||||
|
||||
interface Config {
|
||||
host: string
|
||||
port: number
|
||||
}
|
||||
|
||||
/**
|
||||
* The main entry point.
|
||||
*/
|
||||
function main(): void {
|
||||
const config = require('./config.json') as Config
|
||||
|
||||
console.log(`Starting server on ${config.host}:${config.port}`)
|
||||
const webServer = http.createServer((req, res) => {
|
||||
serveHandler(req, res, {
|
||||
public: 'public',
|
||||
})
|
||||
})
|
||||
|
||||
webServer.on('request', (req) => {
|
||||
console.log(`[${req.method}] ${req.url}`)
|
||||
})
|
||||
webServer.on('upgrade', (req, _) => {
|
||||
console.log(`[${req.method}] ${req.url}\n` + ` Upgrade: ${req.headers.upgrade}`)
|
||||
})
|
||||
webServer.listen(config.port, config.host)
|
||||
|
||||
const websocketServer = new websocket.Server({
|
||||
server: webServer,
|
||||
})
|
||||
|
||||
const broadcastServer = new Server()
|
||||
websocketServer.on('connection', (socket: WebSocket) => broadcastServer.onConnection(socket))
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells the server the client wants to join the given room
|
||||
*/
|
||||
interface MessageJoin {
|
||||
type: 'join'
|
||||
roomId: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells the client that it is a broadcaster
|
||||
*/
|
||||
interface MessageBroadcast {
|
||||
type: 'broadcast'
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells the client that it is a viewer
|
||||
*/
|
||||
interface MessageView {
|
||||
type: 'view'
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask the server to resend the VIEWER messages
|
||||
*/
|
||||
interface MessageRequestViewers {
|
||||
type: 'requestviewers'
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells the broadcaster a viewer has connected
|
||||
*/
|
||||
interface MessageViewer {
|
||||
type: 'viewer'
|
||||
viewerId: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a WebRTC message between the viewer and the server
|
||||
*/
|
||||
interface MessageWebRTCViewer {
|
||||
type: 'webrtcviewer'
|
||||
kind: 'offer' | 'answer' | 'candidate'
|
||||
message: any
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a WebRTC message between the server and the broadcaster
|
||||
*/
|
||||
interface MessageWebRTCBroadcaster {
|
||||
type: 'webrtcbroadcaster'
|
||||
viewerId: string
|
||||
kind: 'offer' | 'answer' | 'candidate'
|
||||
message: any
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells the broadcaster a viewer has disconnected
|
||||
*/
|
||||
interface MessageViewerDisconnected {
|
||||
type: 'viewerdisconnected'
|
||||
viewerId: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells the viewer the broadcaster has disconnected.
|
||||
*/
|
||||
interface MessageBroadcasterDisconnected {
|
||||
type: 'broadcasterdisconnected'
|
||||
}
|
||||
|
||||
type FromBroadcasterMessage = MessageJoin | MessageWebRTCBroadcaster | MessageRequestViewers
|
||||
type FromViewerMessage = MessageJoin | MessageWebRTCViewer
|
||||
|
||||
class Server {
|
||||
/**
|
||||
* Object containing kv-pairs of room IDs and room class instances.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
private rooms = new Map<string, Room>()
|
||||
|
||||
/**
|
||||
* Handles a new WebSocket connection.
|
||||
*
|
||||
* @param socket
|
||||
*/
|
||||
onConnection(socket: WebSocket): void {
|
||||
socket.onmessage = (event: MessageEvent) => {
|
||||
let message
|
||||
|
||||
try {
|
||||
message = JSON.parse(event.data)
|
||||
} catch (e) {
|
||||
// The JSON message is invalid
|
||||
return
|
||||
}
|
||||
|
||||
if (message.type != 'join') {
|
||||
// No messages are valid until a client has sent a "JOIN"
|
||||
return
|
||||
}
|
||||
|
||||
const roomId = message.roomId
|
||||
|
||||
if (roomId == null || roomId.length < 1) {
|
||||
// No, or an invalid roomId was given in the message
|
||||
return
|
||||
}
|
||||
|
||||
if (this.rooms.has(roomId)) {
|
||||
this.rooms.get(roomId)?.addViewer(socket)
|
||||
} else {
|
||||
this.newRoom(roomId, socket)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new room with the given roomId and broadcaster. Throws an exception
|
||||
* if the roomId is already taken.
|
||||
*
|
||||
* @param roomId
|
||||
* @param broadcaster
|
||||
*/
|
||||
newRoom(roomId: string, broadcaster: WebSocket): void {
|
||||
if (this.rooms.has(roomId)) {
|
||||
throw (
|
||||
'Attempted to create room with the same ID as an existing room. ' +
|
||||
'This likely indicates an error in the server implementation.'
|
||||
)
|
||||
}
|
||||
|
||||
this.rooms.set(roomId, new Room(broadcaster))
|
||||
broadcaster.onclose = (_event: CloseEvent) => this.closeRoom(roomId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the room with the given roomId.
|
||||
*
|
||||
* @param roomId The ID of the room to close
|
||||
*/
|
||||
closeRoom(roomId: string): void {
|
||||
this.rooms.get(roomId)?.closeRoom()
|
||||
this.rooms.delete(roomId)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a screensharing room.
|
||||
*/
|
||||
class Room {
|
||||
private counter: number = 0
|
||||
|
||||
private broadcaster: WebSocket
|
||||
private viewers: { [id: string]: WebSocket } = {}
|
||||
|
||||
/**
|
||||
* Room constructor.
|
||||
*
|
||||
* @param broadcaster The WebSocket of the broadcaster of this room
|
||||
*/
|
||||
constructor(broadcaster: WebSocket) {
|
||||
this.broadcaster = broadcaster
|
||||
|
||||
broadcaster.onmessage = (event: MessageEvent) => {
|
||||
let message
|
||||
|
||||
try {
|
||||
message = JSON.parse(event.data)
|
||||
} catch (e) {
|
||||
// The JSON message is invalid
|
||||
return
|
||||
}
|
||||
|
||||
this.handleBroadcasterMessage(message)
|
||||
}
|
||||
|
||||
// Tell the client that he has been assigned the role "broadcaster"
|
||||
const message: MessageBroadcast = {
|
||||
type: 'broadcast',
|
||||
}
|
||||
|
||||
broadcaster.send(JSON.stringify(message))
|
||||
}
|
||||
|
||||
/**
|
||||
* Called whenever a broadcaster sends a message.
|
||||
*
|
||||
* @param msg The message
|
||||
*/
|
||||
handleBroadcasterMessage(msg: any): void {
|
||||
if (!instanceOfFromBroadcasterMessage(msg)) {
|
||||
// The given message is not valid
|
||||
return
|
||||
}
|
||||
|
||||
switch (msg.type) {
|
||||
case 'webrtcbroadcaster': {
|
||||
const viewerId = msg.viewerId
|
||||
const viewer = this.viewers[viewerId]
|
||||
|
||||
if (viewer == null) {
|
||||
// No viewer with the ID "viewerId" exists
|
||||
break
|
||||
}
|
||||
|
||||
const message: MessageWebRTCViewer = {
|
||||
type: 'webrtcviewer',
|
||||
kind: msg.kind,
|
||||
message: msg.message,
|
||||
}
|
||||
|
||||
viewer.send(JSON.stringify(message))
|
||||
|
||||
break
|
||||
}
|
||||
case 'requestviewers': {
|
||||
for (const viewerId in this.viewers) {
|
||||
const messageViewer: MessageViewer = {
|
||||
type: 'viewer',
|
||||
viewerId: viewerId,
|
||||
}
|
||||
|
||||
this.broadcaster.send(JSON.stringify(messageViewer))
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to add a new viewer to this room.
|
||||
*
|
||||
* @param viewer The WebSocket of the viewer that joined the room
|
||||
*/
|
||||
addViewer(viewer: WebSocket): void {
|
||||
const id: string = (this.counter++).toString()
|
||||
|
||||
viewer.onmessage = (event: MessageEvent) => {
|
||||
let message
|
||||
|
||||
try {
|
||||
message = JSON.parse(event.data)
|
||||
} catch (e) {
|
||||
// The JSON message is invalid
|
||||
return
|
||||
}
|
||||
|
||||
this.handleViewerMessage(id, message)
|
||||
}
|
||||
|
||||
viewer.onclose = (_event: CloseEvent) => this.handleViewerDisconnect(id)
|
||||
|
||||
// Tell the client that he has been assigned the role "viewer"
|
||||
const messageView: MessageView = {
|
||||
type: 'view',
|
||||
}
|
||||
|
||||
viewer.send(JSON.stringify(messageView))
|
||||
|
||||
// Tell the broadcaster a viewer has connected
|
||||
const messageViewer: MessageViewer = {
|
||||
type: 'viewer',
|
||||
viewerId: id,
|
||||
}
|
||||
|
||||
this.broadcaster.send(JSON.stringify(messageViewer))
|
||||
this.viewers[id] = viewer
|
||||
}
|
||||
|
||||
/**
|
||||
* Called whenever a viewer sends a message.
|
||||
*
|
||||
* @param viewerId The ID of the viewer that sent the message
|
||||
* @param msg The message
|
||||
*/
|
||||
handleViewerMessage(viewerId: string, msg: any): void {
|
||||
if (!instanceOfFromViewerMessage(msg)) {
|
||||
// The given message is not valid
|
||||
return
|
||||
}
|
||||
|
||||
switch (msg.type) {
|
||||
case 'webrtcviewer': {
|
||||
const message: MessageWebRTCBroadcaster = {
|
||||
type: 'webrtcbroadcaster',
|
||||
kind: msg.kind,
|
||||
message: msg.message,
|
||||
viewerId: viewerId,
|
||||
}
|
||||
|
||||
this.broadcaster.send(JSON.stringify(message))
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called whenever a viewer disconnects.
|
||||
*
|
||||
* @param viewerId The ID of the viewer that disconnected
|
||||
*/
|
||||
handleViewerDisconnect(viewerId: string): void {
|
||||
if (!(viewerId in this.viewers)) {
|
||||
throw (
|
||||
'Attempted to remove nonexistent viewer from room. ' +
|
||||
'This likely indicates an error in the server implementation.'
|
||||
)
|
||||
}
|
||||
|
||||
delete this.viewers[viewerId]
|
||||
|
||||
// Notify the broadcaster of the disconnect
|
||||
const message: MessageViewerDisconnected = {
|
||||
type: 'viewerdisconnected',
|
||||
viewerId: viewerId,
|
||||
}
|
||||
|
||||
this.broadcaster.send(JSON.stringify(message))
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the room and tells all viewers the broadcaster has disconnected.
|
||||
*/
|
||||
closeRoom(): void {
|
||||
for (const viewerId in this.viewers) {
|
||||
const viewer = this.viewers[viewerId]
|
||||
const messageBroadcasterDisconnected: MessageBroadcasterDisconnected = {
|
||||
type: 'broadcasterdisconnected',
|
||||
}
|
||||
|
||||
viewer.send(JSON.stringify(messageBroadcasterDisconnected))
|
||||
viewer.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if and only if the given object is a FromBroadcasterMessage.
|
||||
*
|
||||
* @param object
|
||||
*/
|
||||
function instanceOfFromBroadcasterMessage(object: any): object is FromBroadcasterMessage {
|
||||
return (
|
||||
instanceOfMessageJoin(object) ||
|
||||
instanceOfMessageWebRTCBroadcaster(object) ||
|
||||
instanceOfMessageRequestViewers(object)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if and only if the given object is a FromViewerMessage.
|
||||
*
|
||||
* @param object
|
||||
*/
|
||||
function instanceOfFromViewerMessage(object: any): object is FromViewerMessage {
|
||||
return instanceOfMessageJoin(object) || instanceOfMessageWebRTCViewer(object)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if and only if the given object is a MessageJoin.
|
||||
*
|
||||
* @param object
|
||||
*/
|
||||
function instanceOfMessageJoin(object: any): object is MessageJoin {
|
||||
const goodType = 'type' in object && object.type === 'join'
|
||||
const goodRoomId = 'roomId' in object && typeof object.roomId === 'string'
|
||||
|
||||
return goodType && goodRoomId
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if and only if the given object is a MessageWebRTCBroadcaster.
|
||||
*
|
||||
* @param object
|
||||
*/
|
||||
function instanceOfMessageWebRTCBroadcaster(object: any): object is MessageWebRTCBroadcaster {
|
||||
const goodType = 'type' in object && object.type === 'webrtcbroadcaster'
|
||||
const goodViewerId = 'viewerId' in object && typeof object.viewerId === 'string'
|
||||
const goodKind = 'kind' in object && ['offer', 'answer', 'candidate'].includes(object.kind)
|
||||
const goodMessage = 'message' in object
|
||||
|
||||
return goodType && goodViewerId && goodKind && goodMessage
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if and only if the given object is a MessageRequestViewers.
|
||||
*
|
||||
* @param object
|
||||
*/
|
||||
function instanceOfMessageRequestViewers(object: any): object is MessageRequestViewers {
|
||||
return 'type' in object && object.type === 'requestviewers'
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if and only if the given object is a MessageWebRTCViewer.
|
||||
*
|
||||
* @param object
|
||||
*/
|
||||
function instanceOfMessageWebRTCViewer(object: any): object is MessageWebRTCViewer {
|
||||
const goodType = 'type' in object && object.type === 'webrtcviewer'
|
||||
const goodKind = 'kind' in object && ['offer', 'answer', 'candidate'].includes(object.kind)
|
||||
const goodMessage = 'message' in object
|
||||
|
||||
return goodType && goodKind && goodMessage
|
||||
}
|
||||
|
||||
main()
|
||||
18
src/server/tsconfig.json
Normal file
18
src/server/tsconfig.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "ES2018",
|
||||
"sourceMap": true,
|
||||
"noImplicitAny": true,
|
||||
"noImplicitThis": true,
|
||||
"strictNullChecks": true,
|
||||
"alwaysStrict": true,
|
||||
"strictFunctionTypes": true,
|
||||
"strictBindCallApply": true,
|
||||
"strictPropertyInitialization": true,
|
||||
"strict": true,
|
||||
"lib": ["ES2018", "DOM"],
|
||||
"outDir": "../../build"
|
||||
},
|
||||
"files": ["server.ts"]
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user