Video Status Bitflags + Improvements
* Consolidate all the Video flags (active, nsfw, mutual, mutualOpen) into a bitmask flag (single integer) * New video flag for when the source has muted their video, to show a crossed out grey mic on their camera for other chatters * Bugfixes around syncing the mute state for self and other videos when videos are opened, closed and opened again * Profile pictures on the DMs list
This commit is contained in:
parent
3c1ad4ec6d
commit
2445d45d3f
24
Protocol.md
24
Protocol.md
|
@ -30,6 +30,22 @@ The video stream can be interrupted and closed via various methods:
|
||||||
* Also, a `who` update that says a person's videoActive went false will instruct all clients who had the video open, to close it (in case the PeerConnection closure didn't already do this).
|
* Also, a `who` update that says a person's videoActive went false will instruct all clients who had the video open, to close it (in case the PeerConnection closure didn't already do this).
|
||||||
* If a user exits the room, e.g. exited their browser abruptly without gracefully closing PeerConnections, any client who had their video open will close it immediately.
|
* If a user exits the room, e.g. exited their browser abruptly without gracefully closing PeerConnections, any client who had their video open will close it immediately.
|
||||||
|
|
||||||
|
# Video Flags
|
||||||
|
|
||||||
|
The various video settings sent on Who List updates are now consolidated
|
||||||
|
to a bit flag field:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
VideoFlag: {
|
||||||
|
Active: 1 << 0, // or 00000001 in binary
|
||||||
|
NSFW: 1 << 1, // or 00000010
|
||||||
|
Muted: 1 << 2, // or 00000100, etc.
|
||||||
|
IsTalking: 1 << 3,
|
||||||
|
MutualRequired: 1 << 4,
|
||||||
|
MutualOpen: 1 << 5,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
# WebSocket Message Actions
|
# WebSocket Message Actions
|
||||||
|
|
||||||
Every message has an "action" and may have other fields depending on the action type.
|
Every message has an "action" and may have other fields depending on the action type.
|
||||||
|
@ -201,8 +217,7 @@ The client sends "me" messages to send their webcam broadcast status and NSFW fl
|
||||||
// Client Me
|
// Client Me
|
||||||
{
|
{
|
||||||
"action": "me",
|
"action": "me",
|
||||||
"videoActive": true,
|
"video": 1,
|
||||||
"nsfw": false
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -213,7 +228,7 @@ The server may also push "me" messages to the user: for example if there is a co
|
||||||
{
|
{
|
||||||
"action": "me",
|
"action": "me",
|
||||||
"username": "soandso 12345",
|
"username": "soandso 12345",
|
||||||
"videoActive": true
|
"video": 1,
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -233,8 +248,7 @@ The `who` action sends the Who Is Online list to all connected chatters.
|
||||||
"op": false, // operator status
|
"op": false, // operator status
|
||||||
"avatar": "/picture/soandso.png",
|
"avatar": "/picture/soandso.png",
|
||||||
"profileURL": "/u/soandso",
|
"profileURL": "/u/soandso",
|
||||||
"videoActive": true,
|
"video": 0,
|
||||||
"nsfW": false
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,8 +49,8 @@ func (s *Server) Statistics() http.HandlerFunc {
|
||||||
unique[sub.Username] = struct{}{}
|
unique[sub.Username] = struct{}{}
|
||||||
|
|
||||||
// Count cameras by color.
|
// Count cameras by color.
|
||||||
if sub.VideoActive {
|
if sub.VideoStatus&VideoFlagActive == VideoFlagActive {
|
||||||
if sub.VideoNSFW {
|
if sub.VideoStatus&VideoFlagNSFW == VideoFlagNSFW {
|
||||||
result.Cameras.Red++
|
result.Cameras.Red++
|
||||||
} else {
|
} else {
|
||||||
result.Cameras.Blue++
|
result.Cameras.Blue++
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
package barertc
|
package barertc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.kirsle.net/apps/barertc/pkg/config"
|
||||||
"git.kirsle.net/apps/barertc/pkg/log"
|
"git.kirsle.net/apps/barertc/pkg/log"
|
||||||
"github.com/mattn/go-shellwords"
|
"github.com/mattn/go-shellwords"
|
||||||
)
|
)
|
||||||
|
@ -42,7 +45,7 @@ func (s *Server) ProcessCommand(sub *Subscriber, msg Message) bool {
|
||||||
sub.ChatServer("/nsfw: username not found: %s", username)
|
sub.ChatServer("/nsfw: username not found: %s", username)
|
||||||
} else {
|
} else {
|
||||||
other.ChatServer("Your camera has been marked as NSFW by %s", sub.Username)
|
other.ChatServer("Your camera has been marked as NSFW by %s", sub.Username)
|
||||||
other.VideoNSFW = true
|
other.VideoStatus |= VideoFlagNSFW
|
||||||
other.SendMe()
|
other.SendMe()
|
||||||
s.SendWhoList()
|
s.SendWhoList()
|
||||||
sub.ChatServer("%s has their camera marked as NSFW", username)
|
sub.ChatServer("%s has their camera marked as NSFW", username)
|
||||||
|
@ -52,11 +55,23 @@ func (s *Server) ProcessCommand(sub *Subscriber, msg Message) bool {
|
||||||
sub.ChatServer(RenderMarkdown("Moderator commands are:\n\n" +
|
sub.ChatServer(RenderMarkdown("Moderator commands are:\n\n" +
|
||||||
"* `/kick <username>` to kick from chat\n" +
|
"* `/kick <username>` to kick from chat\n" +
|
||||||
"* `/nsfw <username>` to mark their camera NSFW\n" +
|
"* `/nsfw <username>` to mark their camera NSFW\n" +
|
||||||
|
"* `/shutdown` to gracefully shut down (reboot) the chat server\n" +
|
||||||
|
"* `/kickall` to kick EVERYBODY off and force them to log back in\n" +
|
||||||
"* `/help` to show this message\n\n" +
|
"* `/help` to show this message\n\n" +
|
||||||
"Note: shell-style quoting is supported, if a username has a space in it, quote the whole username, e.g.: `/kick \"username 2\"`",
|
"Note: shell-style quoting is supported, if a username has a space in it, quote the whole username, e.g.: `/kick \"username 2\"`",
|
||||||
))
|
))
|
||||||
return true
|
return true
|
||||||
|
case "/shutdown":
|
||||||
|
s.Broadcast(Message{
|
||||||
|
Action: ActionError,
|
||||||
|
Username: "ChatServer",
|
||||||
|
Message: "The chat server is going down for a reboot NOW!",
|
||||||
|
})
|
||||||
|
os.Exit(1)
|
||||||
|
case "/kickall":
|
||||||
|
s.KickAllCommand()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Not handled.
|
// Not handled.
|
||||||
|
@ -87,6 +102,48 @@ func (s *Server) KickCommand(words []string, sub *Subscriber) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// KickAllCommand kicks everybody out of the room.
|
||||||
|
func (s *Server) KickAllCommand() {
|
||||||
|
|
||||||
|
// If we have JWT enabled and a landing page, link users to it.
|
||||||
|
if config.Current.JWT.Enabled && config.Current.JWT.LandingPageURL != "" {
|
||||||
|
s.Broadcast(Message{
|
||||||
|
Action: ActionError,
|
||||||
|
Username: "ChatServer",
|
||||||
|
Message: fmt.Sprintf(
|
||||||
|
"<strong>Notice:</strong> The chat operator has requested that you log back in to the chat room. "+
|
||||||
|
"Probably, this is because a new feature was launched that needs you to reload the page. "+
|
||||||
|
"You may refresh the tab or <a href=\"%s\">click here</a> to re-enter the room.",
|
||||||
|
config.Current.JWT.LandingPageURL,
|
||||||
|
),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
s.Broadcast(Message{
|
||||||
|
Action: ActionError,
|
||||||
|
Username: "ChatServer",
|
||||||
|
Message: "<strong>Notice:</strong> The chat operator has kicked everybody from the room. Usually, this " +
|
||||||
|
"may mean a new feature of the chat has been launched and you need to reload the page for it " +
|
||||||
|
"to function correctly.",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kick everyone off.
|
||||||
|
s.Broadcast(Message{
|
||||||
|
Action: ActionKick,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Disconnect everybody.
|
||||||
|
s.subscribersMu.RLock()
|
||||||
|
defer s.subscribersMu.RUnlock()
|
||||||
|
for _, sub := range s.IterSubscribers(true) {
|
||||||
|
if !sub.authenticated {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
s.DeleteSubscriber(sub)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// BanCommand handles the `/ban` operator command.
|
// BanCommand handles the `/ban` operator command.
|
||||||
func (s *Server) BanCommand(words []string, sub *Subscriber) {
|
func (s *Server) BanCommand(words []string, sub *Subscriber) {
|
||||||
if len(words) == 1 {
|
if len(words) == 1 {
|
||||||
|
|
|
@ -12,16 +12,17 @@ import (
|
||||||
|
|
||||||
// Version of the config format - when new fields are added, it will attempt
|
// Version of the config format - when new fields are added, it will attempt
|
||||||
// to write the settings.toml to disk so new defaults populate.
|
// to write the settings.toml to disk so new defaults populate.
|
||||||
var currentVersion = 3
|
var currentVersion = 4
|
||||||
|
|
||||||
// Config for your BareRTC app.
|
// Config for your BareRTC app.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Version int // will re-save your settings.toml on migrations
|
Version int // will re-save your settings.toml on migrations
|
||||||
|
|
||||||
JWT struct {
|
JWT struct {
|
||||||
Enabled bool
|
Enabled bool
|
||||||
Strict bool
|
Strict bool
|
||||||
SecretKey string
|
SecretKey string
|
||||||
|
LandingPageURL string
|
||||||
}
|
}
|
||||||
|
|
||||||
Title string
|
Title string
|
||||||
|
|
|
@ -252,7 +252,7 @@ func (s *Server) OnFile(sub *Subscriber, msg Message) {
|
||||||
|
|
||||||
// OnMe handles current user state updates.
|
// OnMe handles current user state updates.
|
||||||
func (s *Server) OnMe(sub *Subscriber, msg Message) {
|
func (s *Server) OnMe(sub *Subscriber, msg Message) {
|
||||||
if msg.VideoActive {
|
if msg.VideoStatus&VideoFlagActive == VideoFlagActive {
|
||||||
log.Debug("User %s turns on their video feed", sub.Username)
|
log.Debug("User %s turns on their video feed", sub.Username)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -278,10 +278,7 @@ func (s *Server) OnMe(sub *Subscriber, msg Message) {
|
||||||
msg.ChatStatus = "away"
|
msg.ChatStatus = "away"
|
||||||
}
|
}
|
||||||
|
|
||||||
sub.VideoActive = msg.VideoActive
|
sub.VideoStatus = msg.VideoStatus
|
||||||
sub.VideoMutual = msg.VideoMutual
|
|
||||||
sub.VideoMutualOpen = msg.VideoMutualOpen
|
|
||||||
sub.VideoNSFW = msg.NSFW
|
|
||||||
sub.ChatStatus = msg.ChatStatus
|
sub.ChatStatus = msg.ChatStatus
|
||||||
|
|
||||||
// Sync the WhoList to everybody.
|
// Sync the WhoList to everybody.
|
||||||
|
|
|
@ -23,11 +23,8 @@ type Message struct {
|
||||||
WhoList []WhoList `json:"whoList,omitempty"`
|
WhoList []WhoList `json:"whoList,omitempty"`
|
||||||
|
|
||||||
// Sent on `me` actions along with Username
|
// Sent on `me` actions along with Username
|
||||||
VideoActive bool `json:"videoActive,omitempty"` // user tells us their cam status
|
VideoStatus int `json:"video,omitempty"` // user video flags
|
||||||
VideoMutual bool `json:"videoMutual,omitempty"` // user wants mutual viewers
|
ChatStatus string `json:"status,omitempty"` // online vs. away
|
||||||
VideoMutualOpen bool `json:"videoMutualOpen,omitempty"`
|
|
||||||
ChatStatus string `json:"status,omitempty"` // online vs. away
|
|
||||||
NSFW bool `json:"nsfw,omitempty"` // user tags their video NSFW
|
|
||||||
|
|
||||||
// Message ID to support takebacks/local deletions
|
// Message ID to support takebacks/local deletions
|
||||||
MessageID int `json:"msgID,omitempty"`
|
MessageID int `json:"msgID,omitempty"`
|
||||||
|
@ -75,16 +72,24 @@ const (
|
||||||
|
|
||||||
// WhoList is a member entry in the chat room.
|
// WhoList is a member entry in the chat room.
|
||||||
type WhoList struct {
|
type WhoList struct {
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
Nickname string `json:"nickname,omitempty"`
|
Nickname string `json:"nickname,omitempty"`
|
||||||
VideoActive bool `json:"videoActive,omitempty"`
|
Status string `json:"status"`
|
||||||
VideoMutual bool `json:"videoMutual,omitempty"`
|
Video int `json:"video"`
|
||||||
VideoMutualOpen bool `json:"videoMutualOpen,omitempty"`
|
|
||||||
NSFW bool `json:"nsfw,omitempty"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
|
|
||||||
// JWT auth extra settings.
|
// JWT auth extra settings.
|
||||||
Operator bool `json:"op"`
|
Operator bool `json:"op"`
|
||||||
Avatar string `json:"avatar,omitempty"`
|
Avatar string `json:"avatar,omitempty"`
|
||||||
ProfileURL string `json:"profileURL,omitempty"`
|
ProfileURL string `json:"profileURL,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// VideoFlags to convey the state and setting of users' cameras concisely.
|
||||||
|
// Also see the VideoFlag object in BareRTC.js for front-end sync.
|
||||||
|
const (
|
||||||
|
VideoFlagActive int = 1 << iota // user's camera is enabled/broadcasting
|
||||||
|
VideoFlagNSFW // viewer's camera is marked as NSFW
|
||||||
|
VideoFlagMuted // user source microphone is muted
|
||||||
|
VideoFlagIsTalking // broadcaster seems to be talking
|
||||||
|
VideoFlagMutualRequired // video wants viewers to share their camera too
|
||||||
|
VideoFlagMutualOpen // viewer wants to auto-open viewers' cameras
|
||||||
|
)
|
||||||
|
|
|
@ -40,9 +40,16 @@ func IndexPage() http.HandlerFunc {
|
||||||
|
|
||||||
// Are we enforcing strict JWT authentication?
|
// Are we enforcing strict JWT authentication?
|
||||||
if config.Current.JWT.Enabled && config.Current.JWT.Strict && !authOK {
|
if config.Current.JWT.Enabled && config.Current.JWT.Strict && !authOK {
|
||||||
|
// Do we have a landing page to redirect to?
|
||||||
|
if config.Current.JWT.LandingPageURL != "" {
|
||||||
|
w.Header().Add("Location", config.Current.JWT.LandingPageURL)
|
||||||
|
w.WriteHeader(http.StatusFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
w.WriteHeader(http.StatusForbidden)
|
w.WriteHeader(http.StatusForbidden)
|
||||||
w.Write([]byte(
|
w.Write([]byte(
|
||||||
fmt.Sprintf("Authentication denied. Please go back and try again."),
|
"Authentication denied. Please go back and try again.",
|
||||||
))
|
))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,20 +21,17 @@ import (
|
||||||
// Subscriber represents a connected WebSocket session.
|
// Subscriber represents a connected WebSocket session.
|
||||||
type Subscriber struct {
|
type Subscriber struct {
|
||||||
// User properties
|
// User properties
|
||||||
ID int // ID assigned by server
|
ID int // ID assigned by server
|
||||||
Username string
|
Username string
|
||||||
VideoActive bool
|
ChatStatus string
|
||||||
VideoMutual bool
|
VideoStatus int
|
||||||
VideoMutualOpen bool
|
JWTClaims *jwt.Claims
|
||||||
VideoNSFW bool
|
authenticated bool // has passed the login step
|
||||||
ChatStatus string
|
conn *websocket.Conn
|
||||||
JWTClaims *jwt.Claims
|
ctx context.Context
|
||||||
authenticated bool // has passed the login step
|
cancel context.CancelFunc
|
||||||
conn *websocket.Conn
|
messages chan []byte
|
||||||
ctx context.Context
|
closeSlow func()
|
||||||
cancel context.CancelFunc
|
|
||||||
messages chan []byte
|
|
||||||
closeSlow func()
|
|
||||||
|
|
||||||
muteMu sync.RWMutex
|
muteMu sync.RWMutex
|
||||||
booted map[string]struct{} // usernames booted off your camera
|
booted map[string]struct{} // usernames booted off your camera
|
||||||
|
@ -130,8 +127,7 @@ func (sub *Subscriber) SendMe() {
|
||||||
sub.SendJSON(Message{
|
sub.SendJSON(Message{
|
||||||
Action: ActionMe,
|
Action: ActionMe,
|
||||||
Username: sub.Username,
|
Username: sub.Username,
|
||||||
VideoActive: sub.VideoActive,
|
VideoStatus: sub.VideoStatus,
|
||||||
NSFW: sub.VideoNSFW,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -384,18 +380,14 @@ func (s *Server) SendWhoList() {
|
||||||
}
|
}
|
||||||
|
|
||||||
who := WhoList{
|
who := WhoList{
|
||||||
Username: user.Username,
|
Username: user.Username,
|
||||||
Status: user.ChatStatus,
|
Status: user.ChatStatus,
|
||||||
VideoActive: user.VideoActive,
|
Video: user.VideoStatus,
|
||||||
VideoMutual: user.VideoMutual,
|
|
||||||
VideoMutualOpen: user.VideoMutualOpen,
|
|
||||||
NSFW: user.VideoNSFW,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If this person had booted us, force their camera to "off"
|
// If this person had booted us, force their camera to "off"
|
||||||
if user.Boots(sub.Username) || user.Mutes(sub.Username) {
|
if user.Boots(sub.Username) || user.Mutes(sub.Username) {
|
||||||
who.VideoActive = false
|
who.Video = 0
|
||||||
who.NSFW = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if user.JWTClaims != nil {
|
if user.JWTClaims != nil {
|
||||||
|
|
|
@ -127,6 +127,16 @@ const app = Vue.createApp({
|
||||||
audioDeviceID: null,
|
audioDeviceID: null,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Video flag constants (sync with values in messages.go)
|
||||||
|
VideoFlag: {
|
||||||
|
Active: 1 << 0,
|
||||||
|
NSFW: 1 << 1,
|
||||||
|
Muted: 1 << 2,
|
||||||
|
IsTalking: 1 << 3,
|
||||||
|
MutualRequired: 1 << 4,
|
||||||
|
MutualOpen: 1 << 5,
|
||||||
|
},
|
||||||
|
|
||||||
// WebRTC sessions with other users.
|
// WebRTC sessions with other users.
|
||||||
WebRTC: {
|
WebRTC: {
|
||||||
// Streams per username.
|
// Streams per username.
|
||||||
|
@ -315,6 +325,16 @@ const app = Vue.createApp({
|
||||||
// Returns if the current user has operator rights
|
// Returns if the current user has operator rights
|
||||||
return this.jwt.claims.op;
|
return this.jwt.claims.op;
|
||||||
},
|
},
|
||||||
|
myVideoFlag() {
|
||||||
|
// Compute the current user's video status flags.
|
||||||
|
let status = 0;
|
||||||
|
if (this.webcam.active) status |= this.VideoFlag.Active;
|
||||||
|
if (this.webcam.muted) status |= this.VideoFlag.Muted;
|
||||||
|
if (this.webcam.nsfw) status |= this.VideoFlag.NSFW;
|
||||||
|
if (this.webcam.mutual) status |= this.VideoFlag.MutualRequired;
|
||||||
|
if (this.webcam.mutualOpen) status |= this.VideoFlag.MutualOpen;
|
||||||
|
return status;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
// Load user prefs from localStorage, called on startup
|
// Load user prefs from localStorage, called on startup
|
||||||
|
@ -375,11 +395,8 @@ const app = Vue.createApp({
|
||||||
sendMe() {
|
sendMe() {
|
||||||
this.ws.conn.send(JSON.stringify({
|
this.ws.conn.send(JSON.stringify({
|
||||||
action: "me",
|
action: "me",
|
||||||
videoActive: this.webcam.active,
|
video: this.myVideoFlag,
|
||||||
videoMutual: this.webcam.mutual,
|
|
||||||
videoMutualOpen: this.webcam.mutualOpen,
|
|
||||||
status: this.status,
|
status: this.status,
|
||||||
nsfw: this.webcam.nsfw,
|
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
onMe(msg) {
|
onMe(msg) {
|
||||||
|
@ -391,11 +408,11 @@ const app = Vue.createApp({
|
||||||
}
|
}
|
||||||
|
|
||||||
// The server can set our webcam NSFW flag.
|
// The server can set our webcam NSFW flag.
|
||||||
if (this.webcam.nsfw != msg.nsfw) {
|
let myNSFW = this.webcam.nsfw;
|
||||||
this.webcam.nsfw = msg.nsfw;
|
let theirNSFW = (msg.video & this.VideoFlag.NSFW) > 0;
|
||||||
|
if (myNSFW != theirNSFW) {
|
||||||
|
this.webcam.nsfw = theirNSFW;
|
||||||
}
|
}
|
||||||
|
|
||||||
// this.ChatClient(`User sync from backend: ${JSON.stringify(msg)}`);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// WhoList updates.
|
// WhoList updates.
|
||||||
|
@ -412,14 +429,14 @@ const app = Vue.createApp({
|
||||||
for (let row of this.whoList) {
|
for (let row of this.whoList) {
|
||||||
this.whoMap[row.username] = row;
|
this.whoMap[row.username] = row;
|
||||||
if (this.WebRTC.streams[row.username] != undefined &&
|
if (this.WebRTC.streams[row.username] != undefined &&
|
||||||
row.videoActive !== true) {
|
!(row.video & this.VideoFlag.Active)) {
|
||||||
this.closeVideo(row.username, "offerer");
|
this.closeVideo(row.username, "offerer");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Has the back-end server forgotten we are on video? This can
|
// Has the back-end server forgotten we are on video? This can
|
||||||
// happen if we disconnect/reconnect while we were streaming.
|
// happen if we disconnect/reconnect while we were streaming.
|
||||||
if (this.webcam.active && !this.whoMap[this.username]?.videoActive) {
|
if (this.webcam.active && !(this.whoMap[this.username]?.video & this.VideoFlag.Active)) {
|
||||||
this.sendMe();
|
this.sendMe();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -473,18 +490,10 @@ const app = Vue.createApp({
|
||||||
},
|
},
|
||||||
onOpen(msg) {
|
onOpen(msg) {
|
||||||
// Response for the opener to begin WebRTC connection.
|
// Response for the opener to begin WebRTC connection.
|
||||||
const secret = msg.openSecret;
|
|
||||||
// console.log("OPEN: connect to %s with secret %s", msg.username, secret);
|
|
||||||
// this.ChatClient(`onOpen called for ${msg.username}.`);
|
|
||||||
|
|
||||||
this.startWebRTC(msg.username, true);
|
this.startWebRTC(msg.username, true);
|
||||||
},
|
},
|
||||||
onRing(msg) {
|
onRing(msg) {
|
||||||
// Message for the receiver to begin WebRTC connection.
|
|
||||||
const secret = msg.openSecret;
|
|
||||||
// console.log("RING: connection from %s with secret %s", msg.username, secret);
|
|
||||||
this.ChatServer(`${msg.username} has opened your camera.`);
|
this.ChatServer(`${msg.username} has opened your camera.`);
|
||||||
|
|
||||||
this.startWebRTC(msg.username, false);
|
this.startWebRTC(msg.username, false);
|
||||||
},
|
},
|
||||||
onUserExited(msg) {
|
onUserExited(msg) {
|
||||||
|
@ -768,7 +777,7 @@ const app = Vue.createApp({
|
||||||
|
|
||||||
// If we are the offerer, and this member wants to auto-open our camera
|
// If we are the offerer, and this member wants to auto-open our camera
|
||||||
// then add our own stream to the connection.
|
// then add our own stream to the connection.
|
||||||
if (isOfferer && this.whoMap[username].videoMutualOpen && this.webcam.active) {
|
if (isOfferer && (this.whoMap[username].video & this.VideoFlag.MutualOpen) && this.webcam.active) {
|
||||||
let stream = this.webcam.stream;
|
let stream = this.webcam.stream;
|
||||||
stream.getTracks().forEach(track => {
|
stream.getTracks().forEach(track => {
|
||||||
pc.addTrack(track, stream)
|
pc.addTrack(track, stream)
|
||||||
|
@ -1113,7 +1122,7 @@ const app = Vue.createApp({
|
||||||
|
|
||||||
// Is the target user NSFW? Go thru the modal.
|
// Is the target user NSFW? Go thru the modal.
|
||||||
let dontShowAgain = localStorage["skip-nsfw-modal"] == "true";
|
let dontShowAgain = localStorage["skip-nsfw-modal"] == "true";
|
||||||
if (user.nsfw && !dontShowAgain && !force) {
|
if ((user.video & this.VideoFlag.NSFW) && !dontShowAgain && !force) {
|
||||||
this.nsfwModalView.user = user;
|
this.nsfwModalView.user = user;
|
||||||
this.nsfwModalView.visible = true;
|
this.nsfwModalView.visible = true;
|
||||||
return;
|
return;
|
||||||
|
@ -1130,7 +1139,7 @@ const app = Vue.createApp({
|
||||||
}
|
}
|
||||||
|
|
||||||
// If this user requests mutual viewership...
|
// If this user requests mutual viewership...
|
||||||
if (user.videoMutual && !this.webcam.active) {
|
if ((user.video & this.VideoFlag.MutualRequired) && !this.webcam.active) {
|
||||||
this.ChatClient(
|
this.ChatClient(
|
||||||
`<strong>${user.username}</strong> has requested that you should share your own camera too before opening theirs.`
|
`<strong>${user.username}</strong> has requested that you should share your own camera too before opening theirs.`
|
||||||
);
|
);
|
||||||
|
@ -1155,6 +1164,8 @@ const app = Vue.createApp({
|
||||||
if (name === "offerer") {
|
if (name === "offerer") {
|
||||||
// We are closing another user's video stream.
|
// We are closing another user's video stream.
|
||||||
delete (this.WebRTC.streams[username]);
|
delete (this.WebRTC.streams[username]);
|
||||||
|
delete (this.WebRTC.muted[username]);
|
||||||
|
delete (this.WebRTC.poppedOut[username]);
|
||||||
if (this.WebRTC.pc[username] != undefined && this.WebRTC.pc[username].offerer != undefined) {
|
if (this.WebRTC.pc[username] != undefined && this.WebRTC.pc[username].offerer != undefined) {
|
||||||
this.WebRTC.pc[username].offerer.close();
|
this.WebRTC.pc[username].offerer.close();
|
||||||
delete (this.WebRTC.pc[username]);
|
delete (this.WebRTC.pc[username]);
|
||||||
|
@ -1183,6 +1194,8 @@ const app = Vue.createApp({
|
||||||
this.WebRTC.pc[username].answerer.close();
|
this.WebRTC.pc[username].answerer.close();
|
||||||
}
|
}
|
||||||
delete (this.WebRTC.pc[username]);
|
delete (this.WebRTC.pc[username]);
|
||||||
|
delete (this.WebRTC.muted[username]);
|
||||||
|
delete (this.WebRTC.poppedOut[username]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inform backend we have closed it.
|
// Inform backend we have closed it.
|
||||||
|
@ -1193,16 +1206,33 @@ const app = Vue.createApp({
|
||||||
// and then we turn ours off: we should unfollow the ones with mutual video.
|
// and then we turn ours off: we should unfollow the ones with mutual video.
|
||||||
for (let row of this.whoList) {
|
for (let row of this.whoList) {
|
||||||
let username = row.username;
|
let username = row.username;
|
||||||
if (row.videoMutual && this.WebRTC.pc[username] != undefined) {
|
if ((row.video & this.VideoFlag.MutualRequired) && this.WebRTC.pc[username] != undefined) {
|
||||||
this.closeVideo(username);
|
this.closeVideo(username);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
webcamIconClass(user) {
|
||||||
|
// Return the icon to show on a video button.
|
||||||
|
// - Usually a video icon
|
||||||
|
// - May be a crossed-out video if isVideoNotAllowed
|
||||||
|
// - Or an eyeball for cameras already opened
|
||||||
|
if (user.username === this.username && this.webcam.active) {
|
||||||
|
return 'fa-eye'; // user sees their own self camera always
|
||||||
|
}
|
||||||
|
|
||||||
|
// Already opened?
|
||||||
|
if (this.WebRTC.pc[user.username] != undefined && this.WebRTC.streams[user.username] != undefined) {
|
||||||
|
return 'fa-eye';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isVideoNotAllowed(user)) return 'fa-video-slash';
|
||||||
|
return 'fa-video';
|
||||||
|
},
|
||||||
isVideoNotAllowed(user) {
|
isVideoNotAllowed(user) {
|
||||||
// Returns whether the video button to open a user's cam will be not allowed (crossed out)
|
// Returns whether the video button to open a user's cam will be not allowed (crossed out)
|
||||||
|
|
||||||
// Mutual video sharing is required on this camera, and ours is not active
|
// Mutual video sharing is required on this camera, and ours is not active
|
||||||
if (user.videoActive && user.videoMutual && !this.webcam.active) {
|
if ((user.video & this.VideoFlag.Active) && (user.video & this.VideoFlag.MutualRequired) && !this.webcam.active) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1263,6 +1293,7 @@ const app = Vue.createApp({
|
||||||
this.webcam.elem.srcObject = null;
|
this.webcam.elem.srcObject = null;
|
||||||
this.webcam.stream = null;
|
this.webcam.stream = null;
|
||||||
this.webcam.active = false;
|
this.webcam.active = false;
|
||||||
|
this.webcam.muted = false;
|
||||||
this.whoTab = "online";
|
this.whoTab = "online";
|
||||||
|
|
||||||
// Close all WebRTC sessions.
|
// Close all WebRTC sessions.
|
||||||
|
@ -1283,6 +1314,16 @@ const app = Vue.createApp({
|
||||||
this.webcam.stream.getAudioTracks().forEach(track => {
|
this.webcam.stream.getAudioTracks().forEach(track => {
|
||||||
track.enabled = !this.webcam.muted;
|
track.enabled = !this.webcam.muted;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Communicate our local mute to others.
|
||||||
|
this.sendMe();
|
||||||
|
},
|
||||||
|
isSourceMuted(username) {
|
||||||
|
// See if the webcam broadcaster muted their mic at the source
|
||||||
|
if (this.whoMap[username] != undefined && this.whoMap[username].video & this.VideoFlag.Muted) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
},
|
},
|
||||||
isMuted(username) {
|
isMuted(username) {
|
||||||
return this.WebRTC.muted[username] === true;
|
return this.WebRTC.muted[username] === true;
|
||||||
|
|
|
@ -544,12 +544,26 @@
|
||||||
<a :href="'#'+c.channel"
|
<a :href="'#'+c.channel"
|
||||||
@click.prevent="setChannel(c.channel)"
|
@click.prevent="setChannel(c.channel)"
|
||||||
:class="{'is-active': c.channel == channel}">
|
:class="{'is-active': c.channel == channel}">
|
||||||
[[c.name]]
|
|
||||||
|
|
||||||
<span v-if="hasUnread(c.channel)"
|
<div class="columns is-mobile">
|
||||||
class="tag is-danger">
|
<!-- Avatar URL if available (copied from Who List) -->
|
||||||
[[hasUnread(c.channel)]]
|
<div class="column is-narrow pr-0" style="position: relative">
|
||||||
</span>
|
<img v-if="avatarForUsername(normalizeUsername(c.channel))" :src="avatarForUsername(normalizeUsername(c.channel))"
|
||||||
|
width="24" height="24" alt="">
|
||||||
|
<img v-else src="/static/img/shy.png"
|
||||||
|
width="24" height="24">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="column">
|
||||||
|
[[c.name]]
|
||||||
|
|
||||||
|
<span v-if="hasUnread(c.channel)"
|
||||||
|
class="tag is-danger">
|
||||||
|
[[hasUnread(c.channel)]]
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -621,6 +635,8 @@
|
||||||
</video>
|
</video>
|
||||||
|
|
||||||
<div class="caption">
|
<div class="caption">
|
||||||
|
<i class="fa fa-microphone-slash mr-1 has-text-grey"
|
||||||
|
v-if="webcam.muted"></i>
|
||||||
[[username]]
|
[[username]]
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -659,6 +675,8 @@
|
||||||
autoplay>
|
autoplay>
|
||||||
</video>
|
</video>
|
||||||
<div class="caption">
|
<div class="caption">
|
||||||
|
<i class="fa fa-microphone-slash mr-1 has-text-grey"
|
||||||
|
v-if="isSourceMuted(username)"></i>
|
||||||
[[username]]
|
[[username]]
|
||||||
</div>
|
</div>
|
||||||
<div class="close">
|
<div class="close">
|
||||||
|
@ -1022,19 +1040,19 @@
|
||||||
|
|
||||||
<!-- Video button -->
|
<!-- Video button -->
|
||||||
<button type="button" class="button is-small px-2 py-1"
|
<button type="button" class="button is-small px-2 py-1"
|
||||||
:disabled="!u.videoActive"
|
:disabled="!(u.video & VideoFlag.Active)"
|
||||||
:class="{
|
:class="{
|
||||||
'is-danger is-outlined': u.videoActive && u.nsfw,
|
'is-danger is-outlined': (u.video & VideoFlag.Active) && (u.video & VideoFlag.NSFW),
|
||||||
'is-info is-outlined': u.videoActive && !u.nsfw,
|
'is-info is-outlined': (u.video & VideoFlag.Active) && !(u.video & VideoFlag.NSFW),
|
||||||
'cursor-notallowed': isVideoNotAllowed(u),
|
'cursor-notallowed': isVideoNotAllowed(u),
|
||||||
}"
|
}"
|
||||||
:title="`Open video stream` +
|
:title="`Open video stream` +
|
||||||
(u.videoActive && u.videoMutual ? '; mutual video sharing required' : '') +
|
(u.video & VideoFlag.MutualRequired ? '; mutual video sharing required' : '') +
|
||||||
(u.videoActive && u.videoMutualOpen ? '; will auto-open your video' : '')"
|
(u.video & VideoFlag.MutualOpen ? '; will auto-open your video' : '')"
|
||||||
|
|
||||||
@click="openVideo(u)">
|
@click="openVideo(u)">
|
||||||
<i class="fa"
|
<i class="fa"
|
||||||
:class="isVideoNotAllowed(u) ? 'fa-video-slash' : 'fa-video'"></i>
|
:class="webcamIconClass(u)"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user