initial commit

This commit is contained in:
AlexandreRouma
2025-10-31 18:38:33 -04:00
commit 140bc3c3f5
16 changed files with 1637 additions and 0 deletions

139
www/css/style.css Normal file
View File

@@ -0,0 +1,139 @@
/* Debugging */
body {
background: #202025;
font-family: Arial;
color: white;
padding: 0;
margin: 0;
}
#localPlayback {
background: black;
width: 200%;
border-radius: 0.25em;
transform: translate(-25%, 0);
}
#playback {
position: fixed;
margin: 0;
padding: 0;
width: 100%;
height: 100%;
background: black;
}
#connForm, #pinValForm, #streamForm, #idleScreen, #pinScreen {
width: 25em;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
input {
/* Sizing */
box-sizing: border-box;
width: 100%;
padding: 0.7em;
/* Border parameters */
border-style: solid;
border-width: 1px;
border-color: #949494;
border-radius: 0.25em;
/* Text parameters */
color: black;
font-size:medium;
text-align:center;
/* Background parameters */
background: #e6e6e6;
}
button {
/* Sizing */
box-sizing: border-box;
width: 100%;
padding: 0.7em;
/* Border parameters */
border-style: solid;
border-width: 1px;
border-color: #2189ff;
border-radius: 0.25em;
/* Text parameters */
color: white;
font-size:medium;
text-align:center;
/* Background parameters */
background: #1080FF;
/* Cursor parameters */
cursor:pointer;
}
button:hover {
/* Background parameters */
background: #2189ff;
}
button:active {
/* Background parameters */
background: #2b8eff;
}
button:disabled {
/* Border parameters */
border-color: #797979;
/* Text parameters */
color: rgb(145, 145, 145);
/* Background parameters */
background: #5f5f5f;
/* Cursor parameters */
cursor: not-allowed;
}
.TV {
font-size: 80pt;
margin: 0;
width: 100%;
text-align: center;
}
footer {
/* Text parameters */
color: white;
font-size:small;
/* Position */
position: absolute;
bottom: 0;
right: 0;
/* Margins */
margin: 0.5em;
}
a {
color: #1080FF;
text-decoration: none;
}
h1 {
text-align: center;
}
#dispIDLabel, #pinLabel {
font-size: 30pt;
}
#dispID, #pin {
font-size: 80pt;
}

36
www/display/index.html Normal file
View File

@@ -0,0 +1,36 @@
<!DOCTYPE html>
<html>
<head>
<!-- Configure the page encoding -->
<meta charset="UTF-8">
<!-- Title of the software -->
<title>Quick Screen Share - Screen</title>
<!-- Load the stylesheet from the main style file -->
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<div id="idleScreen">
<span id="dispIDLabel">Display ID:</span><br>
<span id="dispID"></span>
</div>
<div id="pinScreen" hidden>
<span id="pinLabel">PIN:</span><br>
<span id="pin"></span>
</div>
<!-- Video widget -->
<video id="playback" autoplay playsinline hidden></video>
</body>
<!-- Footer to show credit to the software and developers -->
<footer id="credits">
Powered by <a href="https://x.com/ryzerth" target="_blank">Quick Screen Share</a>. Made with ❤️ by <a href="https://x.com/ryzerth" target="_blank">Ryzerth</a>
</footer>
<!-- Main screen code -->
<script src="/scripts/screen.js"></script>
</html>

38
www/index.html Normal file
View File

@@ -0,0 +1,38 @@
<!DOCTYPE html>
<html>
<head>
<!-- Configure the page encoding -->
<meta charset="UTF-8">
<!-- Title of the software -->
<title>Quick Screen Share</title>
<!-- Load the stylesheet from the main style file -->
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<!-- Connection form -->
<div id="connForm">
<p class="TV">📺</p><br>
<input type="text" id="dispName" placeholder="Display ID" required autocomplete="off" oninput="onDispNameChange()"><br><br>
<input type="text" id="otp" placeholder="OTP" required autocomplete="off" oninput="onOTPChange()"><br><br>
<button id="connect" onclick="connect()" disabled autocomplete="off">Connect</button>
</div>
<!-- Streaming form -->
<div id="streamForm" hidden>
<h1>You are live!</h1>
<video id="localPlayback" autoplay playsinline></video><br><br>
<button id="disconnect" onclick="disconnect()">Disconnect</button>
</div>
</body>
<!-- Footer to show credit to the software and developers -->
<footer>
Powered by <a href="https://x.com/ryzerth" target="_blank">Quick Screen Share</a>. Made with ❤️ by <a href="https://x.com/ryzerth" target="_blank">Ryzerth</a>
</footer>
<!-- Main client code -->
<script src="/scripts/client.js"></script>
</html>

