mirror of
https://github.com/AlexandreRouma/wiscast.git
synced 2026-04-18 15:44:24 +00:00
progress
This commit is contained in:
@@ -1,71 +1,321 @@
|
||||
// Streaming objects
|
||||
let sock = null;
|
||||
let stream = null;
|
||||
let conn = null;
|
||||
|
||||
// GUI Objects
|
||||
let connForm = document.getElementById('connForm');
|
||||
let dispNameTb = document.getElementById('dispName');
|
||||
let connBtn = document.getElementById('connect');
|
||||
let pinValForm = document.getElementById('pinValForm');
|
||||
let dispPINTb = document.getElementById('dispPIN');
|
||||
let validateBtn = document.getElementById('validate');
|
||||
let streamForm = document.getElementById('streamForm');
|
||||
let locPlayback = document.getElementById('localPlayback');
|
||||
|
||||
// User API class
|
||||
class WisCastUserAPI {
|
||||
class WisCastUserAPIClient {
|
||||
// Socket to the API endpoint
|
||||
#sock;
|
||||
|
||||
// Endpoint URL
|
||||
#endpoint;
|
||||
|
||||
// Called when a config message is received
|
||||
#onconfig = (config) => {};
|
||||
|
||||
// Called when a success message is received
|
||||
#onsuccess = () => {};
|
||||
|
||||
// Called when an error message is received
|
||||
#onerror = (err) => {}
|
||||
|
||||
// Called when a WebRTC answer message is received
|
||||
#onwebrtcanswer = (answer) => {}
|
||||
|
||||
/**
|
||||
* Handler called when an ICE candidate is received
|
||||
* @param {RTCIceCandidateInit} candidate The received ICE candidate.
|
||||
*/
|
||||
onicecandidate = (candidate) => {}
|
||||
|
||||
/**
|
||||
* Create a User API client instance.
|
||||
* @param {String} endpoint URL of the API endpoint.
|
||||
*/
|
||||
constructor(endpoint) {
|
||||
// Save the endpoint
|
||||
this.endpoint = endpoint;
|
||||
this.#endpoint = endpoint;
|
||||
}
|
||||
|
||||
// Connect to the API
|
||||
/**
|
||||
* Connect to the API server.
|
||||
* @returns {Object} Configuration to use for the session.
|
||||
*/
|
||||
async connect() {
|
||||
// Connect to the WebSocket endpoint
|
||||
console.log('Connecting to the API...')
|
||||
this.#sock = new WebSocket(endpoint);
|
||||
return new Promise(async (res) => {
|
||||
// Register the handler for config messages
|
||||
this.#onconfig = (config) => { res(config); };
|
||||
|
||||
// Handle connection
|
||||
sock.addEventListener('open', this.#connectHandler);
|
||||
// Connect to the WebSocket endpoint
|
||||
console.log('Connecting to the API...');
|
||||
this.#sock = new WebSocket(this.#endpoint);
|
||||
|
||||
// Handle messages
|
||||
sock.addEventListener('message', this.#messageHandler);
|
||||
// Handle connection
|
||||
this.#sock.addEventListener('open', async (event) => { await this.#connectHandler(event); });
|
||||
|
||||
// Handle disconnection
|
||||
sock.addEventListener('close', this.#disconnectHandler);
|
||||
// Handle messages
|
||||
this.#sock.addEventListener('message', async (event) => { await this.#messageHandler(event); });
|
||||
|
||||
// Handle disconnection
|
||||
this.#sock.addEventListener('close', async (event) => { await this.#disconnectHandler(event); });
|
||||
});
|
||||
}
|
||||
|
||||
// Connect to a display using its ID and OTP
|
||||
/**
|
||||
* Connect to a display.
|
||||
* @param {String} dispID ID of the display.
|
||||
* @param {String} OTP One-Time-Pass currently shown on the display.
|
||||
*/
|
||||
async connectDisplay(dispID, OTP) {
|
||||
return new Promise(async (res) => {
|
||||
// Register the success and error handlers
|
||||
this.#onsuccess = () => { res(null); }
|
||||
this.#onerror = (err) => { res(err); }
|
||||
|
||||
// Send the connection command
|
||||
await this.#sock.send(JSON.stringify({
|
||||
type: 'connect',
|
||||
dispID: dispID,
|
||||
otp: OTP
|
||||
}))
|
||||
});
|
||||
}
|
||||
|
||||
#connectHandler(event) {
|
||||
console.log('Connected!')
|
||||
/**
|
||||
* Send a WebRTC offer to the display. Must already be connected.
|
||||
* @param {RTCSessionDescriptionInit} offer Offer to send to the display.
|
||||
* @returns {RTCSessionDescriptionInit} The answer from the display or null on error.
|
||||
*/
|
||||
async sendWebRTCOffer(offer) {
|
||||
return new Promise(async (res) => {
|
||||
// Register the answer and error handlers
|
||||
this.#onwebrtcanswer = (answer) => { res(answer); }
|
||||
this.#onerror = (err) => { res(err); }
|
||||
|
||||
// Send the connection command
|
||||
await this.#sock.send(JSON.stringify({
|
||||
type: 'webrtc-offer',
|
||||
offer: offer
|
||||
}))
|
||||
});
|
||||
}
|
||||
|
||||
#messageHandler(event) {
|
||||
console.log(event.data)
|
||||
/**
|
||||
* Send an ICE candidate to the display. Must already be connected.
|
||||
* @param {RTCIceCandidateInit} candidate ICE candidate to send to the display.
|
||||
*/
|
||||
async sendICECandidate(candidate) {
|
||||
// Send the connection command
|
||||
await this.#sock.send(JSON.stringify({
|
||||
type: 'ice-candidate',
|
||||
candidate: candidate
|
||||
}))
|
||||
}
|
||||
|
||||
#disconnectHandler(event) {
|
||||
console.log('Disconnected :/')
|
||||
// Disconnect from the display
|
||||
async disconnectDisplay() {
|
||||
// TODO
|
||||
}
|
||||
|
||||
async #connectHandler(event) {
|
||||
console.log('Connected!');
|
||||
|
||||
// Send initialization message
|
||||
await this.#sock.send(JSON.stringify({
|
||||
type: 'init',
|
||||
clientType: 'user'
|
||||
}))
|
||||
}
|
||||
|
||||
async #messageHandler(event) {
|
||||
// Parse the message
|
||||
const msg = JSON.parse(event.data);
|
||||
|
||||
// Handle the message depending on its type
|
||||
switch (msg.type) {
|
||||
case 'success':
|
||||
// Call the success handler
|
||||
this.#onsuccess();
|
||||
break;
|
||||
|
||||
case 'error':
|
||||
// Call the success handler
|
||||
console.log('Error:', msg.code)
|
||||
this.#onerror(msg.code);
|
||||
break;
|
||||
|
||||
case 'config':
|
||||
// Call the config handler
|
||||
this.#onconfig(msg.config);
|
||||
break;
|
||||
|
||||
case 'webrtc-answer':
|
||||
// Call the answer handler
|
||||
this.#onwebrtcanswer(msg.answer);
|
||||
break;
|
||||
|
||||
case 'ice-candidate':
|
||||
// Call the answer handler
|
||||
this.onicecandidate(msg.candidate);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
async #disconnectHandler(event) {
|
||||
console.log('Disconnected :/');
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
// Create the API connection
|
||||
const api = new WisCastUserAPI(`ws://${location.host}/sig`);
|
||||
// Get GUI objects from their IDs
|
||||
const connForm = document.getElementById('connForm');
|
||||
const dispIDTb = document.getElementById('dispIDTb');
|
||||
const dispOTPTb = document.getElementById('dispOTPTb');
|
||||
const connBtn = document.getElementById('connectBtn');
|
||||
const streamForm = document.getElementById('streamForm');
|
||||
const locPlayback = document.getElementById('localPlayback');
|
||||
const disconnectBtn = document.getElementById('disconnectBtn');
|
||||
|
||||
// Create the API client
|
||||
const client = new WisCastUserAPIClient(`wss://${location.host}/sig`);
|
||||
|
||||
// Global state
|
||||
let config = null;
|
||||
let conn = null;
|
||||
let stream = null;
|
||||
|
||||
// Register a checking function for the contents of the display ID and OTP
|
||||
check = (event) => {
|
||||
// Only enable the connect button if the content of both is valid
|
||||
console.log('change')
|
||||
connBtn.disabled = (dispIDTb.value === '' || dispOTPTb.value.length !== 6 || !config);
|
||||
}
|
||||
dispIDTb.oninput = check;
|
||||
dispOTPTb.oninput = check;
|
||||
|
||||
// Register a handler for when enter is pressed in the display name textbox
|
||||
dispIDTb.onkeyup = (event) => {
|
||||
// Check that the key was enter
|
||||
if (event.key != 'Enter') { return; }
|
||||
|
||||
// Check that the name textbox is not empty
|
||||
if (dispIDTb.value === '') { return; }
|
||||
|
||||
// Select the OTP textbox
|
||||
dispOTPTb.focus();
|
||||
dispOTPTb.select();
|
||||
};
|
||||
|
||||
// Register a handler for when enter is pressed in the display OTP textbox
|
||||
dispOTPTb.onkeyup = (event) => {
|
||||
// Check that the key was enter
|
||||
if (event.key != 'Enter') { return; }
|
||||
|
||||
// Check that the connect button is enabled
|
||||
if (connBtn.disabled) { return; }
|
||||
|
||||
// Press the connect button
|
||||
connBtn.click();
|
||||
};
|
||||
|
||||
// Connect to the server
|
||||
await api.connect();
|
||||
config = await client.connect();
|
||||
|
||||
// Register a handler for clicking the connection button
|
||||
connBtn.onclick = async (event) => {
|
||||
// Disable the text boxes and the button
|
||||
dispIDTb.disabled = true;
|
||||
dispOTPTb.disabled = true;
|
||||
connBtn.disabled = true;
|
||||
|
||||
// Change the status
|
||||
connBtn.textContent = 'Getting permissions...';
|
||||
|
||||
// Get the stream for the screen
|
||||
stream = await navigator.mediaDevices.getDisplayMedia({ video: { cursor: 'always' } });
|
||||
|
||||
// Disable the text boxes and the button
|
||||
dispIDTb.disabled = true;
|
||||
dispOTPTb.disabled = true;
|
||||
connBtn.disabled = true;
|
||||
|
||||
// Change the status
|
||||
connBtn.textContent = 'Authenticating...';
|
||||
|
||||
// Attempt to connect to the display
|
||||
const err = await client.connectDisplay(dispIDTb.value, dispOTPTb.value);
|
||||
if (err) {
|
||||
// TODO: Show the error
|
||||
console.log(err)
|
||||
|
||||
// Reset the GUI
|
||||
dispIDTb.value = '';
|
||||
dispOTPTb.value = '';
|
||||
connBtn.textContent = 'Connect';
|
||||
dispIDTb.disabled = false;
|
||||
dispOTPTb.disabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Change the status
|
||||
connBtn.textContent = 'Connecting...';
|
||||
|
||||
// Create the connection
|
||||
conn = new RTCPeerConnection({'iceServers': [{'urls': config.iceServers[0]}]});
|
||||
|
||||
// Handle ice candidates from user to display
|
||||
conn.onicecandidate = async (event) => {
|
||||
// If there is a new candidate, send it to the peer through websockets
|
||||
if (event.candidate) { await client.sendICECandidate(event.candidate); }
|
||||
};
|
||||
|
||||
// Handle ice candidates from display to user
|
||||
client.onicecandidate = (candidate) => {
|
||||
conn.addIceCandidate(candidate);
|
||||
}
|
||||
|
||||
// Handle connection and disconnection of peer
|
||||
conn.onconnectionstatechange = (event) => {
|
||||
switch (conn.connectionState) {
|
||||
case 'connected':
|
||||
// Switch to stream screen
|
||||
connForm.hidden = true;
|
||||
streamForm.hidden = false;
|
||||
locPlayback.srcObject = stream;
|
||||
console.log("Streaming!")
|
||||
break;
|
||||
|
||||
case 'disconnected':
|
||||
console.log("Stream ended.")
|
||||
// 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].onended = (event) => {
|
||||
location.reload();
|
||||
};
|
||||
|
||||
// Create and send an offer
|
||||
const offer = await conn.createOffer();
|
||||
await conn.setLocalDescription(offer);
|
||||
await conn.setRemoteDescription(await client.sendWebRTCOffer(offer));
|
||||
};
|
||||
|
||||
// Register the disconnect button click event
|
||||
disconnectBtn.onclick = (event) => {
|
||||
// Just reload the page
|
||||
location.reload();
|
||||
};
|
||||
|
||||
// Do a check to potentially enable the connection button
|
||||
check(null);
|
||||
}
|
||||
|
||||
// Run the main function
|
||||
|
||||
Reference in New Issue
Block a user