mirror of
https://github.com/AlexandreRouma/wiscast.git
synced 2026-04-20 08:22:42 +00:00
initial commit
This commit is contained in:
139
www/css/style.css
Normal file
139
www/css/style.css
Normal 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
36
www/display/index.html
Normal 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
38
www/index.html
Normal 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
185
www/scripts/client.js
Normal 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
155
www/scripts/screen.js
Normal 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();
|
||||
Reference in New Issue
Block a user