This commit is contained in:
AlexandreRouma
2025-11-05 02:02:36 -05:00
parent 6605a2d933
commit f352b50f18
10 changed files with 784 additions and 125 deletions

View File

@@ -1,4 +1,5 @@
package main
const CONF_TIMEOUT_MS = 5000
var CONF_ICE_SERVERS = [1]string{"stun:stun.l.google.com:19302"}
const CONF_OTP_LIFESPAN_MS = 30000;
const CONF_TIMEOUT_MS = 5000;
var CONF_ICE_SERVERS = [1]string{"stun:stun.l.google.com:19302"};

View File

@@ -25,32 +25,32 @@ type Display struct {
otp string;
// Channel to pass the answer from the display coroutine to the user couroutine
answerCh chan string;
answerCh chan interface{};
}
// Helper function to flush channels
func chFlush(ch *chan string) {
func chFlush(ch *chan interface{}) {
for {
empty := false
empty := false;
select {
// If data is available, read it and try again
case <-*ch:
continue
case <-(*ch):
continue;
// If no data is available, stop reading
default:
empty = true
empty = true;
}
if empty { break }
if empty { break; }
}
}
// Helper function to read from a channel with a timeout
func chReadTimeout(ch *chan string, timeoutMS int) (string, error) {
func chReadTimeout(ch *chan interface{}, timeoutMS int) (interface{}, error) {
select {
// If data is available, return it with no error
case data := <-*ch:
return data, nil
case data := <-(*ch):
return data, nil;
// If no data has been received and the timeout is reached, return an error
case <-time.After(time.Millisecond * time.Duration(timeoutMS)):
@@ -91,7 +91,7 @@ func (this *Display) stream() {
}
// Send a WebRTC offer to the display and get an answer
func (this *Display) webRTCOffer(offer string, timeoutMS int) (string, error) {
func (this *Display) sendWebRTCOffer(offer interface{}, timeoutMS int) (interface{}, error) {
// Flush the answer channel
chFlush(&this.answerCh)
@@ -116,7 +116,7 @@ func (this *Display) webRTCOffer(offer string, timeoutMS int) (string, error) {
}
// Send an ICE candiate to the display
func (this *Display) iceCandidate(candidate string) {
func (this *Display) sendIceCandidate(candidate interface{}) {
// Acquire the sending mutex
this.sockSendMtx.Lock()
@@ -136,6 +136,7 @@ func (this *Display) iceCandidate(candidate string) {
func displayHandler(sock *websocket.Conn, dispID string, otp string) {
// Create the display object
disp := Display{ sock: sock, otp: otp }
disp.answerCh = make(chan interface{});
// Acquire the sending mutex
disp.sockSendMtx.Lock()
@@ -165,53 +166,57 @@ func displayHandler(sock *websocket.Conn, dispID string, otp string) {
sendMessage(sock, Message{
mtype: "config",
arguments: map[string]interface{}{
"timeout": CONF_TIMEOUT_MS,
"iceServers": CONF_ICE_SERVERS,
"config": map[string]interface{}{
"otpLifespan": CONF_OTP_LIFESPAN_MS,
"timeout": CONF_TIMEOUT_MS,
"iceServers": CONF_ICE_SERVERS,
},
},
})
// Release the sending mutex
disp.sockSendMtx.Unlock()
disp.sockSendMtx.Unlock();
// Log the new display
log.Println("New display: ID='" + dispID + "', OTP='" + otp + "'")
log.Println("New display: ID='" + dispID + "', OTP='" + otp + "'");
// Message loop
for {
// Receive a message
msg, err := recvMessage(sock, 0)
msg, err := recvMessage(sock, 0);
// Give up on the connection if there was an error
if (err != nil) { break }
if (err != nil) { break; }
// Handle the message depending on its type
switch msg.mtype {
case "otp":
// Check that the message contains an OTP
otp, valid := msg.arguments["otp"].(string)
if (!valid) { break }
otp, valid := msg.arguments["otp"].(string);
if (!valid) { break; }
// Acquire the display's OTP
disp.otpMtx.Lock()
disp.otpMtx.Lock();
// Update the OTP
disp.otp = otp
disp.otp = otp;
log.Println("New OTP for ID='" + dispID + "': OTP='" + otp + "'");
// Release the display's OTP
disp.otpMtx.Unlock()
disp.otpMtx.Unlock();
case "answer":
case "webrtc-answer":
// Check that the message contains an answer
answer, valid := msg.arguments["answer"].(string)
if (!valid) { break }
answer := msg.arguments["answer"];
if (answer == nil) { break; }
// Send the answer through the display's answer channel
disp.answerCh <- answer
disp.answerCh <- answer;
case "ice-candidate":
// Check that the message contains an ice candidate
candidate, valid := msg.arguments["candidate"].(string)
if (!valid) { break; }
candidate := msg.arguments["candidate"];
if (candidate == nil) { break; }
// Acquire the user's display pointer
disp.userMtx.Lock();
@@ -238,5 +243,12 @@ func displayHandler(sock *websocket.Conn, dispID string, otp string) {
}
}
// TODO: Gracefull disconnect the connected user if there is one
// Acquire the display list
displaysLck.Lock();
// Remove the display from the list
delete(displays, dispID);
// Release the display list
displaysLck.Unlock();
}

View File

@@ -18,5 +18,6 @@ func main() {
// Run the server
err := http.ListenAndServe(":3000", nil)
// err := http.ListenAndServeTLS(":3443", ".old/fullchain.pem", ".old/privkey.pem", nil);
if (err != nil) { log.Fatal(err) }
}

View File

@@ -1,9 +1,11 @@
package main
// Packages
import "encoding/json"
import "errors"
import "time"
import "github.com/gorilla/websocket"
//import "log"
// Backend message object
type Message struct {

53
user.go
View File

@@ -19,8 +19,20 @@ type User struct {
display *Display;
}
// Send an error to the user
func (this *User) error(err int) {
// Acquire the sending mutex
this.sockSendMtx.Lock()
// Send the error
sendErrorMessage(this.sock, http.StatusNotFound);
// Release the sending mutex
this.sockSendMtx.Unlock()
}
// Send an ICE candiate to the user
func (this *User) iceCandidate(candidate string) {
func (this *User) iceCandidate(candidate interface{}) {
// Acquire the sending mutex
this.sockSendMtx.Lock()
@@ -45,8 +57,10 @@ func userHandler(sock *websocket.Conn) {
sendMessage(sock, Message{
mtype: "config",
arguments: map[string]interface{}{
"timeout": CONF_TIMEOUT_MS,
"iceServers": CONF_ICE_SERVERS,
"config": map[string]interface{}{
"timeout": CONF_TIMEOUT_MS,
"iceServers": CONF_ICE_SERVERS,
},
},
});
@@ -114,13 +128,13 @@ func userHandler(sock *websocket.Conn) {
// TODO: Check for error
// Release the user's display pointer
user.displayMtx.Lock();
user.displayMtx.Unlock();
// Release the display list
displaysLck.Unlock();
// Log the connection
log.Println("User successfully connected to display: ID='", dispID, "'");
log.Println("User successfully connected to display: ID='" + dispID + "'");
// Notify the user of the successful connection
sendMessage(sock, Message{
@@ -129,8 +143,8 @@ func userHandler(sock *websocket.Conn) {
case "webrtc-offer":
// Check that the message contains an offer
offer, valid := msg.arguments["offer"].(string)
if (!valid) { break; }
offer := msg.arguments["offer"];
if (offer == nil) { break; }
// Acquire the user's display pointer
user.displayMtx.Lock();
@@ -146,7 +160,7 @@ func userHandler(sock *websocket.Conn) {
}
// Send the offer to the display and get the response
answer, err := user.display.webRTCOffer(offer, CONF_TIMEOUT_MS);
answer, err := user.display.sendWebRTCOffer(offer, CONF_TIMEOUT_MS);
if (err != nil) {
// Release the user's display pointer
user.displayMtx.Unlock();
@@ -169,8 +183,8 @@ func userHandler(sock *websocket.Conn) {
case "ice-candidate":
// Check that the message contains an ice candidate
candidate, valid := msg.arguments["candidate"].(string)
if (!valid) { break; }
candidate := msg.arguments["candidate"]
if (candidate == nil) { break; }
// Acquire the user's display pointer
user.displayMtx.Lock();
@@ -186,7 +200,7 @@ func userHandler(sock *websocket.Conn) {
}
// Send the ice candidtate to the display
user.display.iceCandidate(candidate);
user.display.sendIceCandidate(candidate);
// Release the user's display pointer
user.displayMtx.Unlock();
@@ -197,5 +211,20 @@ func userHandler(sock *websocket.Conn) {
}
}
// TODO: Gracefull disconnect the connected display if there is one
// Acquire the user's display pointer
user.displayMtx.Lock();
// The user is associated with a display
if (user.display != nil) {
log.Println("User disconnecting from display");
// Disassociate the user from the display
user.display.user = nil;
// Reset the display
user.display.reset();
}
// Release the user's display pointer
user.displayMtx.Unlock();
}

View File

@@ -23,7 +23,7 @@ body {
background: black;
}
#connForm, #pinValForm, #streamForm, #idleScreen, #pinScreen {
#connForm, #pinValForm, #streamForm {
width: 25em;
position: absolute;
left: 50%;
@@ -130,10 +130,37 @@ h1 {
text-align: center;
}
#dispIDLabel, #pinLabel {
#idleScreen {
width: fit-content;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
.otp-container {
width: fit-content;
}
#dispIDLabel, #otpLabel {
font-size: 30pt;
}
#dispID, #pin {
#dispID, #otp {
font-size: 80pt;
}
.lifespan-container {
padding: 0;
background-color: #797979;
width: 100%;
height: 1em;
border-radius: 0.25em;
}
.lifespan {
background-color: #1080FF;
height: 100%;
animation-timing-function: linear;
border-radius: 0.25em;
}

View File

@@ -13,10 +13,18 @@
<body>
<div id="idleScreen">
<span id="dispIDLabel">Display ID:</span><br>
<span id="dispID"></span>
<span id="otpLabel">OTP:</span><br>
<span id="otp"></span>
<div class="disp-id-container">
<span id="dispIDLabel">Display Name</span><br>
<span id="dispID"></span>
</div>
<br>
<div class="otp-container">
<span id="otpLabel">OTP</span><br>
<span id="otp"></span>
<div class="lifespan-container">
<div id="lifespan" class="lifespan"></div>
</div>
</div>
</div>
<!-- Video widget -->

View File

@@ -15,16 +15,16 @@
<!-- Connection form -->
<div id="connForm">
<p class="TV">📺</p><br>
<input type="text" id="dispName" placeholder="Display ID" required autocomplete="off"><br><br>
<input type="text" id="otp" placeholder="OTP" required autocomplete="off"><br><br>
<button id="connect" disabled autocomplete="off">Connect</button>
<input type="text" id="dispIDTb" placeholder="Display Name" required autocomplete="off"><br><br>
<input type="text" id="dispOTPTb" placeholder="OTP" required autocomplete="off" maxlength="6"><br><br>
<button id="connectBtn" 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">Disconnect</button>
<button id="disconnectBtn">Disconnect</button>
</div>
</body>

View File

@@ -1,39 +1,368 @@
// Streaming objects
let sock = null;
let conn = null;
// User API class
class WisCastDisplayAPIClient {
// Socket to the API endpoint
#sock;
// 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');
// Endpoint URL
#endpoint;
// 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')
// Display ID
#dispID;
// // DEBUGGING ONLY
// await sock.send(JSON.stringify({
// type: 'init',
// pin: dispPINTb.value
// }))
// Initial OTP
#initOTP;
// Called when a config message is received
#onconfig = (config) => {};
/**
* Handler called when streaming should be started.
*/
onstream = () => {};
/**
* Handler called when a WebRTC offer is received.
* @param {RTCSessionDescriptionInit} offer The received WebRTC offer.
*/
onwebrtcoffer = (offer) => { return null; };
/**
* Handler called when an ICE candidate is received
* @param {RTCIceCandidateInit} candidate The received ICE candidate.
*/
onicecandidate = (candidate) => {};
/**
* Handler called when the connection to the user should be reset.
*/
onreset = () => {};
/**
* Create a User API client instance.
* @param {String} endpoint URL of the API endpoint.
*/
constructor(endpoint) {
// Save the endpoint
this.#endpoint = endpoint;
}
/**
* Connect to the API server.
* @param displayID ID of the display to give to the server.
* @param initialOTP Initial OTP to give the server.
* @returns {Object} Configuration to use for the session.
*/
async connect(displayID, initialOTP) {
// Save the parameters
this.#dispID = displayID;
this.#initOTP = initialOTP;
// Do the rest asynchronously
return new Promise(async (res) => {
// Register the handler for config messages
this.#onconfig = (config) => { res(config); };
// Connect to the WebSocket endpoint
console.log('Connecting to the API...');
this.#sock = new WebSocket(this.#endpoint);
// Handle connection
this.#sock.addEventListener('open', async (event) => { await this.#connectHandler(event); });
// Handle messages
this.#sock.addEventListener('message', async (event) => { await this.#messageHandler(event); });
// Handle disconnection
this.#sock.addEventListener('close', async (event) => { await this.#disconnectHandler(event); });
});
}
/**
* Set a new OTP.
* @param {String} otp New OTP.
*/
async setOTP(otp) {
// Send the connection command
await this.#sock.send(JSON.stringify({
type: 'otp',
otp: otp
}))
}
/**
* Send a WebRTC answer to the user.
* @param {RTCSessionDescriptionInit} answer ICE candidate to send to the display.
*/
async sendWebRTCAnswer(answer) {
// Send the connection command
await this.#sock.send(JSON.stringify({
type: 'webrtc-answer',
answer: answer
}))
}
/**
* 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
}))
}
// 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: 'display',
dispID: this.#dispID,
otp: this.#initOTP
}))
}
async #messageHandler(event) {
// Parse the message
const msg = JSON.parse(event.data);
// Handle the message depending on its type
switch (msg.type) {
case 'config':
console.log(msg)
// Call the config handler
this.#onconfig(msg.config);
break;
case 'stream':
this.onstream();
break;
case 'webrtc-offer':
// Call the offer handler to get the answer
answer = await this.onwebrtcoffer(msg.offer);
// Send the answer back to the server
await this.#sock.send(JSON.stringify({
type: 'webrtc-answer',
answer: answer
}))
break;
case 'ice-candidate':
// Call the answer handler
this.onicecandidate(msg.candidate);
break;
case 'reset':
// Call the reset handler
this.onreset();
break;
}
}
async #disconnectHandler(event) {
console.log('Disconnected :/');
}
}
async function initWebRTC(client, config) {
// Create the WebRTC connection
let conn = new RTCPeerConnection({'iceServers': [{'urls': config.iceServers[0]}]});
// 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
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;
});
}
function genOTP() {
let otp = '';
for (let i = 0; i < 6; i++) {
otp += Math.floor(Math.random() * 10);
}
return otp;
}
async function main() {
// 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, 8).toUpperCase();
}
// Generate the initial OTP
let initOTP = genOTP();
// GUI Objects
const idleScreen = document.getElementById('idleScreen');
const dispIDSpan = document.getElementById('dispID');
const otpSpan = document.getElementById('otp');
const playback = document.getElementById('playback');
const credits = document.getElementById('credits');
const lifespan = document.getElementById('lifespan');
// Set the ID and OTP spans
dispIDSpan.textContent = dispID;
otpSpan.textContent = initOTP;
// Global state
let conn = null;
// Create the API client
const client = new WisCastDisplayAPIClient(`wss://${location.host}/sig`);
// Connect to the server
const config = await client.connect(dispID, initOTP);
// Define the progress bar animation
const animKeyframes = [
{ width: '100%' },
{ width: '0%' },
];
const animTiming = {
duration: config.otpLifespan,
iterations: 1
};
// Start the animation
lifespan.animate(animKeyframes, animTiming);
// Generate a new OTP every given interval
console.log(lifespan)
setInterval(() => {
// Generate a new OTP
const otp = genOTP();
// Send it to the server
client.setOTP(otp);
// Update it in the GUI
otpSpan.textContent = otp;
// Restart the animation
lifespan.animate(animKeyframes, animTiming);
}, config.otpLifespan);
// Define the WebRTC initialization function
const initWebRTC = () => {
// Create the WebRTC connection
conn = new RTCPeerConnection({'iceServers': [{'urls': config.iceServers[0]}]});
// Handle offers
client.onwebrtcoffer = async (offer) => {
// Pass on the offer to WebRTC
await conn.setRemoteDescription(new RTCSessionDescription(offer));
// Create an answer
answer = await conn.createAnswer();
await conn.setLocalDescription(answer);
// Return the answer to the server
return answer;
};
// Handle ice candidate from user to display
client.onicecandidate = async (candidate) => {
// Add the ice candidate to the WebRTC connection
await conn.addIceCandidate(candidate);
};
// Handle ice candidate from display to user
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 connection and disconnection of peer
conn.onconnectionstatechange = async (event) => {
switch (conn.connectionState) {
case 'connected':
// Switch to playback mode
credits.hidden = true;
playback.hidden = false;
break;
case 'disconnected':
// Completely reset the state
playback.hidden = true;
credits.hidden = false;
playback.srcObject = null;
conn = null;
// Initialize WebRTC
await initWebRTC();
break;
}
};
// Send remote stream to the playback widget
conn.ontrack = (event) => {
const [remoteStream] = event.streams;
playback.srcObject = remoteStream;
};
}
// Init WebRTC
initWebRTC();
// Register the reset handler
client.onreset = async () => {
// Completely reset the state
playback.hidden = true;
credits.hidden = false;
playback.srcObject = null;
conn = null;
// Initialize WebRTC
await initWebRTC();
}
}
// Run the main function
main();
await sock.send(JSON.stringify({
type: 'init',
clientType: 'display',
dispID: 'TEST',
otp: '123456'
}));
});
sock.addEventListener('message', (event) => {
console.log(event.data)
});
sock.addEventListener('close', (event) => {
console.log('Disconnected from websocket')
});

View File

@@ -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