Fix webcam freezing issues with mutualOpen video connections

This commit is contained in:
Noah 2023-11-18 15:38:02 -08:00
parent 356d2ddfa8
commit deb3bb616b
5 changed files with 209 additions and 218 deletions

View File

@ -87,8 +87,20 @@ func LogChannel(s *Server, channel string, username string, msg messages.Message
) )
} }
// Tear down log files for subscribers.
func (s *Subscriber) teardownLogs() {
if s.logfh == nil {
return
}
for username, fh := range s.logfh {
log.Error("TeardownLogs(%s/%s)", s.Username, username)
fh.Close()
}
}
// Initialize a logging directory. // Initialize a logging directory.
func initLogFile(sub LogCacheable, components ...string) (io.Writer, error) { func initLogFile(sub LogCacheable, components ...string) (io.WriteCloser, error) {
// Initialize the logfh cache? // Initialize the logfh cache?
var logfh = sub.GetLogFilehandleCache() var logfh = sub.GetLogFilehandleCache()
@ -126,19 +138,19 @@ func initLogFile(sub LogCacheable, components ...string) (io.Writer, error) {
// Interface for objects that hold log filehandle caches. // Interface for objects that hold log filehandle caches.
type LogCacheable interface { type LogCacheable interface {
GetLogFilehandleCache() map[string]io.Writer GetLogFilehandleCache() map[string]io.WriteCloser
} }
// Implementations of LogCacheable. // Implementations of LogCacheable.
func (sub *Subscriber) GetLogFilehandleCache() map[string]io.Writer { func (sub *Subscriber) GetLogFilehandleCache() map[string]io.WriteCloser {
if sub.logfh == nil { if sub.logfh == nil {
sub.logfh = map[string]io.Writer{} sub.logfh = map[string]io.WriteCloser{}
} }
return sub.logfh return sub.logfh
} }
func (s *Server) GetLogFilehandleCache() map[string]io.Writer { func (s *Server) GetLogFilehandleCache() map[string]io.WriteCloser {
if s.logfh == nil { if s.logfh == nil {
s.logfh = map[string]io.Writer{} s.logfh = map[string]io.WriteCloser{}
} }
return s.logfh return s.logfh
} }

View File

@ -19,7 +19,7 @@ type Server struct {
subscribers map[*Subscriber]struct{} subscribers map[*Subscriber]struct{}
// Cached filehandles for channel logging. // Cached filehandles for channel logging.
logfh map[string]io.Writer logfh map[string]io.WriteCloser
} }
// NewServer initializes the Server. // NewServer initializes the Server.

View File

@ -48,7 +48,7 @@ type Subscriber struct {
// Logging. // Logging.
log bool log bool
logfh map[string]io.Writer logfh map[string]io.WriteCloser
} }
// ReadLoop spawns a goroutine that reads from the websocket connection. // ReadLoop spawns a goroutine that reads from the websocket connection.
@ -288,6 +288,9 @@ func (s *Server) DeleteSubscriber(sub *Subscriber) {
sub.cancel() sub.cancel()
} }
// Clean up any log files.
sub.teardownLogs()
s.subscribersMu.Lock() s.subscribersMu.Lock()
delete(s.subscribers, sub) delete(s.subscribers, sub)
s.subscribersMu.Unlock() s.subscribersMu.Unlock()

View File

