diff --git a/config.go b/config.go index 65bbd1b..a0a2830 100644 --- a/config.go +++ b/config.go @@ -1,4 +1,5 @@ package main -const CONF_TIMEOUT_MS = 5000 -var CONF_ICE_SERVERS = [1]string{"stun:stun.l.google.com:19302"} \ No newline at end of file +const CONF_OTP_LIFESPAN_MS = 30000; +const CONF_TIMEOUT_MS = 5000; +var CONF_ICE_SERVERS = [1]string{"stun:stun.l.google.com:19302"}; \ No newline at end of file diff --git a/display.go b/display.go index b1dcc22..46912a2 100644 --- a/display.go +++ b/display.go @@ -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(); } \ No newline at end of file diff --git a/main.go b/main.go index 27a411a..181bc57 100644 --- a/main.go +++ b/main.go @@ -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) } } \ No newline at end of file diff --git a/message.go b/message.go index 38d3c33..e290f18 100644 --- a/message.go +++ b/message.go @@ -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 { diff --git a/user.go b/user.go index eab2a59..b22e54d 100644 --- a/user.go +++ b/user.go @@ -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(); } \ No newline at end of file diff --git a/www/css/style.css b/www/css/style.css index 6f8c449..1f40ba1 100644 --- a/www/css/style.css +++ b/www/css/style.css @@ -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; } \ No newline at end of file diff --git a/www/display/index.html b/www/display/index.html index 1876b6d..dd75600 100644 --- a/www/display/index.html +++ b/www/display/index.html @@ -13,10 +13,18 @@
- Display ID:
- - OTP:
- +
+ Display Name
+ +
+
+
+ OTP
+ +
+
+
+
diff --git a/www/index.html b/www/index.html index 3a6f216..6af5e43 100644 --- a/www/index.html +++ b/www/index.html @@ -15,16 +15,16 @@

📺


-

-

- +

+

+
diff --git a/www/scripts/display.js b/www/scripts/display.js index 83b1fbc..43a2953 100644 --- a/www/scripts/display.js +++ b/www/scripts/display.js @@ -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') -}); \ No newline at end of file diff --git a/www/scripts/user.js b/www/scripts/user.js index d0e31f5..0924dea 100644 --- a/www/scripts/user.js +++ b/www/scripts/user.js @@ -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