mirror of
https://github.com/AlexandreRouma/wiscast.git
synced 2026-04-18 07:42:44 +00:00
progress
This commit is contained in:
171
display.go
171
display.go
@@ -1,49 +1,122 @@
|
||||
package main
|
||||
|
||||
// Packages
|
||||
import "github.com/gorilla/websocket"
|
||||
import "errors"
|
||||
import "log"
|
||||
import "net/http"
|
||||
import "sync"
|
||||
import "time"
|
||||
import "github.com/gorilla/websocket"
|
||||
|
||||
// Display instance
|
||||
type Display struct {
|
||||
// WebSocket used to communicate with the display
|
||||
sock *websocket.Conn
|
||||
sockSendMtx sync.Mutex
|
||||
|
||||
// User currently connected to the display
|
||||
user *User
|
||||
|
||||
// One-time-pass currently shown on the display
|
||||
otpMtx sync.Mutex
|
||||
otp string
|
||||
|
||||
// Channel to pass the answer from the display coroutine to the user couroutine
|
||||
answerCh chan string
|
||||
}
|
||||
|
||||
// Helper function to flush channels
|
||||
func chFlush(ch *chan string) {
|
||||
for {
|
||||
empty := false
|
||||
select {
|
||||
// If data is available, read it and try again
|
||||
case <-*ch:
|
||||
continue
|
||||
|
||||
// If no data is available, stop reading
|
||||
default:
|
||||
empty = true
|
||||
}
|
||||
if empty { break }
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to read from a channel with a timeout
|
||||
func chReadTimeout(ch *chan string, timeoutMS int) (string, error) {
|
||||
select {
|
||||
// If data is available, return it with no error
|
||||
case data := <-*ch:
|
||||
return data, nil
|
||||
|
||||
// If no data has been received and the timeout is reached, return an error
|
||||
case <-time.After(timeoutMS * time.Millisecond):
|
||||
return "", errors.New("timeout")
|
||||
}
|
||||
}
|
||||
|
||||
// List of all connected displays
|
||||
var displaysLck sync.Mutex
|
||||
var displays map[string]*Display
|
||||
var displays = map[string]*Display{}
|
||||
|
||||
// Get the display back to its idle state
|
||||
func (this *Display) reset() {
|
||||
// Acquire the sending mutex
|
||||
this.sockSendMtx.Lock()
|
||||
|
||||
// Send a reset command
|
||||
this.sock.WriteMessage(websocket.TextMessage, encodeMessage(Message{
|
||||
mtype: "reset",
|
||||
}))
|
||||
|
||||
// Release the sending mutex
|
||||
this.sockSendMtx.Unlock()
|
||||
}
|
||||
|
||||
// Switch the display to streaming mode
|
||||
func (this *Display) stream() {
|
||||
// Acquire the sending mutex
|
||||
this.sockSendMtx.Lock()
|
||||
|
||||
// Send a show-pin command
|
||||
this.sock.WriteMessage(websocket.TextMessage, encodeMessage(Message{
|
||||
mtype: "stream",
|
||||
}))
|
||||
|
||||
// Release the sending mutex
|
||||
this.sockSendMtx.Unlock()
|
||||
}
|
||||
|
||||
// Send a WebRTC offer to the display and get an answer
|
||||
func (this *Display) webRTCOffer(offer string, timeoutMS int) string {
|
||||
// TODO
|
||||
return ""
|
||||
func (this *Display) webRTCOffer(offer string, timeoutMS int) (string, error) {
|
||||
// Flush the answer channel
|
||||
chFlush(&this.answerCh)
|
||||
|
||||
// Acquire the sending mutex
|
||||
this.sockSendMtx.Lock()
|
||||
|
||||
// Send the offer
|
||||
this.sock.WriteMessage(websocket.TextMessage, encodeMessage(Message{
|
||||
mtype: "webrtc-offer",
|
||||
arguments: map[string]interface{}{
|
||||
"offer": offer,
|
||||
},
|
||||
}))
|
||||
|
||||
// Release the sending mutex
|
||||
this.sockSendMtx.Unlock()
|
||||
|
||||
// TODO: Close the connection if the display failed to respond?
|
||||
|
||||
// Receive the answer
|
||||
return chReadTimeout(&this.answerCh, CONF_TIMEOUT_MS)
|
||||
}
|
||||
|
||||
// Send an ICE candiate to the display
|
||||
func (this *Display) iceCandidate(candidate string) {
|
||||
// Acquire the sending mutex
|
||||
this.sockSendMtx.Lock()
|
||||
|
||||
// Send the candidate
|
||||
sendMessage(this.sock, Message{
|
||||
mtype: "ice-candidate",
|
||||
@@ -51,9 +124,95 @@ func (this *Display) iceCandidate(candidate string) {
|
||||
"candidate": candidate,
|
||||
},
|
||||
})
|
||||
|
||||
// Release the sending mutex
|
||||
this.sockSendMtx.Unlock()
|
||||
}
|
||||
|
||||
// Connection handler for displays
|
||||
func displayHandler(sock *websocket.Conn, dispID string) {
|
||||
func displayHandler(sock *websocket.Conn, dispID string, otp string) {
|
||||
// Create the display object
|
||||
disp := Display{ sock: sock, otp: otp }
|
||||
|
||||
// Acquire the sending mutex
|
||||
disp.sockSendMtx.Lock()
|
||||
|
||||
// Acquire the display list
|
||||
displaysLck.Lock()
|
||||
|
||||
// Check that a display with that ID doesn't already exist
|
||||
if displays[dispID] != nil {
|
||||
// Release the display list
|
||||
displaysLck.Unlock()
|
||||
|
||||
// Send back an error
|
||||
sendErrorMessage(sock, http.StatusConflict)
|
||||
|
||||
// Release the sending mutex
|
||||
disp.sockSendMtx.Unlock()
|
||||
}
|
||||
|
||||
// Add the display to the list
|
||||
displays[dispID] = &disp
|
||||
|
||||
// Release the display list
|
||||
displaysLck.Unlock()
|
||||
|
||||
// Send back the config for the display to use
|
||||
sendMessage(sock, Message{
|
||||
mtype: "config",
|
||||
arguments: map[string]interface{}{
|
||||
"timeout": CONF_TIMEOUT_MS,
|
||||
"iceServers": CONF_ICE_SERVERS,
|
||||
},
|
||||
})
|
||||
|
||||
// Release the sending mutex
|
||||
disp.sockSendMtx.Unlock()
|
||||
|
||||
// Log the new display
|
||||
log.Println("New display: ID='" + dispID + "', OTP='" + otp + "'")
|
||||
|
||||
// Message loop
|
||||
for {
|
||||
// Receive a message
|
||||
msg, err := recvMessage(sock, 0)
|
||||
|
||||
// Give up on the connection if there was an error
|
||||
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 }
|
||||
|
||||
// Acquire the display's OTP
|
||||
disp.otpMtx.Lock()
|
||||
|
||||
// Update the OTP
|
||||
disp.otp = otp
|
||||
|
||||
// Release the display's OTP
|
||||
disp.otpMtx.Unlock()
|
||||
|
||||
case "answer":
|
||||
// Check that the message contains an answer
|
||||
answer, valid := msg.arguments["answer"].(string)
|
||||
if (!valid) { break }
|
||||
|
||||
// Send the answer through the display's answer channel
|
||||
disp.answerCh <- answer
|
||||
|
||||
case "ice-candidate":
|
||||
// TODO
|
||||
|
||||
default:
|
||||
// Give up
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Gracefull disconnect the connected user if there is one
|
||||
}
|
||||
4
main.go
4
main.go
@@ -14,9 +14,9 @@ func main() {
|
||||
http.Handle("/", static)
|
||||
|
||||
// Create a handler for the signaling backend
|
||||
// http.HandleFunc("/sig", wsHandler)
|
||||
http.HandleFunc("/sig", wsHandler)
|
||||
|
||||
// Run the server
|
||||
err := http.ListenAndServe(":3000", nil)
|
||||
if( err != nil) { log.Fatal(err) }
|
||||
if (err != nil) { log.Fatal(err) }
|
||||
}
|
||||
92
message.go
92
message.go
@@ -1,48 +1,110 @@
|
||||
package main
|
||||
|
||||
import "encoding/json"
|
||||
import "errors"
|
||||
import "time"
|
||||
import "github.com/gorilla/websocket"
|
||||
//import "encoding/json"
|
||||
|
||||
// Backend message object
|
||||
type Message struct {
|
||||
// Type of message
|
||||
mtype string
|
||||
mtype string;
|
||||
|
||||
// Arguments of the message
|
||||
arguments map[string]interface{}
|
||||
arguments map[string]interface{};
|
||||
}
|
||||
|
||||
// Get the display back to its idle state
|
||||
func encodeMessage(msg Message) []byte {
|
||||
// TODO
|
||||
return nil
|
||||
// Create the message map and set the message type
|
||||
msgJson := map[string]interface{}{};
|
||||
msgJson["type"] = msg.mtype;
|
||||
|
||||
// Add all arguments
|
||||
for k, v := range msg.arguments {
|
||||
// Skip the type key
|
||||
if (k == "type") { continue };
|
||||
|
||||
// Add the key/value to the argument map
|
||||
msgJson[k] = v;
|
||||
}
|
||||
|
||||
// Serialize the message
|
||||
data, _ := json.Marshal(msgJson);
|
||||
|
||||
// Return the data
|
||||
return data;
|
||||
}
|
||||
|
||||
// Get the display back to its idle state
|
||||
func decodeMessage(data []byte) Message {
|
||||
// TODO
|
||||
return Message{}
|
||||
func decodeMessage(data []byte) (Message, error) {
|
||||
// Attempt to parse the message
|
||||
var msgJson map[string]interface{};
|
||||
err := json.Unmarshal(data, &msgJson);
|
||||
if (err != nil) { return Message{}, err; }
|
||||
|
||||
// If no message type is given, return an error
|
||||
if msgJson["type"] == nil { return Message{}, errors.New("Invalid message"); }
|
||||
|
||||
// If the message type is not a string, return an error
|
||||
mtype, valid := msgJson["type"].(string);
|
||||
if !valid { return Message{}, errors.New("Invalid message"); }
|
||||
|
||||
// Create the message object
|
||||
msg := Message{ mtype: mtype, arguments: map[string]interface{}{} };
|
||||
|
||||
// Extract the arguments
|
||||
for k, v := range msgJson {
|
||||
// Skip the type key
|
||||
if (k == "type") { continue; }
|
||||
|
||||
// Add the key/value to the argument map
|
||||
msg.arguments[k] = v;
|
||||
}
|
||||
|
||||
// Return the decoded message with no error
|
||||
return msg, nil;
|
||||
}
|
||||
|
||||
// Encode a message and send it over a WebSocket
|
||||
func sendMessage(sock *websocket.Conn, msg Message) {
|
||||
// Encode and send the message
|
||||
sock.WriteMessage(websocket.TextMessage, encodeMessage(msg))
|
||||
sock.WriteMessage(websocket.TextMessage, encodeMessage(msg));
|
||||
}
|
||||
|
||||
// Receive a message from a WebSocket and decode it
|
||||
func recvMessage(sock *websocket.Conn, timeoutMS int) Message {
|
||||
// TODO
|
||||
return Message{}
|
||||
func recvMessage(sock *websocket.Conn, timeoutMS int) (Message, error) {
|
||||
for {
|
||||
// Configure the timeout
|
||||
if timeoutMS > 0 {
|
||||
// Milliseconds given
|
||||
sock.SetReadDeadline(time.Now().Add(time.Millisecond * time.Duration(timeoutMS)))
|
||||
} else {
|
||||
// No timeout given
|
||||
sock.SetReadDeadline(time.Time{});
|
||||
}
|
||||
|
||||
// Receive a WebSocket message
|
||||
mt, msgData, err := sock.ReadMessage();
|
||||
|
||||
// If there was an error, give up and return it
|
||||
if (err != nil) { return Message{}, err; }
|
||||
|
||||
// If the message is not a text message, continue waiting
|
||||
if (mt != websocket.TextMessage) { continue; }
|
||||
|
||||
// Return the decoded message
|
||||
return decodeMessage(msgData);
|
||||
}
|
||||
}
|
||||
|
||||
// Encode an error message and send it over a WebSocket
|
||||
func sendErrorMessage(sock *websocket.Conn, err string) {
|
||||
func sendErrorMessage(sock *websocket.Conn, code int) {
|
||||
// Send the error message
|
||||
sock.WriteMessage(websocket.TextMessage, encodeMessage(Message{
|
||||
mtype: "error",
|
||||
arguments: map[string]interface{}{
|
||||
"error": err,
|
||||
"code": code,
|
||||
},
|
||||
}))
|
||||
}));
|
||||
}
|
||||
137
user.go
137
user.go
@@ -1,29 +1,27 @@
|
||||
package main
|
||||
|
||||
// Packages
|
||||
//import "log"
|
||||
import "github.com/gorilla/websocket"
|
||||
//import "encoding/json"
|
||||
import "log"
|
||||
import "sync"
|
||||
import "net/http"
|
||||
import "github.com/gorilla/websocket"
|
||||
|
||||
// General client instance
|
||||
type User struct {
|
||||
// WebSocket used to communicate with the user
|
||||
sock *websocket.Conn
|
||||
sock *websocket.Conn;
|
||||
|
||||
// Display mutex
|
||||
displayMtx sync.Mutex
|
||||
displayMtx sync.Mutex;
|
||||
|
||||
// Display that the user is connecting to
|
||||
display *Display
|
||||
display *Display;
|
||||
}
|
||||
|
||||
// TODO: Check type
|
||||
|
||||
// Connection handler for users
|
||||
func userHandler(sock *websocket.Conn) {
|
||||
// Initialize the user instance
|
||||
user := User{ sock: sock, display: nil }
|
||||
user := User{ sock: sock, display: nil };
|
||||
|
||||
// Send back the config for the user to use
|
||||
sendMessage(sock, Message{
|
||||
@@ -32,110 +30,116 @@ func userHandler(sock *websocket.Conn) {
|
||||
"timeout": CONF_TIMEOUT_MS,
|
||||
"iceServers": CONF_ICE_SERVERS,
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
// Message loop
|
||||
for {
|
||||
// Receive a message
|
||||
msg := recvMessage(sock, 0)
|
||||
msg, err := recvMessage(sock, 0);
|
||||
|
||||
// TODO: exit on error
|
||||
// Give up on the connection if there was an error
|
||||
if (err != nil) { break; }
|
||||
|
||||
// Handle the message depending on its type
|
||||
switch msg.mtype {
|
||||
case "connect":
|
||||
// Check that a display ID was provided
|
||||
if msg.arguments["dispID"] == nil {
|
||||
sendErrorMessage(sock, "Missing display ID")
|
||||
continue;
|
||||
}
|
||||
dispID, valid := msg.arguments["dispID"].(string)
|
||||
if (!valid) { break; }
|
||||
|
||||
// Check that an OTP was provided
|
||||
if msg.arguments["otp"] == nil {
|
||||
sendErrorMessage(sock, "Missing OTP")
|
||||
continue;
|
||||
}
|
||||
otp, valid := msg.arguments["otp"].(string)
|
||||
if (!valid) { break; }
|
||||
|
||||
// Acquire the display ID list
|
||||
displaysLck.Lock()
|
||||
displaysLck.Lock();
|
||||
|
||||
// Check that the display ID exists
|
||||
dispID := msg.arguments["dispID"].(string)
|
||||
if displays[dispID] == nil {
|
||||
if (displays[dispID] == nil) {
|
||||
// Release the display list
|
||||
displaysLck.Unlock()
|
||||
displaysLck.Unlock();
|
||||
|
||||
// Send back an error
|
||||
sendErrorMessage(sock, "Unknown display")
|
||||
sendErrorMessage(sock, http.StatusNotFound);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Acquire the displays OTP
|
||||
displays[dispID].otpMtx.Lock();
|
||||
|
||||
// Check the OTP
|
||||
otp := msg.arguments["otp"].(string)
|
||||
if otp == "" || otp != displays[dispID].otp {
|
||||
if (otp == "" || otp != displays[dispID].otp) {
|
||||
// Release the display's OTP
|
||||
displays[dispID].otpMtx.Unlock();
|
||||
|
||||
// Release the display list
|
||||
displaysLck.Unlock()
|
||||
displaysLck.Unlock();
|
||||
|
||||
// Send back an error
|
||||
sendErrorMessage(sock, "Invalid OTP")
|
||||
sendErrorMessage(sock, http.StatusUnauthorized);
|
||||
continue;
|
||||
}
|
||||
|
||||
// TODO: Check types
|
||||
// Release the display's OTP
|
||||
displays[dispID].otpMtx.Unlock();
|
||||
|
||||
// Acquire the user's display pointer
|
||||
user.displayMtx.Lock()
|
||||
user.displayMtx.Lock();
|
||||
|
||||
// Register the user and display to each other
|
||||
user.display = displays[dispID]
|
||||
user.display.user = &user
|
||||
user.display = displays[dispID];
|
||||
user.display.user = &user;
|
||||
|
||||
// Put the display into streaming mode
|
||||
user.display.stream()
|
||||
user.display.stream();
|
||||
|
||||
// TODO: Check for error
|
||||
|
||||
// Release the user's display pointer
|
||||
user.displayMtx.Lock()
|
||||
user.displayMtx.Lock();
|
||||
|
||||
// Release the display list
|
||||
displaysLck.Unlock()
|
||||
displaysLck.Unlock();
|
||||
|
||||
// Log the connection
|
||||
log.Println("User successfully connected to display: ID='", dispID, "'");
|
||||
|
||||
// Notify the user of the successful connection
|
||||
sendMessage(sock, Message{
|
||||
mtype: "success",
|
||||
})
|
||||
});
|
||||
|
||||
case "webrtc-offer":
|
||||
// Check that the message contains an offer
|
||||
if msg.arguments["offer"] == nil {
|
||||
// Send back an error
|
||||
sendErrorMessage(sock, "No offer given")
|
||||
continue;
|
||||
}
|
||||
|
||||
// TODO: Check type
|
||||
offer, valid := msg.arguments["offer"].(string)
|
||||
if (!valid) { break; }
|
||||
|
||||
// Acquire the user's display pointer
|
||||
user.displayMtx.Lock()
|
||||
user.displayMtx.Lock();
|
||||
|
||||
// Check that the user is connected to a display
|
||||
if user.display == nil {
|
||||
if (user.display == nil) {
|
||||
// Release the user's display pointer
|
||||
user.displayMtx.Unlock()
|
||||
user.displayMtx.Unlock();
|
||||
|
||||
// Send back an error
|
||||
sendErrorMessage(sock, "Not connected")
|
||||
sendErrorMessage(sock, http.StatusForbidden);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Send the offer to the display and get the response
|
||||
answer := user.display.webRTCOffer(msg.arguments["offer"].(string), CONF_TIMEOUT_MS)
|
||||
answer, err := user.display.webRTCOffer(offer, CONF_TIMEOUT_MS);
|
||||
if (err != nil) {
|
||||
// Release the user's display pointer
|
||||
user.displayMtx.Unlock();
|
||||
|
||||
// TODO: Check for error
|
||||
// Send back an error
|
||||
sendErrorMessage(sock, http.StatusBadGateway);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Release the user's display pointer
|
||||
user.displayMtx.Unlock()
|
||||
user.displayMtx.Unlock();
|
||||
|
||||
// Send back the response
|
||||
sendMessage(sock, Message{
|
||||
@@ -143,42 +147,39 @@ func userHandler(sock *websocket.Conn) {
|
||||
arguments: map[string]interface{}{
|
||||
"answer": answer,
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
case "ice-candidate":
|
||||
// Check that the message contains an ice candidate
|
||||
if msg.arguments["candidate"] == nil {
|
||||
// Send back an error
|
||||
sendErrorMessage(sock, "No offer given")
|
||||
continue;
|
||||
}
|
||||
|
||||
// TODO: Check type
|
||||
candidate, valid := msg.arguments["candidate"].(string)
|
||||
if (!valid) { break; }
|
||||
|
||||
// Acquire the user's display pointer
|
||||
user.displayMtx.Lock()
|
||||
user.displayMtx.Lock();
|
||||
|
||||
// Check that the user is connected to a display
|
||||
if user.display == nil {
|
||||
if (user.display == nil) {
|
||||
// Release the user's display pointer
|
||||
user.displayMtx.Unlock()
|
||||
user.displayMtx.Unlock();
|
||||
|
||||
// Send back an error
|
||||
sendErrorMessage(sock, "Not connected")
|
||||
sendErrorMessage(sock, http.StatusForbidden);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Send the ice candidtate to the display
|
||||
user.display.iceCandidate(msg.arguments["candidate"].(string))
|
||||
user.display.iceCandidate(candidate);
|
||||
|
||||
// TODO: Check error
|
||||
|
||||
// Release the user's display pointer
|
||||
user.displayMtx.Unlock()
|
||||
user.displayMtx.Unlock();
|
||||
|
||||
default:
|
||||
// Send back an error
|
||||
sendErrorMessage(sock, "Invalid message type")
|
||||
// Give up
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If the user was connected to a display, disconnect it
|
||||
// TODO: Gracefull disconnect the connected display if there is one
|
||||
}
|
||||
14
wshandler.go
14
wshandler.go
@@ -21,7 +21,10 @@ func wsHandler(respWriter http.ResponseWriter, req *http.Request) {
|
||||
defer sock.Close()
|
||||
|
||||
// Receive the init message
|
||||
msg := recvMessage(sock, 5000)
|
||||
msg, err := recvMessage(sock, 5000)
|
||||
|
||||
// If there was an error or timeout, give up on the connection
|
||||
if err != nil { return }
|
||||
|
||||
// If it's not an init message, give up
|
||||
if msg.mtype != "init" { return }
|
||||
@@ -34,9 +37,14 @@ func wsHandler(respWriter http.ResponseWriter, req *http.Request) {
|
||||
|
||||
case "display":
|
||||
// Check that the display has provided its ID
|
||||
if msg.arguments["dispID"] == nil { return }
|
||||
dispID, valid := msg.arguments["dispID"].(string)
|
||||
if !valid { return }
|
||||
|
||||
// Check that the display has provided its OTP
|
||||
otp, valid := msg.arguments["otp"].(string)
|
||||
if !valid { return }
|
||||
|
||||
// Handle as a display
|
||||
displayHandler(sock, msg.arguments["dispID"].(string))
|
||||
displayHandler(sock, dispID, otp)
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
<meta charset="UTF-8">
|
||||
|
||||
<!-- Title of the software -->
|
||||
<title>Quick Screen Share - Screen</title>
|
||||
<title>WisCast - Display</title>
|
||||
|
||||
<!-- Load the stylesheet from the main style file -->
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
@@ -15,11 +15,8 @@
|
||||
<div id="idleScreen">
|
||||
<span id="dispIDLabel">Display ID:</span><br>
|
||||
<span id="dispID"></span>
|
||||
</div>
|
||||
|
||||
<div id="pinScreen" hidden>
|
||||
<span id="pinLabel">PIN:</span><br>
|
||||
<span id="pin"></span>
|
||||
<span id="otpLabel">OTP:</span><br>
|
||||
<span id="otp"></span>
|
||||
</div>
|
||||
|
||||
<!-- Video widget -->
|
||||
@@ -28,9 +25,9 @@
|
||||
|
||||
<!-- Footer to show credit to the software and developers -->
|
||||
<footer id="credits">
|
||||
Powered by <a href="https://x.com/ryzerth" target="_blank">Quick Screen Share</a>. Made with ❤️ by <a href="https://x.com/ryzerth" target="_blank">Ryzerth</a>
|
||||
Powered by <a href="https://wiscast.org/" target="_blank">WisCast</a>. Made with ❤️ by <a href="https://x.com/ryzerth" target="_blank">Ryzerth</a>
|
||||
</footer>
|
||||
|
||||
<!-- Main screen code -->
|
||||
<script src="/scripts/screen.js"></script>
|
||||
<script src="/scripts/display.js"></script>
|
||||
</html>
|
||||
@@ -5,7 +5,7 @@
|
||||
<meta charset="UTF-8">
|
||||
|
||||
<!-- Title of the software -->
|
||||
<title>Quick Screen Share</title>
|
||||
<title>WisCast - Connect</title>
|
||||
|
||||
<!-- Load the stylesheet from the main style file -->
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
@@ -30,9 +30,9 @@
|
||||
|
||||
<!-- Footer to show credit to the software and developers -->
|
||||
<footer>
|
||||
Powered by <a href="https://x.com/ryzerth" target="_blank">Quick Screen Share</a>. Made with ❤️ by <a href="https://x.com/ryzerth" target="_blank">Ryzerth</a>
|
||||
Powered by <a href="https://wiscast.org/" target="_blank">WisCast</a>. Made with ❤️ by <a href="https://x.com/ryzerth" target="_blank">Ryzerth</a>
|
||||
</footer>
|
||||
|
||||
<!-- Main client code -->
|
||||
<script src="/scripts/client.js"></script>
|
||||
<script src="/scripts/user.js"></script>
|
||||
</html>
|
||||
@@ -1,185 +0,0 @@
|
||||
// Streaming objects
|
||||
let sock = null;
|
||||
let stream = null;
|
||||
let conn = null;
|
||||
|
||||
// GUI Objects
|
||||
let connForm = document.querySelector('#connForm');
|
||||
let dispNameTb = document.querySelector('#dispName');
|
||||
let connBtn = document.querySelector('#connect');
|
||||
let pinValForm = document.querySelector('#pinValForm');
|
||||
let dispPINTb = document.querySelector('#dispPIN');
|
||||
let validateBtn = document.querySelector('#validate');
|
||||
let streamForm = document.querySelector('#streamForm');
|
||||
let locPlayback = document.querySelector('#localPlayback');
|
||||
|
||||
// Make sure the connect button cannot be pressed if there is no display name
|
||||
function onDispNameChange() {
|
||||
connBtn.disabled = !(dispNameTb.value.length > 0);
|
||||
}
|
||||
function onPINChange() {
|
||||
validateBtn.disabled = !(dispPINTb.value.length === 6);
|
||||
}
|
||||
|
||||
// Handle enter key pressed
|
||||
dispNameTb.addEventListener('keyup', (event) => {
|
||||
if (event.key === 'Enter' && !connBtn.disabled) {
|
||||
connBtn.click();
|
||||
}
|
||||
})
|
||||
dispPINTb.addEventListener('keyup', (event) => {
|
||||
if (event.key === 'Enter' && !validateBtn.disabled) {
|
||||
validateBtn.click();
|
||||
}
|
||||
})
|
||||
|
||||
// WebSocket message handler
|
||||
async function onMessage(data) {
|
||||
// Parse the message
|
||||
console.log(data)
|
||||
const msg = JSON.parse(data);
|
||||
|
||||
// Process depending on the type
|
||||
switch (msg.type) {
|
||||
case 'disp-ok':
|
||||
// Switch to the PIN screen
|
||||
connForm.hidden = true;
|
||||
pinValForm.hidden = false;
|
||||
dispPINTb.focus();
|
||||
dispPINTb.select();
|
||||
break;
|
||||
|
||||
case 'disp-error':
|
||||
// Show the error
|
||||
// TODO
|
||||
console.log('ERROR: ' + msg.error)
|
||||
break;
|
||||
|
||||
case 'auth-ok':
|
||||
// Show the status update
|
||||
validateBtn.textContent = 'Starting the Stream...';
|
||||
await startStream();
|
||||
break;
|
||||
|
||||
case 'answer':
|
||||
console.log('Got answer')
|
||||
// Pass on the offer to WebRTC
|
||||
await conn.setRemoteDescription(new RTCSessionDescription(msg.answer));
|
||||
break;
|
||||
|
||||
case 'ice-candidate':
|
||||
console.log('Got ice candidate')
|
||||
// Add the ice candidate to the WebRTC connection
|
||||
await conn.addIceCandidate(msg.candidate);
|
||||
break;
|
||||
|
||||
case 'disconnect':
|
||||
// Reload the page
|
||||
location.reload();
|
||||
break;
|
||||
|
||||
default:
|
||||
console.dir(msg)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
async function startStream() {
|
||||
// Get the stream for the screen
|
||||
stream = await navigator.mediaDevices.getDisplayMedia({ video: { cursor: 'always' } });
|
||||
|
||||
// Create the connection
|
||||
conn = new RTCPeerConnection({'iceServers': [{'urls': 'stun:stun.l.google.com:19302'}]});
|
||||
|
||||
// Handle 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 stream screen
|
||||
pinValForm.hidden = true;
|
||||
streamForm.hidden = false;
|
||||
locPlayback.srcObject = stream;
|
||||
|
||||
console.log("Connected!")
|
||||
|
||||
break;
|
||||
|
||||
case 'disconnected':
|
||||
console.log("Disconnected.")
|
||||
// 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].addEventListener('ended', (event) => {
|
||||
location.reload();
|
||||
})
|
||||
|
||||
// Create and send an offer
|
||||
const offer = await conn.createOffer();
|
||||
await conn.setLocalDescription(offer);
|
||||
await sock.send(JSON.stringify({
|
||||
type: 'offer',
|
||||
offer: offer
|
||||
}))
|
||||
}
|
||||
|
||||
async function connect() {
|
||||
// Disable the connect button and show status
|
||||
dispNameTb.disabled = true;
|
||||
connBtn.disabled = true;
|
||||
connBtn.textContent = 'Connecting...';
|
||||
|
||||
// Send a connect command to the server
|
||||
await sock.send(JSON.stringify({
|
||||
type: 'connect',
|
||||
dispID: dispNameTb.value
|
||||
}));
|
||||
}
|
||||
|
||||
async function validatePIN() {
|
||||
// Disable the validate button and show status
|
||||
dispPINTb.disabled = true;
|
||||
validateBtn.disabled = true;
|
||||
validateBtn.textContent = 'Checking the PIN...';
|
||||
|
||||
// Send the validate pin command to the server
|
||||
await sock.send(JSON.stringify({
|
||||
type: 'validate-pin',
|
||||
pin: dispPINTb.value
|
||||
}));
|
||||
}
|
||||
|
||||
async function disconnect() {
|
||||
// Just reload the page
|
||||
location.reload();
|
||||
}
|
||||
|
||||
// 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')
|
||||
});
|
||||
sock.addEventListener('message', (event) => { onMessage(event.data); })
|
||||
39
www/scripts/display.js
Normal file
39
www/scripts/display.js
Normal file
@@ -0,0 +1,39 @@
|
||||
// Streaming objects
|
||||
let sock = null;
|
||||
let conn = null;
|
||||
|
||||
// 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');
|
||||
|
||||
// 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')
|
||||
|
||||
// // DEBUGGING ONLY
|
||||
// await sock.send(JSON.stringify({
|
||||
// type: 'init',
|
||||
// pin: dispPINTb.value
|
||||
// }))
|
||||
|
||||
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,155 +0,0 @@
|
||||
// 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, 6);
|
||||
}
|
||||
|
||||
// App objects
|
||||
let sock = null;
|
||||
let conn = null;
|
||||
|
||||
// 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');
|
||||
|
||||
// Show the display ID
|
||||
dispIDSpan.textContent = dispID;
|
||||
|
||||
async function reset() {
|
||||
// Completely reset the state
|
||||
playback.hidden = true;
|
||||
pinScreen.hidden = true;
|
||||
idleScreen.hidden = false;
|
||||
credits.hidden = false;
|
||||
playback.srcObject = null;
|
||||
conn = null;
|
||||
|
||||
// Initialize WebRTC
|
||||
await initRTC();
|
||||
}
|
||||
|
||||
// WebSocket message handler
|
||||
async function onMessage(data) {
|
||||
// Parse the message
|
||||
console.log(data)
|
||||
const msg = JSON.parse(data);
|
||||
|
||||
// Process depending on the type
|
||||
switch (msg.type) {
|
||||
case 'conn-req':
|
||||
// Show the pin
|
||||
pinSpan.textContent = msg.pin;
|
||||
|
||||
// Switch to pin mode
|
||||
idleScreen.hidden = true;
|
||||
pinScreen.hidden = false;
|
||||
break;
|
||||
|
||||
case 'offer':
|
||||
console.log('Got offer')
|
||||
// Pass on the offer to WebRTC
|
||||
await conn.setRemoteDescription(new RTCSessionDescription(msg.offer));
|
||||
|
||||
// Create an answer
|
||||
answer = await conn.createAnswer();
|
||||
await conn.setLocalDescription(answer);
|
||||
|
||||
// Encode and send the answer
|
||||
await sock.send(JSON.stringify({
|
||||
type: 'answer',
|
||||
answer: answer
|
||||
}))
|
||||
break;
|
||||
|
||||
case 'ice-candidate':
|
||||
console.log('Got ice candidate')
|
||||
// Add the ice candidate to the WebRTC connection
|
||||
await conn.addIceCandidate(msg.candidate);
|
||||
break;
|
||||
|
||||
case 'auth-ok':
|
||||
|
||||
break;
|
||||
|
||||
case 'disconnect':
|
||||
// Reset the display
|
||||
reset();
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
async function initRTC() {
|
||||
// Create the WebRTC connection
|
||||
conn = new RTCPeerConnection({'iceServers': [{'urls': 'stun:stun.l.google.com:19302'}]});
|
||||
|
||||
// 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
|
||||
pinScreen.hidden = true;
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
// Main function for the app
|
||||
async function main() {
|
||||
// Connect to the server using WebSockets
|
||||
console.log('Connecting to websocket...')
|
||||
sock = new WebSocket(`ws://${location.host}/sig`);
|
||||
|
||||
// Add handlers for the socket
|
||||
sock.addEventListener('message', (event) => { onMessage(event.data); })
|
||||
|
||||
sock.addEventListener('open', async (event) => {
|
||||
console.log('Connected to websocket')
|
||||
// Tell the server that this is a screen
|
||||
await sock.send(JSON.stringify({
|
||||
type: 'screen',
|
||||
name: dispID
|
||||
}))
|
||||
|
||||
// Initialize WebRTC
|
||||
await initRTC();
|
||||
});
|
||||
}
|
||||
|
||||
// Run the main function
|
||||
main();
|
||||
40
www/scripts/user.js
Normal file
40
www/scripts/user.js
Normal file
@@ -0,0 +1,40 @@
|
||||
// Streaming objects
|
||||
let sock = null;
|
||||
let stream = null;
|
||||
let conn = null;
|
||||
|
||||
// GUI Objects
|
||||
let connForm = document.querySelector('#connForm');
|
||||
let dispNameTb = document.querySelector('#dispName');
|
||||
let connBtn = document.querySelector('#connect');
|
||||
let pinValForm = document.querySelector('#pinValForm');
|
||||
let dispPINTb = document.querySelector('#dispPIN');
|
||||
let validateBtn = document.querySelector('#validate');
|
||||
let streamForm = document.querySelector('#streamForm');
|
||||
let locPlayback = document.querySelector('#localPlayback');
|
||||
|
||||
// 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')
|
||||
|
||||
// // DEBUGGING ONLY
|
||||
// await sock.send(JSON.stringify({
|
||||
// type: 'init',
|
||||
// pin: dispPINTb.value
|
||||
// }))
|
||||
|
||||
await sock.send(JSON.stringify({
|
||||
type: 'init',
|
||||
clientType: 'user'
|
||||
}));
|
||||
});
|
||||
|
||||
sock.addEventListener('message', (event) => {
|
||||
console.log(event.data)
|
||||
});
|
||||
|
||||
sock.addEventListener('close', (event) => {
|
||||
console.log('Disconnected from websocket')
|
||||
});
|
||||
Reference in New Issue
Block a user