Volume Controls, Mute/Unmute Video
* Added a top panel to put your video controls in. * Broadcaster can mute or unmute their own audio input. * When viewing others' cams, buttons appear to control their video: * Their username is displayed in the corner. * Mute/unmute button to silence their audio. * "X" button to close their camera. * Button to show what viewers are currently watching your camera. * Add an "About" page and config for app branding. * Add dark theme CSS for prefers-dark browsers.
This commit is contained in:
parent
1ecff195ac
commit
d8de60c990
|
@ -18,6 +18,7 @@ type Config struct {
|
||||||
SecretKey string
|
SecretKey string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Title string
|
||||||
WebsiteURL string
|
WebsiteURL string
|
||||||
|
|
||||||
PublicChannels []Channel
|
PublicChannels []Channel
|
||||||
|
@ -46,6 +47,7 @@ var Current = DefaultConfig()
|
||||||
// settings.toml file to disk.
|
// settings.toml file to disk.
|
||||||
func DefaultConfig() Config {
|
func DefaultConfig() Config {
|
||||||
var c = Config{
|
var c = Config{
|
||||||
|
Title: "BareRTC",
|
||||||
WebsiteURL: "https://www.example.com",
|
WebsiteURL: "https://www.example.com",
|
||||||
PublicChannels: []Channel{
|
PublicChannels: []Channel{
|
||||||
{
|
{
|
||||||
|
|
|
@ -198,3 +198,33 @@ func (s *Server) OnSDP(sub *Subscriber, msg Message) {
|
||||||
Description: msg.Description,
|
Description: msg.Description,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OnWatch communicates video watching status between users.
|
||||||
|
func (s *Server) OnWatch(sub *Subscriber, msg Message) {
|
||||||
|
// Look up the other subscriber.
|
||||||
|
other, err := s.GetSubscriber(msg.Username)
|
||||||
|
if err != nil {
|
||||||
|
sub.ChatServer(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
other.SendJSON(Message{
|
||||||
|
Action: ActionWatch,
|
||||||
|
Username: sub.Username,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnUnwatch communicates video Unwatching status between users.
|
||||||
|
func (s *Server) OnUnwatch(sub *Subscriber, msg Message) {
|
||||||
|
// Look up the other subscriber.
|
||||||
|
other, err := s.GetSubscriber(msg.Username)
|
||||||
|
if err != nil {
|
||||||
|
sub.ChatServer(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
other.SendJSON(Message{
|
||||||
|
Action: ActionUnwatch,
|
||||||
|
Username: sub.Username,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -32,6 +32,8 @@ const (
|
||||||
ActionMe = "me" // user self-info sent by FE or BE
|
ActionMe = "me" // user self-info sent by FE or BE
|
||||||
ActionOpen = "open" // user wants to view a webcam (open WebRTC)
|
ActionOpen = "open" // user wants to view a webcam (open WebRTC)
|
||||||
ActionRing = "ring" // receiver of a WebRTC open request
|
ActionRing = "ring" // receiver of a WebRTC open request
|
||||||
|
ActionWatch = "watch" // user has received video and is watching you
|
||||||
|
ActionUnwatch = "unwatch" // user has closed your video
|
||||||
|
|
||||||
// Actions sent by server only
|
// Actions sent by server only
|
||||||
ActionPing = "ping"
|
ActionPing = "ping"
|
||||||
|
|
38
pkg/pages.go
38
pkg/pages.go
|
@ -61,12 +61,9 @@ func IndexPage() http.HandlerFunc {
|
||||||
}
|
}
|
||||||
|
|
||||||
tmpl.Funcs(template.FuncMap{
|
tmpl.Funcs(template.FuncMap{
|
||||||
// Cache busting random string for JS and CSS dependency.
|
"AsHTML": func(v string) template.HTML {
|
||||||
// "CacheHash": func() string {
|
return template.HTML(v)
|
||||||
// return util.RandomString(8)
|
},
|
||||||
// },
|
|
||||||
|
|
||||||
//
|
|
||||||
})
|
})
|
||||||
tmpl, err := tmpl.ParseFiles("web/templates/chat.html")
|
tmpl, err := tmpl.ParseFiles("web/templates/chat.html")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -78,3 +75,32 @@ func IndexPage() http.HandlerFunc {
|
||||||
tmpl.ExecuteTemplate(w, "index", values)
|
tmpl.ExecuteTemplate(w, "index", values)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AboutPage returns the HTML template for the about page.
|
||||||
|
func AboutPage() http.HandlerFunc {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Load the template, TODO: once on server startup.
|
||||||
|
tmpl := template.New("index")
|
||||||
|
|
||||||
|
// Variables to give to the front-end page.
|
||||||
|
var values = map[string]interface{}{
|
||||||
|
// A cache-busting hash for JS and CSS includes.
|
||||||
|
"CacheHash": util.RandomString(8),
|
||||||
|
|
||||||
|
// The current website settings.
|
||||||
|
"Config": config.Current,
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpl.Funcs(template.FuncMap{
|
||||||
|
"AsHTML": func(v string) template.HTML {
|
||||||
|
return template.HTML(v)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
tmpl, err := tmpl.ParseFiles("web/templates/about.html")
|
||||||
|
if err != nil {
|
||||||
|
panic(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpl.ExecuteTemplate(w, "index", values)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -31,6 +31,7 @@ func (s *Server) Setup() error {
|
||||||
var mux = http.NewServeMux()
|
var mux = http.NewServeMux()
|
||||||
|
|
||||||
mux.Handle("/", IndexPage())
|
mux.Handle("/", IndexPage())
|
||||||
|
mux.Handle("/about", AboutPage())
|
||||||
mux.Handle("/ws", s.WebSocket())
|
mux.Handle("/ws", s.WebSocket())
|
||||||
mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("web/static"))))
|
mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("web/static"))))
|
||||||
|
|
||||||
|
|
|
@ -73,6 +73,10 @@ func (sub *Subscriber) ReadLoop(s *Server) {
|
||||||
s.OnCandidate(sub, msg)
|
s.OnCandidate(sub, msg)
|
||||||
case ActionSDP:
|
case ActionSDP:
|
||||||
s.OnSDP(sub, msg)
|
s.OnSDP(sub, msg)
|
||||||
|
case ActionWatch:
|
||||||
|
s.OnWatch(sub, msg)
|
||||||
|
case ActionUnwatch:
|
||||||
|
s.OnUnwatch(sub, msg)
|
||||||
default:
|
default:
|
||||||
sub.ChatServer("Unsupported message type.")
|
sub.ChatServer("Unsupported message type.")
|
||||||
}
|
}
|
||||||
|
|
8816
web/static/css/bulma-prefers-dark.css
Normal file
8816
web/static/css/bulma-prefers-dark.css
Normal file
File diff suppressed because it is too large
Load Diff
|
@ -28,14 +28,22 @@ body {
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
background-color: rgba(255, 0, 0, 0.2);
|
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
|
|
||||||
|
background: rgb(190,190,190);
|
||||||
|
background: linear-gradient(0deg, rgb(172, 172, 172) 0%, rgb(214, 214, 214) 100%);
|
||||||
|
|
||||||
display: grid;
|
display: grid;
|
||||||
column-gap: 10px;
|
column-gap: 10px;
|
||||||
row-gap: 10px;
|
row-gap: 10px;
|
||||||
grid-template-columns: 260px 1fr 280px;
|
grid-template-columns: 260px 1fr 280px;
|
||||||
grid-template-rows: 1fr auto;
|
grid-template-rows: auto 1fr auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header row */
|
||||||
|
.chat-container > .chat-header {
|
||||||
|
grid-column: 1 / 4;
|
||||||
|
grid-row: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Left column: DMs and channels */
|
/* Left column: DMs and channels */
|
||||||
|
@ -47,7 +55,7 @@ body {
|
||||||
/* Main column: chat history */
|
/* Main column: chat history */
|
||||||
.chat-container > .chat-column {
|
.chat-container > .chat-column {
|
||||||
grid-column: 2;
|
grid-column: 2;
|
||||||
grid-row: 1;
|
grid-row: 2;
|
||||||
background-color: yellow;
|
background-color: yellow;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
@ -55,7 +63,7 @@ body {
|
||||||
/* Footer row: message entry box */
|
/* Footer row: message entry box */
|
||||||
.chat-container > .chat-footer {
|
.chat-container > .chat-footer {
|
||||||
grid-column: 1 / 4;
|
grid-column: 1 / 4;
|
||||||
grid-row: 2 / 2;
|
grid-row: 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Right column: Who List */
|
/* Right column: Who List */
|
||||||
|
@ -118,7 +126,7 @@ body {
|
||||||
*******************/
|
*******************/
|
||||||
|
|
||||||
.video-feeds {
|
.video-feeds {
|
||||||
background-color: yellow;
|
background-color: #222;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
overflow-x: scroll;
|
overflow-x: scroll;
|
||||||
|
@ -128,9 +136,38 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
.video-feeds > .feed {
|
.video-feeds > .feed {
|
||||||
flex: 10 0 auto;
|
position: relative;
|
||||||
width: 120px;
|
flex: 0 0 144px;
|
||||||
height: 80px;
|
width: 168px;
|
||||||
|
height: 112px;
|
||||||
background-color: black;
|
background-color: black;
|
||||||
margin: 5px;
|
margin: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.video-feeds > .feed > video {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-feeds > .feed > .controls {
|
||||||
|
position: absolute;
|
||||||
|
background: rgba(0, 0, 0, 0.75);
|
||||||
|
right: 4px;
|
||||||
|
bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-feeds > .feed > .close {
|
||||||
|
position: absolute;
|
||||||
|
right: 4px;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-feeds > .feed > .caption {
|
||||||
|
position: absolute;
|
||||||
|
background: rgba(0, 0, 0, 0.75);
|
||||||
|
color: #fff;
|
||||||
|
top: 4px;
|
||||||
|
left: 4px;
|
||||||
|
font-size: small;
|
||||||
|
padding: 2px 4px;
|
||||||
|
}
|
|
@ -47,12 +47,17 @@ const app = Vue.createApp({
|
||||||
active: false,
|
active: false,
|
||||||
elem: null, // <video id="localVideo"> element
|
elem: null, // <video id="localVideo"> element
|
||||||
stream: null, // MediaStream object
|
stream: null, // MediaStream object
|
||||||
|
muted: false, // our outgoing mic is muted, not by default
|
||||||
|
|
||||||
|
// Who all is watching me? map of users.
|
||||||
|
watching: {},
|
||||||
},
|
},
|
||||||
|
|
||||||
// WebRTC sessions with other users.
|
// WebRTC sessions with other users.
|
||||||
WebRTC: {
|
WebRTC: {
|
||||||
// Streams per username.
|
// Streams per username.
|
||||||
streams: {},
|
streams: {},
|
||||||
|
muted: {}, // muted bool per username
|
||||||
|
|
||||||
// RTCPeerConnections per username.
|
// RTCPeerConnections per username.
|
||||||
pc: {},
|
pc: {},
|
||||||
|
@ -113,7 +118,7 @@ const app = Vue.createApp({
|
||||||
this.initHistory(channel.ID);
|
this.initHistory(channel.ID);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.ChatServer("Welcome to BareRTC!")
|
this.ChatClient("Welcome to BareRTC!");
|
||||||
|
|
||||||
// Auto login with JWT token?
|
// Auto login with JWT token?
|
||||||
// TODO: JWT validation on the WebSocket as well.
|
// TODO: JWT validation on the WebSocket as well.
|
||||||
|
@ -237,6 +242,12 @@ const app = Vue.createApp({
|
||||||
this.closeVideo(row.username);
|
this.closeVideo(row.username);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Has the back-end server forgotten we are on video? This can
|
||||||
|
// happen if we disconnect/reconnect while we were streaming.
|
||||||
|
if (this.webcam.active && !this.whoMap[this.username]?.videoActive) {
|
||||||
|
this.sendMe();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Send a video request to access a user's camera.
|
// Send a video request to access a user's camera.
|
||||||
|
@ -361,6 +372,12 @@ const app = Vue.createApp({
|
||||||
case "sdp":
|
case "sdp":
|
||||||
this.onSDP(msg);
|
this.onSDP(msg);
|
||||||
break;
|
break;
|
||||||
|
case "watch":
|
||||||
|
this.onWatch(msg);
|
||||||
|
break;
|
||||||
|
case "unwatch":
|
||||||
|
this.onUnwatch(msg);
|
||||||
|
break;
|
||||||
case "error":
|
case "error":
|
||||||
this.pushHistory({
|
this.pushHistory({
|
||||||
channel: msg.channel,
|
channel: msg.channel,
|
||||||
|
@ -414,7 +431,7 @@ const app = Vue.createApp({
|
||||||
// this.ChatClient("We are the offerer - set up onNegotiationNeeded");
|
// this.ChatClient("We are the offerer - set up onNegotiationNeeded");
|
||||||
pc.onnegotiationneeded = () => {
|
pc.onnegotiationneeded = () => {
|
||||||
console.error("WebRTC OnNegotiationNeeded called!");
|
console.error("WebRTC OnNegotiationNeeded called!");
|
||||||
this.ChatClient("Negotiation Needed, creating WebRTC offer.");
|
// this.ChatClient("Negotiation Needed, creating WebRTC offer.");
|
||||||
pc.createOffer().then(this.localDescCreated(pc, username)).catch(this.ChatClient);
|
pc.createOffer().then(this.localDescCreated(pc, username)).catch(this.ChatClient);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -433,12 +450,14 @@ const app = Vue.createApp({
|
||||||
}
|
}
|
||||||
|
|
||||||
window.requestAnimationFrame(() => {
|
window.requestAnimationFrame(() => {
|
||||||
this.ChatServer("Setting <video> srcObject for " + username);
|
|
||||||
let $ref = document.getElementById(`videofeed-${username}`);
|
let $ref = document.getElementById(`videofeed-${username}`);
|
||||||
console.log("Video elem:", $ref);
|
console.log("Video elem:", $ref);
|
||||||
$ref.srcObject = stream;
|
$ref.srcObject = stream;
|
||||||
// this.$refs[`videofeed-${username}`].srcObject = stream;
|
// this.$refs[`videofeed-${username}`].srcObject = stream;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Inform them they are being watched.
|
||||||
|
this.sendWatch(username, true);
|
||||||
};
|
};
|
||||||
|
|
||||||
// If we were already broadcasting video, send our stream to
|
// If we were already broadcasting video, send our stream to
|
||||||
|
@ -535,6 +554,21 @@ const app = Vue.createApp({
|
||||||
}
|
}
|
||||||
}, console.error);
|
}, console.error);
|
||||||
},
|
},
|
||||||
|
onWatch(msg) {
|
||||||
|
// The user has our video feed open now.
|
||||||
|
this.webcam.watching[msg.username] = true;
|
||||||
|
},
|
||||||
|
onUnwatch(msg) {
|
||||||
|
// The user has closed our video feed.
|
||||||
|
delete(this.webcam.watching[msg.username]);
|
||||||
|
},
|
||||||
|
sendWatch(username, watching) {
|
||||||
|
// Send the watch or unwatch message to backend.
|
||||||
|
this.ws.conn.send(JSON.stringify({
|
||||||
|
action: watching ? "watch" : "unwatch",
|
||||||
|
username: username,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Front-end web app concerns.
|
* Front-end web app concerns.
|
||||||
|
@ -689,27 +723,37 @@ const app = Vue.createApp({
|
||||||
// Camera is already open? Then disconnect the connection.
|
// Camera is already open? Then disconnect the connection.
|
||||||
if (this.WebRTC.pc[user.username] != undefined) {
|
if (this.WebRTC.pc[user.username] != undefined) {
|
||||||
// TODO: this breaks the connection both ways :(
|
// TODO: this breaks the connection both ways :(
|
||||||
// this.closeVideo(user.username);
|
this.closeVideo(user.username);
|
||||||
// return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
// We need to broadcast video to connect to another.
|
|
||||||
// TODO: because if the offerer doesn't add video tracks they
|
|
||||||
// won't request video support so the answerer's video isn't sent
|
|
||||||
if (!this.webcam.active) {
|
|
||||||
this.ChatServer("You will need to turn your own camera on first before you can connect to " + user.username + ".");
|
|
||||||
// return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.sendOpen(user.username);
|
this.sendOpen(user.username);
|
||||||
|
|
||||||
|
// Responsive CSS -> go to chat panel to see the camera
|
||||||
|
this.openChatPanel();
|
||||||
},
|
},
|
||||||
closeVideo(username) {
|
closeVideo(username) {
|
||||||
// A user has logged off the server. Clean up any WebRTC connections.
|
// A user has logged off the server. Clean up any WebRTC connections.
|
||||||
delete (this.WebRTC.streams[username]);
|
delete (this.WebRTC.streams[username]);
|
||||||
|
delete (this.webcam.watching[username]);
|
||||||
if (this.WebRTC.pc[username] != undefined) {
|
if (this.WebRTC.pc[username] != undefined) {
|
||||||
this.WebRTC.pc[username].close();
|
this.WebRTC.pc[username].close();
|
||||||
delete (this.WebRTC.pc[username]);
|
delete (this.WebRTC.pc[username]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Inform backend we have closed it.
|
||||||
|
this.sendWatch(username, false);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Show who watches our video.
|
||||||
|
showViewers() {
|
||||||
|
// TODO: for now, ChatClient is our bro.
|
||||||
|
let users = Object.keys(this.webcam.watching);
|
||||||
|
if (users.length === 0) {
|
||||||
|
this.ChatClient("There is currently nobody viewing your camera.");
|
||||||
|
} else {
|
||||||
|
this.ChatClient("Your current webcam viewers are:<br><br>" + users.join(", "));
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Stop broadcasting.
|
// Stop broadcasting.
|
||||||
|
@ -727,6 +771,29 @@ const app = Vue.createApp({
|
||||||
this.sendMe();
|
this.sendMe();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Mute my microphone if broadcasting.
|
||||||
|
muteMe() {
|
||||||
|
this.webcam.muted = !this.webcam.muted;
|
||||||
|
this.webcam.stream.getAudioTracks().forEach(track => {
|
||||||
|
console.error("Set audio track enabled=%s", !this.webcam.muted, track);
|
||||||
|
track.enabled = !this.webcam.muted;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
isMuted(username) {
|
||||||
|
return this.WebRTC.muted[username] === true;
|
||||||
|
},
|
||||||
|
muteVideo(username) {
|
||||||
|
this.WebRTC.muted[username] = !this.isMuted(username);
|
||||||
|
console.error("muteVideo(%s) is not muted=%s", username, this.WebRTC.muted[username]);
|
||||||
|
|
||||||
|
// Find the <video> tag to mute it.
|
||||||
|
let $ref = document.getElementById(`videofeed-${username}`);
|
||||||
|
console.log("Video elem:", $ref);
|
||||||
|
if ($ref) {
|
||||||
|
$ref.muted = this.WebRTC.muted[username];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
initHistory(channel) {
|
initHistory(channel) {
|
||||||
if (this.channels[channel] == undefined) {
|
if (this.channels[channel] == undefined) {
|
||||||
this.channels[channel] = {
|
this.channels[channel] = {
|
||||||
|
|
55
web/templates/about.html
Normal file
55
web/templates/about.html
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
{{define "index"}}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link rel="stylesheet" type="text/css" href="/static/css/bulma.min.css">
|
||||||
|
<link rel="stylesheet" type="text/css" href="/static/css/bulma-prefers-dark.css">
|
||||||
|
<link rel="stylesheet" href="/static/fontawesome-free-6.1.2-web/css/all.css">
|
||||||
|
<link rel="stylesheet" type="text/css" href="/static/css/chat.css?{{.CacheHash}}">
|
||||||
|
<title>About BareRTC</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="container is-fullhd">
|
||||||
|
<div class="content mt-5">
|
||||||
|
<h1>About BareRTC</h1>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
This chat room software is called <strong>BareRTC</strong> and this
|
||||||
|
page contains information about the software and how to use it.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
BareRTC is an open source project released under the GNU General Public
|
||||||
|
License with code available
|
||||||
|
<a href="https://git.kirsle.net/apps/BareRTC" target="_blank">here</a>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h1>About {{AsHTML .Config.Title}}</h1>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<strong>{{AsHTML .Config.Title}}</strong> is the name of this particular
|
||||||
|
BareRTC server. The administrator may have left some links to more
|
||||||
|
info below:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li><strong>Website:</strong>
|
||||||
|
<a href="{{.Config.WebsiteURL}}" target="_blank">{{.Config.WebsiteURL}}</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
This page will be fleshed out later with help and tips for using this
|
||||||
|
chat room.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
{{end}}
|
|
@ -5,6 +5,7 @@
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<link rel="stylesheet" type="text/css" href="/static/css/bulma.min.css">
|
<link rel="stylesheet" type="text/css" href="/static/css/bulma.min.css">
|
||||||
|
<link rel="stylesheet" type="text/css" href="/static/css/bulma-prefers-dark.css">
|
||||||
<link rel="stylesheet" href="/static/fontawesome-free-6.1.2-web/css/all.css">
|
<link rel="stylesheet" href="/static/fontawesome-free-6.1.2-web/css/all.css">
|
||||||
<link rel="stylesheet" type="text/css" href="/static/css/chat.css?{{.CacheHash}}">
|
<link rel="stylesheet" type="text/css" href="/static/css/chat.css?{{.CacheHash}}">
|
||||||
<title>BareRTC</title>
|
<title>BareRTC</title>
|
||||||
|
@ -46,6 +47,61 @@
|
||||||
|
|
||||||
<div class="chat-container">
|
<div class="chat-container">
|
||||||
|
|
||||||
|
<!-- Top header panel -->
|
||||||
|
<header class="chat-header">
|
||||||
|
<div class="columns is-mobile">
|
||||||
|
<div class="column is-narrow">
|
||||||
|
<strong class="is-6">{{AsHTML .Config.Title}}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="column">
|
||||||
|
<!-- Stop/Start video buttons -->
|
||||||
|
<button type="button"
|
||||||
|
v-if="webcam.active"
|
||||||
|
class="button is-small is-danger"
|
||||||
|
@click="stopVideo()">
|
||||||
|
<i class="fa fa-camera mr-2"></i>
|
||||||
|
Stop
|
||||||
|
</button>
|
||||||
|
<button type="button"
|
||||||
|
v-else
|
||||||
|
class="button is-small is-success"
|
||||||
|
@click="startVideo()"
|
||||||
|
:disabled="webcam.busy">
|
||||||
|
<i class="fa fa-camera mr-2"></i>
|
||||||
|
Start
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Mute/Unmute my mic buttons (if streaming)-->
|
||||||
|
<button type="button"
|
||||||
|
v-if="webcam.active && !webcam.muted"
|
||||||
|
class="button is-small is-danger is-outlined ml-2"
|
||||||
|
@click="muteMe()">
|
||||||
|
<i class="fa fa-microphone mr-2"></i>
|
||||||
|
Mute
|
||||||
|
</button>
|
||||||
|
<button type="button"
|
||||||
|
v-if="webcam.active && webcam.muted"
|
||||||
|
class="button is-small is-danger ml-2"
|
||||||
|
@click="muteMe()">
|
||||||
|
<i class="fa fa-microphone mr-2"></i>
|
||||||
|
Unmute
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Watchers button -->
|
||||||
|
<button type="button"
|
||||||
|
v-if="webcam.active"
|
||||||
|
class="button is-small is-info is-outlined ml-2"
|
||||||
|
@click="showViewers()">
|
||||||
|
<i class="fa fa-eye mr-2"></i>
|
||||||
|
[[Object.keys(webcam.watching).length]]
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="column is-narrow">
|
||||||
|
<a href="/about" target="_blank" class="button is-small is-link">About</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
<!-- Left Column: Channels & DMs -->
|
<!-- Left Column: Channels & DMs -->
|
||||||
<div class="left-column">
|
<div class="left-column">
|
||||||
<div class="card grid-card">
|
<div class="card grid-card">
|
||||||
|
@ -107,6 +163,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Middle Column: Chat Room/History -->
|
||||||
<div class="chat-column">
|
<div class="chat-column">
|
||||||
<div class="card grid-card">
|
<div class="card grid-card">
|
||||||
<header class="card-header has-background-link">
|
<header class="card-header has-background-link">
|
||||||
|
@ -154,18 +211,49 @@
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div class="video-feeds" v-show="webcam.active || Object.keys(WebRTC.streams).length > 0">
|
<div class="video-feeds" v-show="webcam.active || Object.keys(WebRTC.streams).length > 0">
|
||||||
|
<!-- Video Feeds-->
|
||||||
|
|
||||||
|
<!-- My video -->
|
||||||
<video class="feed"
|
<video class="feed"
|
||||||
v-show="webcam.active"
|
v-show="webcam.active"
|
||||||
id="localVideo"
|
id="localVideo"
|
||||||
autoplay muted>
|
autoplay muted>
|
||||||
x
|
|
||||||
</video>
|
</video>
|
||||||
|
|
||||||
|
<!-- Others' videos -->
|
||||||
|
<div class="feed" v-for="(stream, username) in WebRTC.streams" v-bind:key="username">
|
||||||
<video class="feed"
|
<video class="feed"
|
||||||
v-for="(stream, username) in WebRTC.streams"
|
|
||||||
v-bind:key="username"
|
|
||||||
:id="'videofeed-'+username"
|
:id="'videofeed-'+username"
|
||||||
autoplay muted>
|
autoplay>
|
||||||
</video>
|
</video>
|
||||||
|
<div class="caption">
|
||||||
|
[[username]]
|
||||||
|
</div>
|
||||||
|
<div class="close">
|
||||||
|
<a href="#"
|
||||||
|
class="has-text-danger"
|
||||||
|
title="Close video"
|
||||||
|
@click="closeVideo(username)">
|
||||||
|
<i class="fa fa-close"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="controls">
|
||||||
|
<button type="button"
|
||||||
|
v-if="isMuted(username)"
|
||||||
|
class="button is-small is-danger is-outlined p-1"
|
||||||
|
title="Unmute this video"
|
||||||
|
@click="muteVideo(username)">
|
||||||
|
<i class="fa fa-volume-xmark"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button"
|
||||||
|
v-else
|
||||||
|
class="button is-small is-danger is-outlined p-1"
|
||||||
|
title="Mute this video"
|
||||||
|
@click="muteVideo(username)">
|
||||||
|
<i class="fa fa-volume-high"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-content" id="chatHistory">
|
<div class="card-content" id="chatHistory">
|
||||||
|
|
||||||
|
@ -243,22 +331,7 @@
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-narrow">
|
<div class="column is-narrow">
|
||||||
<button type="button"
|
|
||||||
v-if="webcam.active"
|
|
||||||
class="button is-danger"
|
|
||||||
@click="stopVideo()">
|
|
||||||
<i class="fa fa-camera mr-2"></i>
|
|
||||||
Stop
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button type="button"
|
|
||||||
v-else
|
|
||||||
class="button is-success"
|
|
||||||
@click="startVideo()"
|
|
||||||
:disabled="webcam.busy">
|
|
||||||
<i class="fa fa-camera mr-2"></i>
|
|
||||||
Start
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user