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) {
this.WebRTC.pc[username].offerer.close(); if (this.webcam.mutualOpen && this.isWatchingMe(username)) {
delete (this.WebRTC.pc[username]); console.log(`OFFERER(${username}): Close video locally only: do not hang up the connection.`);
} else {
this.WebRTC.pc[username].offerer.close();
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) {
this.WebRTC.pc[username].answerer.close(); if (this.webcam.mutualOpen && this.isWatchingMe(username)) {
delete (this.WebRTC.pc[username]); console.log(`ANSWERER(${username}): Close video locally only: do not hang up the connection.`);
} else {
this.WebRTC.pc[username].answerer.close();
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>
@ -3483,8 +3561,8 @@ export default {
</div> </div>
<!-- Device Pickers: just in case the user had granted video permission in the past, <!-- Device Pickers: just in case the user had granted video permission in the past,
and we are able to enumerate their device names, we can show them here before they and we are able to enumerate their device names, we can show them here before they
go on this time.--> go on this time.-->
<div class="columns is-mobile" v-if="webcam.videoDevices.length > 0 || webcam.audioDevices.length > 0"> <div class="columns is-mobile" v-if="webcam.videoDevices.length > 0 || webcam.audioDevices.length > 0">
<div class="column"> <div class="column">
@ -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,56 +3856,41 @@ 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 -->
<!-- <div class="feed"> <!-- <div class="feed">
hi hi
</div> </div>
<div class="feed"> <div class="feed">
hi hi
</div> </div>
<div class="feed"> <div class="feed">
hi hi
</div> </div>
<div class="feed"> <div class="feed">
hi hi
</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>