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
|
||||
|
||||
const CONF_TIMEOUT_MS = 5000
|
||||
var CONF_ICE_SERVERS = [1]string{"stun:stun.l.google.com:19302"}
|
||||
const CONF_OTP_LIFESPAN_MS = 30000;
|
||||
const CONF_TIMEOUT_MS = 5000;
|
||||
var CONF_ICE_SERVERS = [1]string{"stun:stun.l.google.com:19302"};
|
||||
72
display.go
72
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();
|
||||
}
|
||||
1
main.go
1
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) }
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
package main
|
||||
|
||||
// Packages
|
||||
import "encoding/json"
|
||||
import "errors"
|
||||
import "time"
|
||||
import "github.com/gorilla/websocket"
|
||||
//import "log"
|
||||
|
||||
// Backend message object
|
||||
type Message struct {
|
||||
|
||||
53
user.go
53
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();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -13,10 +13,18 @@
|
||||
|
||||
<body>
|
||||
<div id="idleScreen">
|
||||
<span id="dispIDLabel">Display ID:</span><br>
|
||||
<span id="dispID"></span>
|
||||
<span id="otpLabel">OTP:</span><br>
|
||||
<span id="otp"></span>
|
||||
<div class="disp-id-container">
|
||||
<span id="dispIDLabel">Display Name</span><br>
|
||||
<span id="dispID"></span>
|
||||
</div>
|
||||
<br>
|
||||
<div class="otp-container">
|
||||
<span id="otpLabel">OTP</span><br>
|
||||
<span id="otp"></span>
|
||||
<div class="lifespan-container">
|
||||
<div id="lifespan" class="lifespan"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Video widget -->
|
||||
|
||||
@@ -15,16 +15,16 @@
|
||||
<!-- Connection form -->
|
||||
<div id="connForm">
|
||||
<p class="TV">📺</p><br>
|
||||
<input type="text" id="dispName" placeholder="Display ID" required autocomplete="off"><br><br>
|
||||
<input type="text" id="otp" placeholder="OTP" required autocomplete="off"><br><br>
|
||||
<button id="connect" disabled autocomplete="off">Connect</button>
|
||||
<input type="text" id="dispIDTb" placeholder="Display Name" required autocomplete="off"><br><br>
|
||||
<input type="text" id="dispOTPTb" placeholder="OTP" required autocomplete="off" maxlength="6"><br><br>
|
||||
<button id="connectBtn" disabled autocomplete="off">Connect</button>
|
||||
</div>
|
||||
|
||||
<!-- Streaming form -->
|
||||
<div id="streamForm" hidden>
|
||||
<h1>You are live!</h1>
|
||||
<video id="localPlayback" autoplay playsinline></video><br><br>
|
||||
<button id="disconnect">Disconnect</button>
|
||||
<button id="disconnectBtn">Disconnect</button>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
|
||||
@@ -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')
|
||||
});
|
||||
@@ -1,71 +1,321 @@
|
||||
// Streaming objects
|
||||
let sock = null;
|
||||
let stream = null;
|
||||
let conn = null;
|
||||
|
||||
// GUI Objects
|
||||
let connForm = document.getElementById('connForm');
|
||||
let dispNameTb = document.getElementById('dispName');
|
||||
let connBtn = document.getElementById('connect');
|
||||
let pinValForm = document.getElementById('pinValForm');
|
||||
let dispPINTb = document.getElementById('dispPIN');
|
||||
let validateBtn = document.getElementById('validate');
|
||||
let streamForm = document.getElementById('streamForm');
|
||||
let locPlayback = document.getElementById('localPlayback');
|
||||
|
||||
// User API class
|
||||
class WisCastUserAPI {
|
||||
class WisCastUserAPIClient {
|
||||
// Socket to the API endpoint
|
||||
#sock;
|
||||
|
||||
// Endpoint URL
|
||||
#endpoint;
|
||||
|
||||
// Called when a config message is received
|
||||
#onconfig = (config) => {};
|
||||
|
||||
// Called when a success message is received
|
||||
#onsuccess = () => {};
|
||||
|
||||
// Called when an error message is received
|
||||
#onerror = (err) => {}
|
||||
|
||||
// Called when a WebRTC answer message is received
|
||||
#onwebrtcanswer = (answer) => {}
|
||||
|
||||
/**
|
||||
* Handler called when an ICE candidate is received
|
||||
* @param {RTCIceCandidateInit} candidate The received ICE candidate.
|
||||
*/
|
||||
onicecandidate = (candidate) => {}
|
||||
|
||||
/**
|
||||
* Create a User API client instance.
|
||||
* @param {String} endpoint URL of the API endpoint.
|
||||
*/
|
||||
constructor(endpoint) {
|
||||
// Save the endpoint
|
||||
this.endpoint = endpoint;
|
||||
this.#endpoint = endpoint;
|
||||
}
|
||||
|
||||
// Connect to the API
|
||||
/**
|
||||
* Connect to the API server.
|
||||
* @returns {Object} Configuration to use for the session.
|
||||
*/
|
||||
async connect() {
|
||||
// Connect to the WebSocket endpoint
|
||||
console.log('Connecting to the API...')
|
||||
this.#sock = new WebSocket(endpoint);
|
||||
return new Promise(async (res) => {
|
||||
// Register the handler for config messages
|
||||
this.#onconfig = (config) => { res(config); };
|
||||
|
||||
// Handle connection
|
||||
sock.addEventListener('open', this.#connectHandler);
|
||||
// Connect to the WebSocket endpoint
|
||||
console.log('Connecting to the API...');
|
||||
this.#sock = new WebSocket(this.#endpoint);
|
||||
|
||||
// Handle messages
|
||||
sock.addEventListener('message', this.#messageHandler);
|
||||
// Handle connection
|
||||
this.#sock.addEventListener('open', async (event) => { await this.#connectHandler(event); });
|
||||
|
||||
// Handle disconnection
|
||||
sock.addEventListener('close', this.#disconnectHandler);
|
||||
// Handle messages
|
||||
this.#sock.addEventListener('message', async (event) => { await this.#messageHandler(event); });
|
||||
|
||||
// Handle disconnection
|
||||
this.#sock.addEventListener('close', async (event) => { await this.#disconnectHandler(event); });
|
||||
});
|
||||
}
|
||||
|
||||
// Connect to a display using its ID and OTP
|
||||
/**
|
||||
* Connect to a display.
|
||||
* @param {String} dispID ID of the display.
|
||||
* @param {String} OTP One-Time-Pass currently shown on the display.
|
||||
*/
|
||||
async connectDisplay(dispID, OTP) {
|
||||
return new Promise(async (res) => {
|
||||
// Register the success and error handlers
|
||||
this.#onsuccess = () => { res(null); }
|
||||
this.#onerror = (err) => { res(err); }
|
||||
|
||||
// Send the connection command
|
||||
await this.#sock.send(JSON.stringify({
|
||||
type: 'connect',
|
||||
dispID: dispID,
|
||||
otp: OTP
|
||||
}))
|
||||
});
|
||||
}
|
||||
|
||||
#connectHandler(event) {
|
||||
console.log('Connected!')
|
||||
/**
|
||||
* Send a WebRTC offer to the display. Must already be connected.
|
||||
* @param {RTCSessionDescriptionInit} offer Offer to send to the display.
|
||||
* @returns {RTCSessionDescriptionInit} The answer from the display or null on error.
|
||||
*/
|
||||
async sendWebRTCOffer(offer) {
|
||||
return new Promise(async (res) => {
|
||||
// Register the answer and error handlers
|
||||
this.#onwebrtcanswer = (answer) => { res(answer); }
|
||||
this.#onerror = (err) => { res(err); }
|
||||
|
||||
// Send the connection command
|
||||
await this.#sock.send(JSON.stringify({
|
||||
type: 'webrtc-offer',
|
||||
offer: offer
|
||||
}))
|
||||
});
|
||||
}
|
||||
|
||||
#messageHandler(event) {
|
||||
console.log(event.data)
|
||||
/**
|
||||
* Send an ICE candidate to the display. Must already be connected.
|
||||
* @param {RTCIceCandidateInit} candidate ICE candidate to send to the display.
|
||||
*/
|
||||
async sendICECandidate(candidate) {
|
||||
// Send the connection command
|
||||
await this.#sock.send(JSON.stringify({
|
||||
type: 'ice-candidate',
|
||||
candidate: candidate
|
||||
}))
|
||||
}
|
||||
|
||||
#disconnectHandler(event) {
|
||||
console.log('Disconnected :/')
|
||||
// Disconnect from the display
|
||||
async disconnectDisplay() {
|
||||
// TODO
|
||||
}
|
||||
|
||||
async #connectHandler(event) {
|
||||
console.log('Connected!');
|
||||
|
||||
// Send initialization message
|
||||
await this.#sock.send(JSON.stringify({
|
||||
type: 'init',
|
||||
clientType: 'user'
|
||||
}))
|
||||
}
|
||||
|
||||
async #messageHandler(event) {
|
||||
// Parse the message
|
||||
const msg = JSON.parse(event.data);
|
||||
|
||||
// Handle the message depending on its type
|
||||
switch (msg.type) {
|
||||
case 'success':
|
||||
// Call the success handler
|
||||
this.#onsuccess();
|
||||
break;
|
||||
|
||||
case 'error':
|
||||
// Call the success handler
|
||||
console.log('Error:', msg.code)
|
||||
this.#onerror(msg.code);
|
||||
break;
|
||||
|
||||
case 'config':
|
||||
// Call the config handler
|
||||
this.#onconfig(msg.config);
|
||||
break;
|
||||
|
||||
case 'webrtc-answer':
|
||||
// Call the answer handler
|
||||
this.#onwebrtcanswer(msg.answer);
|
||||
break;
|
||||
|
||||
case 'ice-candidate':
|
||||
// Call the answer handler
|
||||
this.onicecandidate(msg.candidate);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
async #disconnectHandler(event) {
|
||||
console.log('Disconnected :/');
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
// Create the API connection
|
||||
const api = new WisCastUserAPI(`ws://${location.host}/sig`);
|
||||
// Get GUI objects from their IDs
|
||||
const connForm = document.getElementById('connForm');
|
||||
const dispIDTb = document.getElementById('dispIDTb');
|
||||
const dispOTPTb = document.getElementById('dispOTPTb');
|
||||
const connBtn = document.getElementById('connectBtn');
|
||||
const streamForm = document.getElementById('streamForm');
|
||||
const locPlayback = document.getElementById('localPlayback');
|
||||
const disconnectBtn = document.getElementById('disconnectBtn');
|
||||
|
||||
// Create the API client
|
||||
const client = new WisCastUserAPIClient(`wss://${location.host}/sig`);
|
||||
|
||||
// Global state
|
||||
let config = null;
|
||||
let conn = null;
|
||||
let stream = null;
|
||||
|
||||
// Register a checking function for the contents of the display ID and OTP
|
||||
check = (event) => {
|
||||
// Only enable the connect button if the content of both is valid
|
||||
console.log('change')
|
||||
connBtn.disabled = (dispIDTb.value === '' || dispOTPTb.value.length !== 6 || !config);
|
||||
}
|
||||
dispIDTb.oninput = check;
|
||||
dispOTPTb.oninput = check;
|
||||
|
||||
// Register a handler for when enter is pressed in the display name textbox
|
||||
dispIDTb.onkeyup = (event) => {
|
||||
// Check that the key was enter
|
||||
if (event.key != 'Enter') { return; }
|
||||
|
||||
// Check that the name textbox is not empty
|
||||
if (dispIDTb.value === '') { return; }
|
||||
|
||||
// Select the OTP textbox
|
||||
dispOTPTb.focus();
|
||||
dispOTPTb.select();
|
||||
};
|
||||
|
||||
// Register a handler for when enter is pressed in the display OTP textbox
|
||||
dispOTPTb.onkeyup = (event) => {
|
||||
// Check that the key was enter
|
||||
if (event.key != 'Enter') { return; }
|
||||
|
||||
// Check that the connect button is enabled
|
||||
if (connBtn.disabled) { return; }
|
||||
|
||||
// Press the connect button
|
||||
connBtn.click();
|
||||
};
|
||||
|
||||
// Connect to the server
|
||||
await api.connect();
|
||||
config = await client.connect();
|
||||
|
||||
// Register a handler for clicking the connection button
|
||||
connBtn.onclick = async (event) => {
|
||||
// Disable the text boxes and the button
|
||||
dispIDTb.disabled = true;
|
||||
dispOTPTb.disabled = true;
|
||||
connBtn.disabled = true;
|
||||
|
||||
// Change the status
|
||||
connBtn.textContent = 'Getting permissions...';
|
||||
|
||||
// Get the stream for the screen
|
||||
stream = await navigator.mediaDevices.getDisplayMedia({ video: { cursor: 'always' } });
|
||||
|
||||
// Disable the text boxes and the button
|
||||
dispIDTb.disabled = true;
|
||||
dispOTPTb.disabled = true;
|
||||
connBtn.disabled = true;
|
||||
|
||||
// Change the status
|
||||
connBtn.textContent = 'Authenticating...';
|
||||
|
||||
// Attempt to connect to the display
|
||||
const err = await client.connectDisplay(dispIDTb.value, dispOTPTb.value);
|
||||
if (err) {
|
||||
// TODO: Show the error
|
||||
console.log(err)
|
||||
|
||||
// Reset the GUI
|
||||
dispIDTb.value = '';
|
||||
dispOTPTb.value = '';
|
||||
connBtn.textContent = 'Connect';
|
||||
dispIDTb.disabled = false;
|
||||
dispOTPTb.disabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Change the status
|
||||
connBtn.textContent = 'Connecting...';
|
||||
|
||||
// Create the connection
|
||||
conn = new RTCPeerConnection({'iceServers': [{'urls': config.iceServers[0]}]});
|
||||
|
||||
// Handle ice candidates from user to display
|
||||
conn.onicecandidate = async (event) => {
|
||||
// If there is a new candidate, send it to the peer through websockets
|
||||
if (event.candidate) { await client.sendICECandidate(event.candidate); }
|
||||
};
|
||||
|
||||
// Handle ice candidates from display to user
|
||||
client.onicecandidate = (candidate) => {
|
||||
conn.addIceCandidate(candidate);
|
||||
}
|
||||
|
||||
// Handle connection and disconnection of peer
|
||||
conn.onconnectionstatechange = (event) => {
|
||||
switch (conn.connectionState) {
|
||||
case 'connected':
|
||||
// Switch to stream screen
|
||||
connForm.hidden = true;
|
||||
streamForm.hidden = false;
|
||||
locPlayback.srcObject = stream;
|
||||
console.log("Streaming!")
|
||||
break;
|
||||
|
||||
case 'disconnected':
|
||||
console.log("Stream ended.")
|
||||
// Reload the page to ensure the state is complete reset
|
||||
location.reload();
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
// Start streaming the screen
|
||||
stream.getTracks().forEach(track => {
|
||||
conn.addTrack(track, stream);
|
||||
});
|
||||
|
||||
// If the stream ends, reload the page
|
||||
stream.getVideoTracks()[0].onended = (event) => {
|
||||
location.reload();
|
||||
};
|
||||
|
||||
// Create and send an offer
|
||||
const offer = await conn.createOffer();
|
||||
await conn.setLocalDescription(offer);
|
||||
await conn.setRemoteDescription(await client.sendWebRTCOffer(offer));
|
||||
};
|
||||
|
||||
// Register the disconnect button click event
|
||||
disconnectBtn.onclick = (event) => {
|
||||
// Just reload the page
|
||||
location.reload();
|
||||
};
|
||||
|
||||
// Do a check to potentially enable the connection button
|
||||
check(null);
|
||||
}
|
||||
|
||||
// Run the main function
|
||||
|
||||
Reference in New Issue
Block a user