mirror of
https://github.com/AlexandreRouma/wiscast.git
synced 2026-04-18 07:42:44 +00:00
progress
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
const CONF_TIMEOUT_MS = 5000
|
const CONF_OTP_LIFESPAN_MS = 30000;
|
||||||
var CONF_ICE_SERVERS = [1]string{"stun:stun.l.google.com:19302"}
|
const CONF_TIMEOUT_MS = 5000;
|
||||||
|
var CONF_ICE_SERVERS = [1]string{"stun:stun.l.google.com:19302"};
|
||||||
72
display.go
72
display.go
@@ -25,32 +25,32 @@ type Display struct {
|
|||||||
otp string;
|
otp string;
|
||||||
|
|
||||||
// Channel to pass the answer from the display coroutine to the user couroutine
|
// Channel to pass the answer from the display coroutine to the user couroutine
|
||||||
answerCh chan string;
|
answerCh chan interface{};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to flush channels
|
// Helper function to flush channels
|
||||||
func chFlush(ch *chan string) {
|
func chFlush(ch *chan interface{}) {
|
||||||
for {
|
for {
|
||||||
empty := false
|
empty := false;
|
||||||
select {
|
select {
|
||||||
// If data is available, read it and try again
|
// If data is available, read it and try again
|
||||||
case <-*ch:
|
case <-(*ch):
|
||||||
continue
|
continue;
|
||||||
|
|
||||||
// If no data is available, stop reading
|
// If no data is available, stop reading
|
||||||
default:
|
default:
|
||||||
empty = true
|
empty = true;
|
||||||
}
|
}
|
||||||
if empty { break }
|
if empty { break; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to read from a channel with a timeout
|
// 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 {
|
select {
|
||||||
// If data is available, return it with no error
|
// If data is available, return it with no error
|
||||||
case data := <-*ch:
|
case data := <-(*ch):
|
||||||
return data, nil
|
return data, nil;
|
||||||
|
|
||||||
// If no data has been received and the timeout is reached, return an error
|
// If no data has been received and the timeout is reached, return an error
|
||||||
case <-time.After(time.Millisecond * time.Duration(timeoutMS)):
|
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
|
// 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
|
// Flush the answer channel
|
||||||
chFlush(&this.answerCh)
|
chFlush(&this.answerCh)
|
||||||
|
|
||||||
@@ -116,7 +116,7 @@ func (this *Display) webRTCOffer(offer string, timeoutMS int) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Send an ICE candiate to the display
|
// Send an ICE candiate to the display
|
||||||
func (this *Display) iceCandidate(candidate string) {
|
func (this *Display) sendIceCandidate(candidate interface{}) {
|
||||||
// Acquire the sending mutex
|
// Acquire the sending mutex
|
||||||
this.sockSendMtx.Lock()
|
this.sockSendMtx.Lock()
|
||||||
|
|
||||||
@@ -136,6 +136,7 @@ func (this *Display) iceCandidate(candidate string) {
|
|||||||
func displayHandler(sock *websocket.Conn, dispID string, otp string) {
|
func displayHandler(sock *websocket.Conn, dispID string, otp string) {
|
||||||
// Create the display object
|
// Create the display object
|
||||||
disp := Display{ sock: sock, otp: otp }
|
disp := Display{ sock: sock, otp: otp }
|
||||||
|
disp.answerCh = make(chan interface{});
|
||||||
|
|
||||||
// Acquire the sending mutex
|
// Acquire the sending mutex
|
||||||
disp.sockSendMtx.Lock()
|
disp.sockSendMtx.Lock()
|
||||||
@@ -165,53 +166,57 @@ func displayHandler(sock *websocket.Conn, dispID string, otp string) {
|
|||||||
sendMessage(sock, Message{
|
sendMessage(sock, Message{
|
||||||
mtype: "config",
|
mtype: "config",
|
||||||
arguments: map[string]interface{}{
|
arguments: map[string]interface{}{
|
||||||
"timeout": CONF_TIMEOUT_MS,
|
"config": map[string]interface{}{
|
||||||
"iceServers": CONF_ICE_SERVERS,
|
"otpLifespan": CONF_OTP_LIFESPAN_MS,
|
||||||
|
"timeout": CONF_TIMEOUT_MS,
|
||||||
|
"iceServers": CONF_ICE_SERVERS,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// Release the sending mutex
|
// Release the sending mutex
|
||||||
disp.sockSendMtx.Unlock()
|
disp.sockSendMtx.Unlock();
|
||||||
|
|
||||||
// Log the new display
|
// Log the new display
|
||||||
log.Println("New display: ID='" + dispID + "', OTP='" + otp + "'")
|
log.Println("New display: ID='" + dispID + "', OTP='" + otp + "'");
|
||||||
|
|
||||||
// Message loop
|
// Message loop
|
||||||
for {
|
for {
|
||||||
// Receive a message
|
// Receive a message
|
||||||
msg, err := recvMessage(sock, 0)
|
msg, err := recvMessage(sock, 0);
|
||||||
|
|
||||||
// Give up on the connection if there was an error
|
// 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
|
// Handle the message depending on its type
|
||||||
switch msg.mtype {
|
switch msg.mtype {
|
||||||
case "otp":
|
case "otp":
|
||||||
// Check that the message contains an OTP
|
// Check that the message contains an OTP
|
||||||
otp, valid := msg.arguments["otp"].(string)
|
otp, valid := msg.arguments["otp"].(string);
|
||||||
if (!valid) { break }
|
if (!valid) { break; }
|
||||||
|
|
||||||
// Acquire the display's OTP
|
// Acquire the display's OTP
|
||||||
disp.otpMtx.Lock()
|
disp.otpMtx.Lock();
|
||||||
|
|
||||||
// Update the OTP
|
// Update the OTP
|
||||||
disp.otp = otp
|
disp.otp = otp;
|
||||||
|
log.Println("New OTP for ID='" + dispID + "': OTP='" + otp + "'");
|
||||||
|
|
||||||
// Release the display's OTP
|
// Release the display's OTP
|
||||||
disp.otpMtx.Unlock()
|
disp.otpMtx.Unlock();
|
||||||
|
|
||||||
case "answer":
|
case "webrtc-answer":
|
||||||
// Check that the message contains an answer
|
// Check that the message contains an answer
|
||||||
answer, valid := msg.arguments["answer"].(string)
|
answer := msg.arguments["answer"];
|
||||||
if (!valid) { break }
|
if (answer == nil) { break; }
|
||||||
|
|
||||||
// Send the answer through the display's answer channel
|
// Send the answer through the display's answer channel
|
||||||
disp.answerCh <- answer
|
disp.answerCh <- answer;
|
||||||
|
|
||||||
case "ice-candidate":
|
case "ice-candidate":
|
||||||
// Check that the message contains an ice candidate
|
// Check that the message contains an ice candidate
|
||||||
candidate, valid := msg.arguments["candidate"].(string)
|
candidate := msg.arguments["candidate"];
|
||||||
if (!valid) { break; }
|
if (candidate == nil) { break; }
|
||||||
|
|
||||||
// Acquire the user's display pointer
|
// Acquire the user's display pointer
|
||||||
disp.userMtx.Lock();
|
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();
|
||||||
}
|
}
|
||||||
1
main.go
1
main.go
@@ -18,5 +18,6 @@ func main() {
|
|||||||
|
|
||||||
// Run the server
|
// Run the server
|
||||||
err := http.ListenAndServe(":3000", nil)
|
err := http.ListenAndServe(":3000", nil)
|
||||||
|
// err := http.ListenAndServeTLS(":3443", ".old/fullchain.pem", ".old/privkey.pem", nil);
|
||||||
if (err != nil) { log.Fatal(err) }
|
if (err != nil) { log.Fatal(err) }
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
|
// Packages
|
||||||
import "encoding/json"
|
import "encoding/json"
|
||||||
import "errors"
|
import "errors"
|
||||||
import "time"
|
import "time"
|
||||||
import "github.com/gorilla/websocket"
|
import "github.com/gorilla/websocket"
|
||||||
|
//import "log"
|
||||||
|
|
||||||
// Backend message object
|
// Backend message object
|
||||||
type Message struct {
|
type Message struct {
|
||||||
|
|||||||
53
user.go
53
user.go
@@ -19,8 +19,20 @@ type User struct {
|
|||||||
display *Display;
|
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
|
// Send an ICE candiate to the user
|
||||||
func (this *User) iceCandidate(candidate string) {
|
func (this *User) iceCandidate(candidate interface{}) {
|
||||||
// Acquire the sending mutex
|
// Acquire the sending mutex
|
||||||
this.sockSendMtx.Lock()
|
this.sockSendMtx.Lock()
|
||||||
|
|
||||||
@@ -45,8 +57,10 @@ func userHandler(sock *websocket.Conn) {
|
|||||||
sendMessage(sock, Message{
|
sendMessage(sock, Message{
|
||||||
mtype: "config",
|
mtype: "config",
|
||||||
arguments: map[string]interface{}{
|
arguments: map[string]interface{}{
|
||||||
"timeout": CONF_TIMEOUT_MS,
|
"config": map[string]interface{}{
|
||||||
"iceServers": CONF_ICE_SERVERS,
|
"timeout": CONF_TIMEOUT_MS,
|
||||||
|
"iceServers": CONF_ICE_SERVERS,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -114,13 +128,13 @@ func userHandler(sock *websocket.Conn) {
|
|||||||
// TODO: Check for error
|
// TODO: Check for error
|
||||||
|
|
||||||
// Release the user's display pointer
|
// Release the user's display pointer
|
||||||
user.displayMtx.Lock();
|
user.displayMtx.Unlock();
|
||||||
|
|
||||||
// Release the display list
|
// Release the display list
|
||||||
displaysLck.Unlock();
|
displaysLck.Unlock();
|
||||||
|
|
||||||
// Log the connection
|
// 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
|
// Notify the user of the successful connection
|
||||||
sendMessage(sock, Message{
|
sendMessage(sock, Message{
|
||||||
@@ -129,8 +143,8 @@ func userHandler(sock *websocket.Conn) {
|
|||||||
|
|
||||||
case "webrtc-offer":
|
case "webrtc-offer":
|
||||||
// Check that the message contains an offer
|
// Check that the message contains an offer
|
||||||
offer, valid := msg.arguments["offer"].(string)
|
offer := msg.arguments["offer"];
|
||||||
if (!valid) { break; }
|
if (offer == nil) { break; }
|
||||||
|
|
||||||
// Acquire the user's display pointer
|
// Acquire the user's display pointer
|
||||||
user.displayMtx.Lock();
|
user.displayMtx.Lock();
|
||||||
@@ -146,7 +160,7 @@ func userHandler(sock *websocket.Conn) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Send the offer to the display and get the response
|
// 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) {
|
if (err != nil) {
|
||||||
// Release the user's display pointer
|
// Release the user's display pointer
|
||||||
user.displayMtx.Unlock();
|
user.displayMtx.Unlock();
|
||||||
@@ -169,8 +183,8 @@ func userHandler(sock *websocket.Conn) {
|
|||||||
|
|
||||||
case "ice-candidate":
|
case "ice-candidate":
|
||||||
// Check that the message contains an ice candidate
|
// Check that the message contains an ice candidate
|
||||||
candidate, valid := msg.arguments["candidate"].(string)
|
candidate := msg.arguments["candidate"]
|
||||||
if (!valid) { break; }
|
if (candidate == nil) { break; }
|
||||||
|
|
||||||
// Acquire the user's display pointer
|
// Acquire the user's display pointer
|
||||||
user.displayMtx.Lock();
|
user.displayMtx.Lock();
|
||||||
@@ -186,7 +200,7 @@ func userHandler(sock *websocket.Conn) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Send the ice candidtate to the display
|
// Send the ice candidtate to the display
|
||||||
user.display.iceCandidate(candidate);
|
user.display.sendIceCandidate(candidate);
|
||||||
|
|
||||||
// Release the user's display pointer
|
// Release the user's display pointer
|
||||||
user.displayMtx.Unlock();
|
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();
|
||||||
}
|
}
|
||||||
@@ -23,7 +23,7 @@ body {
|
|||||||
background: black;
|
background: black;
|
||||||
}
|
}
|
||||||
|
|
||||||
#connForm, #pinValForm, #streamForm, #idleScreen, #pinScreen {
|
#connForm, #pinValForm, #streamForm {
|
||||||
width: 25em;
|
width: 25em;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
@@ -130,10 +130,37 @@ h1 {
|
|||||||
text-align: center;
|
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;
|
font-size: 30pt;
|
||||||
}
|
}
|
||||||
|
|
||||||
#dispID, #pin {
|
#dispID, #otp {
|
||||||
font-size: 80pt;
|
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;
|
||||||
}
|
}
|
||||||
@@ -13,10 +13,18 @@
|
|||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div id="idleScreen">
|
<div id="idleScreen">
|
||||||
<span id="dispIDLabel">Display ID:</span><br>
|
<div class="disp-id-container">
|
||||||
<span id="dispID"></span>
|
<span id="dispIDLabel">Display Name</span><br>
|
||||||
<span id="otpLabel">OTP:</span><br>
|
<span id="dispID"></span>
|
||||||
<span id="otp"></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>
|
</div>
|
||||||
|
|
||||||
<!-- Video widget -->
|
<!-- Video widget -->
|
||||||
|
|||||||
@@ -15,16 +15,16 @@
|
|||||||
<!-- Connection form -->
|
<!-- Connection form -->
|
||||||
<div id="connForm">
|
<div id="connForm">
|
||||||
<p class="TV">📺</p><br>
|
<p class="TV">📺</p><br>
|
||||||
<input type="text" id="dispName" placeholder="Display ID" required autocomplete="off"><br><br>
|
<input type="text" id="dispIDTb" placeholder="Display Name" required autocomplete="off"><br><br>
|
||||||
<input type="text" id="otp" placeholder="OTP" required autocomplete="off"><br><br>
|
<input type="text" id="dispOTPTb" placeholder="OTP" required autocomplete="off" maxlength="6"><br><br>
|
||||||
<button id="connect" disabled autocomplete="off">Connect</button>
|
<button id="connectBtn" disabled autocomplete="off">Connect</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Streaming form -->
|
<!-- Streaming form -->
|
||||||
<div id="streamForm" hidden>
|
<div id="streamForm" hidden>
|
||||||
<h1>You are live!</h1>
|
<h1>You are live!</h1>
|
||||||
<video id="localPlayback" autoplay playsinline></video><br><br>
|
<video id="localPlayback" autoplay playsinline></video><br><br>
|
||||||
<button id="disconnect">Disconnect</button>
|
<button id="disconnectBtn">Disconnect</button>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|||||||
@@ -1,39 +1,368 @@
|
|||||||
// Streaming objects
|
// User API class
|
||||||
let sock = null;
|
class WisCastDisplayAPIClient {
|
||||||
let conn = null;
|
// Socket to the API endpoint
|
||||||
|
#sock;
|
||||||
|
|
||||||
// GUI Objects
|
// Endpoint URL
|
||||||
let idleScreen = document.querySelector('#idleScreen');
|
#endpoint;
|
||||||
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');
|
|
||||||
|
|
||||||
// Connect to the server using WebSockets
|
// Display ID
|
||||||
console.log('Connecting to websocket...')
|
#dispID;
|
||||||
sock = new WebSocket(`ws://${location.host}/sig`);
|
|
||||||
sock.addEventListener('open', async (event) => {
|
|
||||||
console.log('Connected to websocket')
|
|
||||||
|
|
||||||
// // DEBUGGING ONLY
|
// Initial OTP
|
||||||
// await sock.send(JSON.stringify({
|
#initOTP;
|
||||||
// type: 'init',
|
|
||||||
// pin: dispPINTb.value
|
// 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')
|
|
||||||
});
|
|
||||||
@@ -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
|
// User API class
|
||||||
class WisCastUserAPI {
|
class WisCastUserAPIClient {
|
||||||
// Socket to the API endpoint
|
// Socket to the API endpoint
|
||||||
#sock;
|
#sock;
|
||||||
|
|
||||||
// Endpoint URL
|
// Endpoint URL
|
||||||
#endpoint;
|
#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) {
|
constructor(endpoint) {
|
||||||
// Save the 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() {
|
async connect() {
|
||||||
// Connect to the WebSocket endpoint
|
return new Promise(async (res) => {
|
||||||
console.log('Connecting to the API...')
|
// Register the handler for config messages
|
||||||
this.#sock = new WebSocket(endpoint);
|
this.#onconfig = (config) => { res(config); };
|
||||||
|
|
||||||
// Handle connection
|
// Connect to the WebSocket endpoint
|
||||||
sock.addEventListener('open', this.#connectHandler);
|
console.log('Connecting to the API...');
|
||||||
|
this.#sock = new WebSocket(this.#endpoint);
|
||||||
|
|
||||||
// Handle messages
|
// Handle connection
|
||||||
sock.addEventListener('message', this.#messageHandler);
|
this.#sock.addEventListener('open', async (event) => { await this.#connectHandler(event); });
|
||||||
|
|
||||||
// Handle disconnection
|
// Handle messages
|
||||||
sock.addEventListener('close', this.#disconnectHandler);
|
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) {
|
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) {
|
// Disconnect from the display
|
||||||
console.log('Disconnected :/')
|
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() {
|
async function main() {
|
||||||
// Create the API connection
|
// Get GUI objects from their IDs
|
||||||
const api = new WisCastUserAPI(`ws://${location.host}/sig`);
|
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
|
// 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
|
// Run the main function
|
||||||
|
|||||||
Reference in New Issue
Block a user