185
www/scripts/client.js Normal file
View File

@@ -0,0 +1,185 @@
// Streaming objects
let sock = null;
let stream = null;
let conn = null;
// GUI Objects
let connForm = document.querySelector('#connForm');
let dispNameTb = document.querySelector('#dispName');
let connBtn = document.querySelector('#connect');
let pinValForm = document.querySelector('#pinValForm');
let dispPINTb = document.querySelector('#dispPIN');
let validateBtn = document.querySelector('#validate');
let streamForm = document.querySelector('#streamForm');
let locPlayback = document.querySelector('#localPlayback');
// Make sure the connect button cannot be pressed if there is no display name
function onDispNameChange() {
connBtn.disabled = !(dispNameTb.value.length > 0);
}
function onPINChange() {
validateBtn.disabled = !(dispPINTb.value.length === 6);
}
// Handle enter key pressed
dispNameTb.addEventListener('keyup', (event) => {
if (event.key === 'Enter' && !connBtn.disabled) {
connBtn.click();
}
})
dispPINTb.addEventListener('keyup', (event) => {
if (event.key === 'Enter' && !validateBtn.disabled) {
validateBtn.click();
}
})
// WebSocket message handler
async function onMessage(data) {
// Parse the message
console.log(data)
const msg = JSON.parse(data);
// Process depending on the type
switch (msg.type) {
case 'disp-ok':
// Switch to the PIN screen
connForm.hidden = true;
pinValForm.hidden = false;
dispPINTb.focus();
dispPINTb.select();
break;
case 'disp-error':
// Show the error
// TODO
console.log('ERROR: ' + msg.error)
break;
case 'auth-ok':
// Show the status update
validateBtn.textContent = 'Starting the Stream...';
await startStream();
break;
case 'answer':
console.log('Got answer')
// Pass on the offer to WebRTC
await conn.setRemoteDescription(new RTCSessionDescription(msg.answer));
break;
case 'ice-candidate':
console.log('Got ice candidate')
// Add the ice candidate to the WebRTC connection
await conn.addIceCandidate(msg.candidate);
break;
case 'disconnect':
// Reload the page
location.reload();
break;
default:
console.dir(msg)
break;
}
}
async function startStream() {
// Get the stream for the screen
stream = await navigator.mediaDevices.getDisplayMedia({ video: { cursor: 'always' } });
// Create the connection
conn = new RTCPeerConnection({'iceServers': [{'urls': 'stun:stun.l.google.com:19302'}]});
// Handle ice candidates
conn.addEventListener('icecandidate', async (event) => {
// If there is a new candidate, send it to the peer through websockets
if (event.candidate) {
await sock.send(JSON.stringify({
type: 'ice-candidate',
candidate: event.candidate
}))
}
});
// Handle connection and disconnection of peer
conn.addEventListener('connectionstatechange', (event) => {
switch (conn.connectionState) {
case 'connected':
// Switch to stream screen
pinValForm.hidden = true;
streamForm.hidden = false;
locPlayback.srcObject = stream;
console.log("Connected!")
break;
case 'disconnected':
console.log("Disconnected.")
// Reload the page to ensure the state is complete reset
location.reload();
break;
default:
break;
}
});
// Start streaming the screen
stream.getTracks().forEach(track => {
conn.addTrack(track, stream);
});
// If the stream ends, reload the page
stream.getVideoTracks()[0].addEventListener('ended', (event) => {
location.reload();
})
// Create and send an offer
const offer = await conn.createOffer();
await conn.setLocalDescription(offer);
await sock.send(JSON.stringify({
type: 'offer',
offer: offer
}))
}
async function connect() {
// Disable the connect button and show status
dispNameTb.disabled = true;
connBtn.disabled = true;
connBtn.textContent = 'Connecting...';
// Send a connect command to the server
await sock.send(JSON.stringify({
type: 'connect',
dispID: dispNameTb.value
}));
}
async function validatePIN() {
// Disable the validate button and show status
dispPINTb.disabled = true;
validateBtn.disabled = true;
validateBtn.textContent = 'Checking the PIN...';
// Send the validate pin command to the server
await sock.send(JSON.stringify({
type: 'validate-pin',
pin: dispPINTb.value
}));
}
async function disconnect() {
// Just reload the page
location.reload();
}
// Connect to the server using WebSockets
console.log('Connecting to websocket...')
sock = new WebSocket(`ws://${location.host}/sig`);
sock.addEventListener('open', async (event) => {
console.log('Connected to websocket')
});
sock.addEventListener('message', (event) => { onMessage(event.data); })

