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
+
+
+
+
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 @@
You are live!
-
+
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