@ -445,7 +445,7 @@ export default {
} }
} }
}, },
"webcam.rememberExpresslyClosed": function() { "webcam.rememberExpresslyClosed": function () {
LocalStorage.set('rememberExpresslyClosed', this.webcam.rememberExpresslyClosed); LocalStorage.set('rememberExpresslyClosed', this.webcam.rememberExpresslyClosed);
}, },
@ -899,6 +899,28 @@ export default {
return; return;
} }
// DEBUGGING: print WebRTC statistics
if (this.message.toLowerCase().indexOf("/debug-webrtc") === 0) {
let lines = [
"<strong>WebRTC PeerConnections:</strong>"
];
for (let username of Object.keys(this.WebRTC.pc)) {
let pc = this.WebRTC.pc[username];
let line = `${username}: `;
if (pc.offerer != undefined) {
line += "offerer; ";
}
if (pc.answerer != undefined) {
line += "answerer; ";
}
lines.push(line);
}
this.ChatClient(lines.join("<br>"));
this.message = "";
return;
}
// console.debug("Send message: %s", this.message); // console.debug("Send message: %s", this.message);
this.ws.conn.send(JSON.stringify({ this.ws.conn.send(JSON.stringify({
action: "message", action: "message",
@ -1633,6 +1655,7 @@ export default {
// The user has closed our video feed. // The user has closed our video feed.
delete (this.webcam.watching[msg.username]); delete (this.webcam.watching[msg.username]);
this.playSound("Unwatch"); this.playSound("Unwatch");
this.cleanupPeerConnections();
}, },
sendWatch(username, watching) { sendWatch(username, watching) {
// Send the watch or unwatch message to backend. // Send the watch or unwatch message to backend.
@ -2108,7 +2131,7 @@ export default {
// If the local user had expressly closed this user's camera before, forget // If the local user had expressly closed this user's camera before, forget
// this action because the user now is expressly OPENING this camera on purpose. // this action because the user now is expressly OPENING this camera on purpose.
delete(this.WebRTC.expresslyClosed[user.username]); delete (this.WebRTC.expresslyClosed[user.username]);
// Debounce so we don't spam too much for the same user. // Debounce so we don't spam too much for the same user.
if (this.WebRTC.debounceOpens[user.username]) return; if (this.WebRTC.debounceOpens[user.username]) return;
@ -2174,20 +2197,38 @@ export default {
delete (this.WebRTC.streams[username]); delete (this.WebRTC.streams[username]);
delete (this.WebRTC.muted[username]); delete (this.WebRTC.muted[username]);
delete (this.WebRTC.poppedOut[username]); delete (this.WebRTC.poppedOut[username]);
// Should we close the WebRTC PeerConnection? If they were watching our video back, closing
// the connection MAY cause our video to freeze on their side: if we have the "auto-open my viewer's
// camera" option set, and our viewer sent their video on their open request, and they still have
// our camera open, do not close the connection so we don't freeze their side of the video.
if (this.WebRTC.pc[username] != undefined && this.WebRTC.pc[username].offerer != undefined) { if (this.WebRTC.pc[username] != undefined && this.WebRTC.pc[username].offerer != undefined) {
if (this.webcam.mutualOpen && this.isWatchingMe(username)) {
console.log(`OFFERER(${username}): Close video locally only: do not hang up the connection.`);
} else {
this.WebRTC.pc[username].offerer.close(); this.WebRTC.pc[username].offerer.close();
delete (this.WebRTC.pc[username]); delete (this.WebRTC.pc[username]);
} }
}
// Inform backend we have closed it. // Inform backend we have closed it.
this.sendWatch(username, false); this.sendWatch(username, false);
this.cleanupPeerConnections();
return; return;
} else if (name === "answerer") { } else if (name === "answerer") {
// We have turned off our camera, kick off viewers. // Should we close the WebRTC PeerConnection? If they were watching our video back, closing
// the connection MAY cause our video to freeze on their side: if we have the "auto-open my viewer's
// camera" option set, and our viewer sent their video on their open request, and they still have
// our camera open, do not close the connection so we don't freeze their side of the video.
if (this.WebRTC.pc[username] != undefined && this.WebRTC.pc[username].answerer != undefined) { if (this.WebRTC.pc[username] != undefined && this.WebRTC.pc[username].answerer != undefined) {
if (this.webcam.mutualOpen && this.isWatchingMe(username)) {
console.log(`ANSWERER(${username}): Close video locally only: do not hang up the connection.`);
} else {
this.WebRTC.pc[username].answerer.close(); this.WebRTC.pc[username].answerer.close();
delete (this.WebRTC.pc[username]); delete (this.WebRTC.pc[username]);
} }
}
this.cleanupPeerConnections();
return; return;
} }
@ -2215,6 +2256,7 @@ export default {
// Inform backend we have closed it. // Inform backend we have closed it.
this.sendWatch(username, false); this.sendWatch(username, false);
this.cleanupPeerConnections();
}, },
expresslyCloseVideo(username, name) { expresslyCloseVideo(username, name) {
// Like closeVideo but communicates the user's intent to expressly // Like closeVideo but communicates the user's intent to expressly
@ -2230,6 +2272,43 @@ export default {
this.closeVideo(username, "offerer"); this.closeVideo(username, "offerer");
} }
}, },
cleanupPeerConnections() {
// Helper function to check and clean up WebRTC PeerConnections.
//
// This is fired on Unwatch and CloseVideo events, to double check
// which videos the local user has open + who online is watching our
// video, to close out any lingering WebRTC connections.
for (let username of Object.keys(this.WebRTC.pc)) {
let pc = this.WebRTC.pc[username];
// Is their video on our screen?
if (this.WebRTC.streams[username] != undefined) {
continue;
}
// Are they watching us?
if (this.isWatchingMe(username)) {
continue;
}
// Are they an admin that we booted?
if (this.isBootedAdmin(username)) {
continue;
}
// The WebRTC connections should be closed out.
if (pc.answerer != undefined) {
console.log("Clean up WebRTC answerer connection with " + username);
pc.answerer.close();
delete (this.WebRTC.pc[username]);
}
if (pc.offerer != undefined) {
console.log("Clean up WebRTC offerer connection with " + username);
pc.offerer.close();
delete (this.WebRTC.pc[username]);
}
}
},
muteAllVideos() { muteAllVideos() {
// Mute the mic of all open videos. // Mute the mic of all open videos.
let count = 0; let count = 0;
@ -2357,7 +2436,7 @@ export default {
} }
this.sendUnboot(username); this.sendUnboot(username);
delete(this.WebRTC.booted[username]); delete (this.WebRTC.booted[username]);
return; return;
} }
@ -2379,7 +2458,7 @@ export default {
} }
// Remove them from our list. // Remove them from our list.
delete(this.webcam.watching[username]); delete (this.webcam.watching[username]);
this.ChatClient( this.ChatClient(
`You have booted ${username} off your camera. They will no longer be able ` + `You have booted ${username} off your camera. They will no longer be able ` +
@ -2612,8 +2691,8 @@ export default {
} }
// Were we at mentioned in this message? // Were we at mentioned in this message?
if (message.indexOf("@"+this.username) > -1) { if (message.indexOf("@" + this.username) > -1) {
let re = new RegExp("@"+this.username+"\\b", "ig"); let re = new RegExp("@" + this.username + "\\b", "ig");
message = message.replace(re, `<strong class="has-background-at-mention">@${this.username}</strong>`); message = message.replace(re, `<strong class="has-background-at-mention">@${this.username}</strong>`);
} }
@ -3097,7 +3176,8 @@ export default {
<div class="control"> <div class="control">
<div class="select is-fullwidth"> <div class="select is-fullwidth">
<select v-model="messageStyle"> <select v-model="messageStyle">
<option v-for="s in config.messageStyleSettings" v-bind:key="s[0]" :value="s[0]"> <option v-for="s in config.messageStyleSettings" v-bind:key="s[0]"
:value="s[0]">
{{ s[1] }} {{ s[1] }}
</option> </option>
</select> </select>
@ -3325,8 +3405,7 @@ export default {
<div class="select is-fullwidth"> <div class="select is-fullwidth">
<select v-model="webcam.videoDeviceID" <select v-model="webcam.videoDeviceID"
@change="startVideo({ changeCamera: true, force: true })"> @change="startVideo({ changeCamera: true, force: true })">
<option v-for="(d, i) in webcam.videoDevices" :value="d.id" <option v-for="(d, i) in webcam.videoDevices" :value="d.id" v-bind:key="i">
v-bind:key="i">
{{ d.label || `Camera ${i}` }} {{ d.label || `Camera ${i}` }}
</option> </option>
</select> </select>
@ -3338,8 +3417,7 @@ export default {
<div class="select is-fullwidth"> <div class="select is-fullwidth">
<select v-model="webcam.audioDeviceID" <select v-model="webcam.audioDeviceID"
@change="startVideo({ changeCamera: true, force: true })"> @change="startVideo({ changeCamera: true, force: true })">
<option v-for="(d, i) in webcam.audioDevices" :value="d.id" <option v-for="(d, i) in webcam.audioDevices" :value="d.id" v-bind:key="i">
v-bind:key="i">
{{ d.label || `Microphone ${i}` }} {{ d.label || `Microphone ${i}` }}
</option> </option>
</select> </select>
@ -3525,35 +3603,20 @@ export default {
</div> </div>
<!-- NSFW Modal: before user views a NSFW camera the first time --> <!-- NSFW Modal: before user views a NSFW camera the first time -->
<ExplicitOpenModal :visible="nsfwModalView.visible" <ExplicitOpenModal :visible="nsfwModalView.visible" :user="nsfwModalView.user"
:user="nsfwModalView.user" @accept="openVideo(nsfwModalView.user, true)" @cancel="nsfwModalView.visible = false"
@accept="openVideo(nsfwModalView.user, true)"
@cancel="nsfwModalView.visible=false"
@dont-show-again="setSkipNSFWModal()"></ExplicitOpenModal> @dont-show-again="setSkipNSFWModal()"></ExplicitOpenModal>
<!-- Report Modal --> <!-- Report Modal -->
<ReportModal :visible="reportModal.visible" <ReportModal :visible="reportModal.visible" :busy="reportModal.busy" :user="reportModal.user"
:busy="reportModal.busy" :message="reportModal.message" @accept="doReport" @cancel="reportModal.visible = false"></ReportModal>
:user="reportModal.user"
:message="reportModal.message"
@accept="doReport"
@cancel="reportModal.visible=false"></ReportModal>
<!-- Profile Modal (profile cards popup) --> <!-- Profile Modal (profile cards popup) -->
<ProfileModal :visible="profileModal.visible" <ProfileModal :visible="profileModal.visible" :user="profileModal.user" :username="username" :jwt="jwt.token"
:user="profileModal.user" :website-url="config.website" :is-dnd="isUsernameDND(profileModal.username)"
:username="username" :is-muted="isMutedUser(profileModal.username)" :is-booted="isBooted(profileModal.username)"
:jwt="jwt.token" :profile-webhook-enabled="isWebhookEnabled('profile')" :vip-config="config.VIP" @send-dm="openDMs"
:website-url="config.website" @mute-user="muteUser" @boot-user="bootUser" @cancel="profileModal.visible = false"></ProfileModal>
:is-dnd="isUsernameDND(profileModal.username)"
:is-muted="isMutedUser(profileModal.username)"
:is-booted="isBooted(profileModal.username)"
:profile-webhook-enabled="isWebhookEnabled('profile')"
:vip-config="config.VIP"
@send-dm="openDMs"
@mute-user="muteUser"
@boot-user="bootUser"
@cancel="profileModal.visible=false"></ProfileModal>
<div class="chat-container"> <div class="chat-container">
@ -3608,8 +3671,7 @@ export default {
<!-- Note: the onclick for the previous div is handled in index.html --> <!-- Note: the onclick for the previous div is handled in index.html -->
<div class="dropdown-trigger"> <div class="dropdown-trigger">
<button type="button" class="button is-small is-link px-2" <button type="button" class="button is-small is-link px-2" aria-haspopup="true"
aria-haspopup="true"
aria-controls="chat-settings-menu"> aria-controls="chat-settings-menu">
<span> <span>
<i class="fa fa-bars"></i> <i class="fa fa-bars"></i>
@ -3623,13 +3685,11 @@ export default {
<i class="fa fa-gear mr-1"></i> Chat Settings <i class="fa fa-gear mr-1"></i> Chat Settings
</a> </a>
<a href="#" class="dropdown-item" v-if="numVideosOpen > 0" <a href="#" class="dropdown-item" v-if="numVideosOpen > 0" @click.prevent="closeOpenVideos()">
@click.prevent="closeOpenVideos()">
<i class="fa fa-video-slash mr-1"></i> Close all cameras <i class="fa fa-video-slash mr-1"></i> Close all cameras
</a> </a>
<a href="#" class="dropdown-item" v-if="numVideosOpen > 0" <a href="#" class="dropdown-item" v-if="numVideosOpen > 0" @click.prevent="muteAllVideos()">
@click.prevent="muteAllVideos()">
<i class="fa fa-microphone-slash mr-1"></i> Mute all cameras <i class="fa fa-microphone-slash mr-1"></i> Mute all cameras
</a> </a>
@ -3669,7 +3729,8 @@ export default {
<ul class="menu-list"> <ul class="menu-list">
<li v-for="c in activeChannels()" v-bind:key="c.ID"> <li v-for="c in activeChannels()" v-bind:key="c.ID">
<a :href="'#' + c.ID" @click.prevent="setChannel(c)" :class="{ 'is-active': c.ID == channel }"> <a :href="'#' + c.ID" @click.prevent="setChannel(c)"
:class="{ 'is-active': c.ID == channel }">
{{ c.Name }} {{ c.Name }}
<span v-if="hasUnread(c.ID)" class="tag is-success"> <span v-if="hasUnread(c.ID)" class="tag is-success">
{{ hasUnread(c.ID) }} {{ hasUnread(c.ID) }}
@ -3716,9 +3777,7 @@ export default {
<!-- Close new DMs toggle --> <!-- Close new DMs toggle -->
<div class="tag mt-2"> <div class="tag mt-2">
<label class="checkbox"> <label class="checkbox">
<input type="checkbox" <input type="checkbox" v-model="prefs.closeDMs" :value="true">
v-model="prefs.closeDMs"
:value="true">
Ignore unsolicited DMs Ignore unsolicited DMs
<a href="#" <a href="#"
@ -3756,14 +3815,12 @@ export default {
<!-- Easy video zoom buttons --> <!-- Easy video zoom buttons -->
<div class="column is-narrow is-hidden-mobile" v-if="anyVideosOpen"> <div class="column is-narrow is-hidden-mobile" v-if="anyVideosOpen">
<button type="button" class="button is-small is-outlined" <button type="button" class="button is-small is-outlined" :disabled="webcam.videoScale === 'x4'"
:disabled="webcam.videoScale === 'x4'"
@click="scaleVideoSize(true)"> @click="scaleVideoSize(true)">
<i class="fa fa-magnifying-glass-plus"></i> <i class="fa fa-magnifying-glass-plus"></i>
</button> </button>
<button type="button" class="button is-small is-outlined" <button type="button" class="button is-small is-outlined" :disabled="webcam.videoScale === ''"
:disabled="webcam.videoScale === ''"
@click="scaleVideoSize(false)"> @click="scaleVideoSize(false)">
<i class="fa fa-magnifying-glass-minus"></i> <i class="fa fa-magnifying-glass-minus"></i>
</button> </button>
@ -3773,13 +3830,14 @@ export default {
<div class="column is-narrow" v-if="channel.indexOf('@') === 0"> <div class="column is-narrow" v-if="channel.indexOf('@') === 0">
<!-- If the user has a profile URL --> <!-- If the user has a profile URL -->
<button type="button" v-if="profileURLForUsername(channel)" <button type="button" v-if="profileURLForUsername(channel)"
class="button is-small is-outlined is-light mr-1" @click="openProfile({ username: channel })"> class="button is-small is-outlined is-light mr-1"
@click="openProfile({ username: channel })">
<i class="fa fa-user"></i> <i class="fa fa-user"></i>
</button> </button>
<!-- DMs: Leave convo button --> <!-- DMs: Leave convo button -->
<button type="button" <button type="button" class="float-right button is-small is-warning is-outlined"
class="float-right button is-small is-warning is-outlined" @click="leaveDM()"> @click="leaveDM()">
<i class="fa fa-trash"></i> <i class="fa fa-trash"></i>
</button> </button>
</div> </div>
@ -3798,35 +3856,19 @@ export default {
<!-- Video Feeds--> <!-- Video Feeds-->
<!-- My video --> <!-- My video -->
<VideoFeed <VideoFeed v-show="webcam.active" :local-video="true" :username="username"
v-show="webcam.active" :popped-out="WebRTC.poppedOut[username]" :is-explicit="webcam.nsfw" :is-muted="webcam.muted"
:local-video="true" :is-source-muted="webcam.muted" @mute-video="muteMe()" @popout="popoutVideo"
:username="username"
:popped-out="WebRTC.poppedOut[username]"
:is-explicit="webcam.nsfw"
:is-muted="webcam.muted"
:is-source-muted="webcam.muted"
@mute-video="muteMe()"
@popout="popoutVideo"
@set-volume="setVideoVolume"> @set-volume="setVideoVolume">
</VideoFeed> </VideoFeed>
<!-- Others' videos --> <!-- Others' videos -->
<VideoFeed <VideoFeed v-for="(stream, username) in WebRTC.streams" v-bind:key="username" :username="username"
v-for="(stream, username) in WebRTC.streams" :popped-out="WebRTC.poppedOut[username]" :is-explicit="isUsernameCamNSFW(username)"
v-bind:key="username" :is-source-muted="isSourceMuted(username)" :is-muted="isMuted(username)"
:username="username" :is-watching-me="isWatchingMe(username)" :is-frozen="WebRTC.frozenStreamDetected[username]"
:popped-out="WebRTC.poppedOut[username]" @reopen-video="openVideoByUsername" @mute-video="muteVideo" @popout="popoutVideo"
:is-explicit="isUsernameCamNSFW(username)" @close-video="expresslyCloseVideo" @set-volume="setVideoVolume">
:is-source-muted="isSourceMuted(username)"
:is-muted="isMuted(username)"
:is-watching-me="isWatchingMe(username)"
:is-frozen="WebRTC.frozenStreamDetected[username]"
@reopen-video="openVideoByUsername"
@mute-video="muteVideo"
@popout="popoutVideo"
@close-video="expresslyCloseVideo"
@set-volume="setVideoVolume">
</VideoFeed> </VideoFeed>
<!-- Debugging - copy a lot of these to simulate more videos --> <!-- Debugging - copy a lot of these to simulate more videos -->
@ -3845,9 +3887,10 @@ export default {
</div> --> </div> -->
</div> </div>
<div class="card-content" id="chatHistory" <div class="card-content" id="chatHistory" :class="{
:class="{ 'has-background-dm': isDM, 'has-background-dm': isDM,
'p-1 pb-5': messageStyle.indexOf('compact') === 0 }"> 'p-1 pb-5': messageStyle.indexOf('compact') === 0
}">
<div class="autoscroll-field tag"> <div class="autoscroll-field tag">
<label class="checkbox is-size-6" title="Automatically scroll when new chat messages come in."> <label class="checkbox is-size-6" title="Automatically scroll when new chat messages come in.">
@ -3879,29 +3922,14 @@ export default {
<div v-for="(msg, i) in chatHistory" v-bind:key="i"> <div v-for="(msg, i) in chatHistory" v-bind:key="i">
<MessageBox <MessageBox :message="msg" :is-presence="msg.action === 'presence'" :appearance="messageStyle"
:message="msg" :position="i" :user="getUser(msg.username)" :is-offline="isUserOffline(msg.username)"
:is-presence="msg.action === 'presence'" :username="username" :website-url="config.website" :is-dnd="isUsernameDND(msg.username)"
:appearance="messageStyle" :is-muted="isMutedUser(msg.username)" :reactions="getReactions(msg)"
:position="i" :report-enabled="isWebhookEnabled('report')" :is-dm="isDM" :is-op="isOp"
:user="getUser(msg.username)" @open-profile="showProfileModal" @send-dm="openDMs" @mute-user="muteUser"
:is-offline="isUserOffline(msg.username)" @takeback="takeback" @remove="removeMessage" @report="reportMessage" @react="sendReact">
:username="username" </MessageBox>
:website-url="config.website"
:is-dnd="isUsernameDND(msg.username)"
:is-muted="isMutedUser(msg.username)"
:reactions="getReactions(msg)"
:report-enabled="isWebhookEnabled('report')"
:is-dm="isDM"
:is-op="isOp"
@open-profile="showProfileModal"
@send-dm="openDMs"
@mute-user="muteUser"
@takeback="takeback"
@remove="removeMessage"
@report="reportMessage"
@react="sendReact"
></MessageBox>
</div> </div>
@ -3936,11 +3964,7 @@ export default {
<form @submit.prevent="sendMessage()"> <form @submit.prevent="sendMessage()">
<!-- At Mentions --> <!-- At Mentions -->
<Mentionable <Mentionable :keys="['@']" :items="atMentionItems" offset="12" insert-space>
:keys="['@']"
:items="atMentionItems"
offset="12"
insert-space>
<!-- My text box --> <!-- My text box -->
<input type="text" class="input" id="messageBox" v-model="message" <input type="text" class="input" id="messageBox" v-model="message"
@ -3966,29 +3990,22 @@ export default {
</form> </form>
</div> </div>
<div class="column px-1 is-narrow dropdown is-right is-up" :class="{ 'is-active': showEmojiPicker }" <div class="column px-1 is-narrow dropdown is-right is-up" :class="{ 'is-active': showEmojiPicker }"
@click="showEmojiPicker=true"> @click="showEmojiPicker = true">
<!-- Emoji picker for messages --> <!-- Emoji picker for messages -->
<div class="dropdown-trigger"> <div class="dropdown-trigger">
<button type="button" class="button" <button type="button" class="button" aria-haspopup="true" aria-controls="input-emoji-picker"
aria-haspopup="true"
aria-controls="input-emoji-picker"
@click="hideEmojiPicker()"> @click="hideEmojiPicker()">
<span> <span>
<i class="fa-regular fa-smile"></i> <i class="fa-regular fa-smile"></i>
</span> </span>
</button> </button>
</div> </div>
<div class="dropdown-menu" id="input-emoji-picker" role="menu" <div class="dropdown-menu" id="input-emoji-picker" role="menu" style="z-index: 9000">
style="z-index: 9000">
<!-- Note: z-index so the popup isn't covered by the "Auto-scroll" <!-- Note: z-index so the popup isn't covered by the "Auto-scroll"
label on the chat history panel --> label on the chat history panel -->
<div class="dropdown-content p-0"> <div class="dropdown-content p-0">
<EmojiPicker <EmojiPicker :native="true" :display-recent="true" :disable-skin-tones="true"
:native="true" theme="auto" @select="onSelectEmoji">
:display-recent="true"
:disable-skin-tones="true"
theme="auto"
@select="onSelectEmoji">
</EmojiPicker> </EmojiPicker>
</div> </div>
</div> </div>
@ -4091,43 +4108,22 @@ export default {
<!-- Who Is Online --> <!-- Who Is Online -->
<ul class="menu-list" v-if="whoTab === 'online'"> <ul class="menu-list" v-if="whoTab === 'online'">
<li v-for="(u, i) in sortedWhoList" v-bind:key="i"> <li v-for="(u, i) in sortedWhoList" v-bind:key="i">
<WhoListRow <WhoListRow :user="u" :username="username" :website-url="config.website"
:user="u" :is-dnd="isUsernameDND(u.username)" :is-muted="isMutedUser(u.username)"
:username="username" :is-booted="isBooted(u.username)" :is-op="isOp" :is-video-not-allowed="isVideoNotAllowed(u)"
:website-url="config.website" :video-icon-class="webcamIconClass(u)" :vip-config="config.VIP" @send-dm="openDMs"
:is-dnd="isUsernameDND(u.username)" @mute-user="muteUser" @open-video="openVideo" @open-profile="showProfileModal"></WhoListRow>
:is-muted="isMutedUser(u.username)"
:is-booted="isBooted(u.username)"
:is-op="isOp"
:is-video-not-allowed="isVideoNotAllowed(u)"
:video-icon-class="webcamIconClass(u)"
:vip-config="config.VIP"
@send-dm="openDMs"
@mute-user="muteUser"
@open-video="openVideo"
@open-profile="showProfileModal"></WhoListRow>
</li> </li>
</ul> </ul>
<!-- Watching My Webcam --> <!-- Watching My Webcam -->
<ul class="menu-list" v-if="whoTab === 'watching'"> <ul class="menu-list" v-if="whoTab === 'watching'">
<li v-for="(u, i) in sortedWatchingList" v-bind:key="username"> <li v-for="(u, i) in sortedWatchingList" v-bind:key="username">
<WhoListRow <WhoListRow :is-watching-tab="true" :user="u" :username="username" :website-url="config.website"
:is-watching-tab="true" :is-dnd="isUsernameDND(username)" :is-muted="isMutedUser(username)"
:user="u" :is-booted="isBooted(u.username)" :is-op="isOp" :is-video-not-allowed="isVideoNotAllowed(u)"
:username="username" :video-icon-class="webcamIconClass(u)" :vip-config="config.VIP" @send-dm="openDMs"
:website-url="config.website" @mute-user="muteUser" @open-video="openVideo" @boot-user="bootUser"
:is-dnd="isUsernameDND(username)"
:is-muted="isMutedUser(username)"
:is-booted="isBooted(u.username)"
:is-op="isOp"
:is-video-not-allowed="isVideoNotAllowed(u)"
:video-icon-class="webcamIconClass(u)"
:vip-config="config.VIP"
@send-dm="openDMs"
@mute-user="muteUser"
@open-video="openVideo"
@boot-user="bootUser"
@open-profile="showProfileModal"></WhoListRow> @open-profile="showProfileModal"></WhoListRow>
</li> </li>
</ul> </ul>

View File

@ -32,11 +32,6 @@ export default {
return this.localVideo ? 'localVideo' : `videofeed-${this.username}`; return this.localVideo ? 'localVideo' : `videofeed-${this.username}`;
}, },
}, },
watch: {
mouseOver() {
console.log("mouse over:", this.mouseOver);
},
},
methods: { methods: {
closeVideo() { closeVideo() {
// Note: closeVideo only available for OTHER peoples cameras. // Note: closeVideo only available for OTHER peoples cameras.
@ -85,7 +80,7 @@ export default {
<div class="feed" :class="{ <div class="feed" :class="{
'popped-out': poppedOut, 'popped-out': poppedOut,
'popped-in': !poppedOut, 'popped-in': !poppedOut,
}" @mouseover="mouseOver=true" @mouseleave="mouseOver=false"> }" @mouseover="mouseOver = true" @mouseleave="mouseOver = false">
<video class="feed" :id="videoID" autoplay :muted="localVideo"></video> <video class="feed" :id="videoID" autoplay :muted="localVideo"></video>
<!-- Caption --> <!-- Caption -->
@ -93,23 +88,19 @@ export default {
'has-text-camera-blue': !isExplicit, 'has-text-camera-blue': !isExplicit,
'has-text-camera-red': isExplicit, 'has-text-camera-red': isExplicit,
}"> }">
<i class="fa fa-microphone-slash mr-1 has-text-grey" <i class="fa fa-microphone-slash mr-1 has-text-grey" v-if="isSourceMuted"></i>
v-if="isSourceMuted"></i>
{{ username }} {{ username }}
<i class="fa fa-people-arrows ml-1 has-text-grey is-size-7" <i class="fa fa-people-arrows ml-1 has-text-grey is-size-7" :title="username + ' is watching your camera too'"
:title="username + ' is watching your camera too'"
v-if="isWatchingMe"></i> v-if="isWatchingMe"></i>
<!-- Frozen stream detection --> <!-- Frozen stream detection -->
<a class="fa fa-mountain ml-1" href="#" v-if="!localVideo && isFrozen" <a class="fa fa-mountain ml-1" href="#" v-if="!localVideo && isFrozen" style="color: #00FFFF"
style="color: #00FFFF" @click.prevent="reopenVideo()" @click.prevent="reopenVideo()" title="Frozen video detected!"></a>
title="Frozen video detected!"></a>
</div> </div>
<!-- Close button (others' videos only) --> <!-- Close button (others' videos only) -->
<div class="close" v-if="!localVideo"> <div class="close" v-if="!localVideo">
<a href="#" class="has-text-danger" title="Close video" <a href="#" class="has-text-danger" title="Close video" @click.prevent="closeVideo()">
@click.prevent="closeVideo()">
<i class="fa fa-close"></i> <i class="fa fa-close"></i>
</a> </a>
</div> </div>
@ -117,31 +108,28 @@ export default {
<!-- Controls --> <!-- Controls -->
<div class="controls"> <div class="controls">
<!-- Mute Button --> <!-- Mute Button -->
<button type="button" v-if="!isMuted" <button type="button" v-if="!isMuted" class="button is-small is-success is-outlined ml-1 px-2"
class="button is-small is-success is-outlined ml-1 px-2"
@click="muteVideo()"> @click="muteVideo()">
<i class="fa" <i class="fa" :class="{
:class="{'fa-microphone': localVideo, 'fa-microphone': localVideo,
'fa-volume-high': !localVideo}"></i> 'fa-volume-high': !localVideo
}"></i>
</button> </button>
<button type="button" v-else <button type="button" v-else class="button is-small is-danger ml-1 px-2" @click="muteVideo()">
class="button is-small is-danger ml-1 px-2" <i class="fa" :class="{
@click="muteVideo()"> 'fa-microphone-slash': localVideo,
<i class="fa" 'fa-volume-xmark': !localVideo
:class="{'fa-microphone-slash': localVideo, }"></i>
'fa-volume-xmark': !localVideo}"></i>
</button> </button>
<!-- Pop-out Video --> <!-- Pop-out Video -->
<button type="button" class="button is-small is-light is-outlined p-2 ml-2" <button type="button" class="button is-small is-light is-outlined p-2 ml-2" title="Pop out"
title="Pop out"
@click="popoutVideo()"> @click="popoutVideo()">
<i class="fa fa-up-right-from-square"></i> <i class="fa fa-up-right-from-square"></i>
</button> </button>
<!-- Full screen --> <!-- Full screen -->
<button type="button" class="button is-small is-light is-outlined p-2 ml-2" <button type="button" class="button is-small is-light is-outlined p-2 ml-2" title="Go full screen"
title="Go full screen"
@click="fullscreen()"> @click="fullscreen()">
<i class="fa fa-expand"></i> <i class="fa fa-expand"></i>
</button> </button>
@ -149,16 +137,8 @@ export default {
<!-- Volume slider --> <!-- Volume slider -->
<div class="volume-slider" v-show="!localVideo && !isMuted && mouseOver"> <div class="volume-slider" v-show="!localVideo && !isMuted && mouseOver">
<Slider <Slider v-model="volume" color="#00FF00" track-color="#006600" :min="0" :max="100" :step="1" :height="7"
v-model="volume" orientation="vertical" @change="volumeChanged">
color="#00FF00"
track-color="#006600"
:min="0"
:max="100"
:step="1"
:height="7"
orientation="vertical"
@change="volumeChanged">
</Slider> </Slider>
</div> </div>