155
www/scripts/screen.js Normal file
View File

@@ -0,0 +1,155 @@
// Get or generate a display ID
let params = new URLSearchParams(document.location.search);
let dispID = params.get("dispID");
if (dispID === null) {
// Generate a random name (TODO)
dispID = self.crypto.randomUUID().substring(0, 6);
}
// App objects
let sock = null;
let conn = null;
// GUI Objects
let idleScreen = document.querySelector('#idleScreen');
let dispIDSpan = document.querySelector('#dispID');
let pinScreen = document.querySelector('#pinScreen');
let pinSpan = document.querySelector('#pin');
let playback = document.querySelector('#playback');
let credits = document.querySelector('#credits');
// Show the display ID
dispIDSpan.textContent = dispID;
async function reset() {
// Completely reset the state
playback.hidden = true;
pinScreen.hidden = true;
idleScreen.hidden = false;
credits.hidden = false;
playback.srcObject = null;
conn = null;
// Initialize WebRTC
await initRTC();
}
// WebSocket message handler
async function onMessage(data) {
// Parse the message
console.log(data)
const msg = JSON.parse(data);
// Process depending on the type
switch (msg.type) {
case 'conn-req':
// Show the pin
pinSpan.textContent = msg.pin;
// Switch to pin mode
idleScreen.hidden = true;
pinScreen.hidden = false;
break;
case 'offer':
console.log('Got offer')
// Pass on the offer to WebRTC
await conn.setRemoteDescription(new RTCSessionDescription(msg.offer));
// Create an answer
answer = await conn.createAnswer();
await conn.setLocalDescription(answer);
// Encode and send the answer
await sock.send(JSON.stringify({
type: 'answer',
answer: answer
}))
break;
case 'ice-candidate':
console.log('Got ice candidate')
// Add the ice candidate to the WebRTC connection
await conn.addIceCandidate(msg.candidate);
break;
case 'auth-ok':
break;
case 'disconnect':
// Reset the display
reset();
break;
default:
break;
}
}
async function initRTC() {
// Create the WebRTC connection
conn = new RTCPeerConnection({'iceServers': [{'urls': 'stun:stun.l.google.com:19302'}]});
// Handle new ice candidates
conn.addEventListener('icecandidate', async (event) => {
// If there is a new candidate, send it to the peer through websockets
if (event.candidate) {
await sock.send(JSON.stringify({
type: 'ice-candidate',
candidate: event.candidate
}))
}
});
// Handle connection and disconnection of peer
conn.addEventListener('connectionstatechange', (event) => {
switch (conn.connectionState) {
case 'connected':
// Switch to playback mode
pinScreen.hidden = true;
credits.hidden = true;
playback.hidden = false;
break;
case 'disconnected':
// Reset the display
reset();
break;
default:
break;
}
});
// Send remote stream to the playback widget
conn.addEventListener('track', (event) => {
const [remoteStream] = event.streams;
playback.srcObject = remoteStream;
});
}
// Main function for the app
async function main() {
// Connect to the server using WebSockets
console.log('Connecting to websocket...')
sock = new WebSocket(`ws://${location.host}/sig`);
// Add handlers for the socket
sock.addEventListener('message', (event) => { onMessage(event.data); })
sock.addEventListener('open', async (event) => {
console.log('Connected to websocket')
// Tell the server that this is a screen
await sock.send(JSON.stringify({
type: 'screen',
name: dispID
}))
// Initialize WebRTC
await initRTC();
});
}
// Run the main function
main();