diff --git a/pkg/handlers.go b/pkg/handlers.go index 0cc0483..5a47a9c 100644 --- a/pkg/handlers.go +++ b/pkg/handlers.go @@ -261,6 +261,8 @@ func (s *Server) OnMe(sub *Subscriber, msg Message) { } sub.VideoActive = msg.VideoActive + sub.VideoMutual = msg.VideoMutual + sub.VideoMutualOpen = msg.VideoMutualOpen sub.VideoNSFW = msg.NSFW sub.ChatStatus = msg.ChatStatus diff --git a/pkg/messages.go b/pkg/messages.go index 98fc1b5..f3bb736 100644 --- a/pkg/messages.go +++ b/pkg/messages.go @@ -20,9 +20,11 @@ type Message struct { WhoList []WhoList `json:"whoList,omitempty"` // Sent on `me` actions along with Username - VideoActive bool `json:"videoActive,omitempty"` // user tells us their cam status - ChatStatus string `json:"status,omitempty"` // online vs. away - NSFW bool `json:"nsfw,omitempty"` // user tags their video NSFW + VideoActive bool `json:"videoActive,omitempty"` // user tells us their cam status + VideoMutual bool `json:"videoMutual,omitempty"` // user wants mutual viewers + VideoMutualOpen bool `json:"videoMutualOpen,omitempty"` + ChatStatus string `json:"status,omitempty"` // online vs. away + NSFW bool `json:"nsfw,omitempty"` // user tags their video NSFW // Sent on `open` actions along with the (other) Username. OpenSecret string `json:"openSecret,omitempty"` @@ -66,10 +68,12 @@ const ( // WhoList is a member entry in the chat room. type WhoList struct { - Username string `json:"username"` - VideoActive bool `json:"videoActive,omitempty"` - NSFW bool `json:"nsfw,omitempty"` - Status string `json:"status"` + Username string `json:"username"` + VideoActive bool `json:"videoActive,omitempty"` + VideoMutual bool `json:"videoMutual,omitempty"` + VideoMutualOpen bool `json:"videoMutualOpen,omitempty"` + NSFW bool `json:"nsfw,omitempty"` + Status string `json:"status"` // JWT auth extra settings. Operator bool `json:"op"` diff --git a/pkg/websocket.go b/pkg/websocket.go index 2b357da..f9ca931 100644 --- a/pkg/websocket.go +++ b/pkg/websocket.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "net/http" + "sort" "strings" "sync" "time" @@ -20,18 +21,20 @@ import ( // Subscriber represents a connected WebSocket session. type Subscriber struct { // User properties - ID int // ID assigned by server - Username string - VideoActive bool - VideoNSFW bool - ChatStatus string - JWTClaims *jwt.Claims - authenticated bool // has passed the login step - conn *websocket.Conn - ctx context.Context - cancel context.CancelFunc - messages chan []byte - closeSlow func() + ID int // ID assigned by server + Username string + VideoActive bool + VideoMutual bool + VideoMutualOpen bool + VideoNSFW bool + ChatStatus string + JWTClaims *jwt.Claims + authenticated bool // has passed the login step + conn *websocket.Conn + ctx context.Context + cancel context.CancelFunc + messages chan []byte + closeSlow func() muteMu sync.RWMutex booted map[string]struct{} // usernames booted off your camera @@ -308,8 +311,16 @@ func (s *Server) SendTo(username string, msg Message) error { func (s *Server) SendWhoList() { var ( subscribers = s.IterSubscribers() + usernames = []string{} // distinct and sorted usernames + userSub = map[string]*Subscriber{} ) + for _, sub := range subscribers { + usernames = append(usernames, sub.Username) + userSub[sub.Username] = sub + } + sort.Strings(usernames) + // Build the WhoList for each subscriber. // TODO: it's the only way to fake videoActive for booted user views. for _, sub := range subscribers { @@ -318,16 +329,19 @@ func (s *Server) SendWhoList() { } var users = []WhoList{} - for _, user := range subscribers { + for _, un := range usernames { + user := userSub[un] if user.ChatStatus == "hidden" { continue } who := WhoList{ - Username: user.Username, - Status: user.ChatStatus, - VideoActive: user.VideoActive, - NSFW: user.VideoNSFW, + Username: user.Username, + Status: user.ChatStatus, + VideoActive: user.VideoActive, + VideoMutual: user.VideoMutual, + VideoMutualOpen: user.VideoMutualOpen, + NSFW: user.VideoNSFW, } // If this person had booted us, force their camera to "off" diff --git a/web/static/css/chat.css b/web/static/css/chat.css index 77ba40b..d19d46e 100644 --- a/web/static/css/chat.css +++ b/web/static/css/chat.css @@ -249,4 +249,9 @@ body { position: absolute; top: 14px; left: 16px; +} + +/* Cursors */ +.cursor-notallowed { + cursor: not-allowed; } \ No newline at end of file diff --git a/web/static/js/BareRTC.js b/web/static/js/BareRTC.js index a948529..97f7777 100644 --- a/web/static/js/BareRTC.js +++ b/web/static/js/BareRTC.js @@ -91,6 +91,8 @@ const app = Vue.createApp({ stream: null, // MediaStream object muted: false, // our outgoing mic is muted, not by default nsfw: false, // user has flagged their camera to be NSFW + mutual: false, // user wants viewers to share their own videos + mutualOpen: false, // user wants to open video mutually // Who all is watching me? map of users. watching: {}, @@ -331,6 +333,8 @@ const app = Vue.createApp({ this.ws.conn.send(JSON.stringify({ action: "me", videoActive: this.webcam.active, + videoMutual: this.webcam.mutual, + videoMutualOpen: this.webcam.mutualOpen, status: this.status, nsfw: this.webcam.nsfw, })); @@ -682,6 +686,15 @@ const app = Vue.createApp({ }); } + // If we are the offerer, and this member wants to auto-open our camera + // then add our own stream to the connection. + if (isOfferer && this.whoMap[username].videoMutualOpen && this.webcam.active) { + let stream = this.webcam.stream; + stream.getTracks().forEach(track => { + pc.addTrack(track, stream) + }); + } + // If we are the offerer, begin the connection. if (isOfferer) { pc.createOffer({ @@ -953,6 +966,14 @@ const app = Vue.createApp({ return; } + // If this user requests mutual viewership... + if (user.videoMutual && !this.webcam.active) { + this.ChatClient( + `${user.username} has requested that you should share your own camera too before opening theirs.` + ); + return; + } + this.sendOpen(user.username); // Responsive CSS -> go to chat panel to see the camera @@ -995,6 +1016,16 @@ const app = Vue.createApp({ // Inform backend we have closed it. this.sendWatch(username, false); }, + unMutualVideo() { + // If we had our camera on to watch a video of someone who wants mutual cameras, + // and then we turn ours off: we should unfollow the ones with mutual video. + for (let row of this.whoList) { + let username = row.username; + if (row.videoMutual && this.WebRTC.pc[username] != undefined) { + this.closeVideo(username); + } + } + }, // Show who watches our video. showViewers() { @@ -1052,6 +1083,9 @@ const app = Vue.createApp({ this.closeVideo(username, "answerer"); } + // Hang up on mutual cameras. + this.unMutualVideo(); + // Tell backend our camera state. this.sendMe(); }, diff --git a/web/templates/chat.html b/web/templates/chat.html index bf794c3..2d7b004 100644 --- a/web/templates/chat.html +++ b/web/templates/chat.html @@ -228,8 +228,8 @@ see who is watching will be at the top of the page.

-

- If your camera will be featuring "NSFW" or sexual content, please mark it as such by +

+ If your camera will be featuring "Explicit" or sexual content, please mark it as such by clicking on the button or checking the box below to start with it enabled.

@@ -237,7 +237,28 @@ + + +

+ +

+ +
+ +
+ +
+
@@ -778,8 +799,12 @@ :class="{ 'is-danger is-outlined': u.videoActive && u.nsfw, 'is-info is-outlined': u.videoActive && !u.nsfw, + 'cursor-notallowed': u.videoActive && u.videoMutual && !webcam.active, }" - title="Open video stream" + :title="`Open video stream` + + (u.videoActive && u.videoMutual ? '; mutual video sharing required' : '') + + (u.videoActive && u.videoMutualOpen ? '; will auto-open your video' : '')" + @click="openVideo(u)">