Fix webcam freezing issues with mutualOpen video connections
This commit is contained in:
parent
356d2ddfa8
commit
deb3bb616b
|
@ -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.
|
||||
func initLogFile(sub LogCacheable, components ...string) (io.Writer, error) {
|
||||
func initLogFile(sub LogCacheable, components ...string) (io.WriteCloser, error) {
|
||||
// Initialize the logfh cache?
|
||||
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.
|
||||
type LogCacheable interface {
|
||||
GetLogFilehandleCache() map[string]io.Writer
|
||||
GetLogFilehandleCache() map[string]io.WriteCloser
|
||||
}
|
||||
|
||||
// Implementations of LogCacheable.
|
||||
func (sub *Subscriber) GetLogFilehandleCache() map[string]io.Writer {
|
||||
func (sub *Subscriber) GetLogFilehandleCache() map[string]io.WriteCloser {
|
||||
if sub.logfh == nil {
|
||||
sub.logfh = map[string]io.Writer{}
|
||||
sub.logfh = map[string]io.WriteCloser{}
|
||||
}
|
||||
return sub.logfh
|
||||
}
|
||||
func (s *Server) GetLogFilehandleCache() map[string]io.Writer {
|
||||
func (s *Server) GetLogFilehandleCache() map[string]io.WriteCloser {
|
||||
if s.logfh == nil {
|
||||
s.logfh = map[string]io.Writer{}
|
||||
s.logfh = map[string]io.WriteCloser{}
|
||||
}
|
||||
return s.logfh
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ type Server struct {
|
|||
subscribers map[*Subscriber]struct{}
|
||||
|
||||
// Cached filehandles for channel logging.
|
||||
logfh map[string]io.Writer
|
||||
logfh map[string]io.WriteCloser
|
||||
}
|
||||
|
||||
// NewServer initializes the Server.
|
||||
|
|
|
@ -48,7 +48,7 @@ type Subscriber struct {
|
|||
|
||||
// Logging.
|
||||
log bool
|
||||
logfh map[string]io.Writer
|
||||
logfh map[string]io.WriteCloser
|
||||
}
|
||||
|
||||
// ReadLoop spawns a goroutine that reads from the websocket connection.
|
||||
|
@ -288,6 +288,9 @@ func (s *Server) DeleteSubscriber(sub *Subscriber) {
|
|||
sub.cancel()
|
||||
}
|
||||
|
||||
// Clean up any log files.
|
||||
sub.teardownLogs()
|
||||
|
||||
s.subscribersMu.Lock()
|
||||
delete(s.subscribers, sub)
|
||||
s.subscribersMu.Unlock()
|
||||
|
|
336
src/App.vue
336
src/App.vue
|
@ -445,7 +445,7 @@ export default {
|
|||
}
|
||||
}
|
||||
},
|
||||
"webcam.rememberExpresslyClosed": function() {
|
||||
"webcam.rememberExpresslyClosed": function () {
|
||||
LocalStorage.set('rememberExpresslyClosed', this.webcam.rememberExpresslyClosed);
|
||||
},
|
||||
|
||||
|
@ -899,6 +899,28 @@ export default {
|
|||
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);
|
||||
this.ws.conn.send(JSON.stringify({
|
||||
action: "message",
|
||||
|
@ -1633,6 +1655,7 @@ export default {
|
|||
// The user has closed our video feed.
|
||||
delete (this.webcam.watching[msg.username]);
|
||||
this.playSound("Unwatch");
|
||||
this.cleanupPeerConnections();
|
||||
},
|
||||
sendWatch(username, watching) {
|
||||
// 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
|
||||
// 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.
|
||||
if (this.WebRTC.debounceOpens[user.username]) return;
|
||||
|
@ -2174,20 +2197,38 @@ export default {
|
|||
delete (this.WebRTC.streams[username]);
|
||||
delete (this.WebRTC.muted[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) {
|
||||
this.WebRTC.pc[username].offerer.close();
|
||||
delete (this.WebRTC.pc[username]);
|
||||
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();
|
||||
delete (this.WebRTC.pc[username]);
|
||||
}
|
||||
}
|
||||
|
||||
// Inform backend we have closed it.
|
||||
this.sendWatch(username, false);
|
||||
this.cleanupPeerConnections();
|
||||
return;
|
||||
} 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) {
|
||||
this.WebRTC.pc[username].answerer.close();
|
||||
delete (this.WebRTC.pc[username]);
|
||||
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();
|
||||
delete (this.WebRTC.pc[username]);
|
||||
}
|
||||
}
|
||||
this.cleanupPeerConnections();
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -2215,6 +2256,7 @@ export default {
|
|||
|
||||
// Inform backend we have closed it.
|
||||
this.sendWatch(username, false);
|
||||
this.cleanupPeerConnections();
|
||||
},
|
||||
expresslyCloseVideo(username, name) {
|
||||
// Like closeVideo but communicates the user's intent to expressly
|
||||
|
@ -2230,6 +2272,43 @@ export default {
|
|||
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() {
|
||||
// Mute the mic of all open videos.
|
||||
let count = 0;
|
||||
|
@ -2357,7 +2436,7 @@ export default {
|
|||
}
|
||||
|
||||
this.sendUnboot(username);
|
||||
delete(this.WebRTC.booted[username]);
|
||||
delete (this.WebRTC.booted[username]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
@ -2379,7 +2458,7 @@ export default {
|
|||
}
|
||||
|
||||
// Remove them from our list.
|
||||
delete(this.webcam.watching[username]);
|
||||
delete (this.webcam.watching[username]);
|
||||
|
||||
this.ChatClient(
|
||||
`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?
|
||||
if (message.indexOf("@"+this.username) > -1) {
|
||||
let re = new RegExp("@"+this.username+"\\b", "ig");
|
||||
if (message.indexOf("@" + this.username) > -1) {
|
||||
let re = new RegExp("@" + this.username + "\\b", "ig");
|
||||
message = message.replace(re, `<strong class="has-background-at-mention">@${this.username}</strong>`);
|
||||
}
|
||||
|
||||
|
@ -3097,7 +3176,8 @@ export default {
|
|||
<div class="control">
|
||||
<div class="select is-fullwidth">
|
||||
<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] }}
|
||||
</option>
|
||||
</select>
|
||||
|
@ -3325,8 +3405,7 @@ export default {
|
|||
<div class="select is-fullwidth">
|
||||
<select v-model="webcam.videoDeviceID"
|
||||
@change="startVideo({ changeCamera: true, force: true })">
|
||||
<option v-for="(d, i) in webcam.videoDevices" :value="d.id"
|
||||
v-bind:key="i">
|
||||
<option v-for="(d, i) in webcam.videoDevices" :value="d.id" v-bind:key="i">
|
||||
{{ d.label || `Camera ${i}` }}
|
||||
</option>
|
||||
</select>
|
||||
|
@ -3338,8 +3417,7 @@ export default {
|
|||
<div class="select is-fullwidth">
|
||||
<select v-model="webcam.audioDeviceID"
|
||||
@change="startVideo({ changeCamera: true, force: true })">
|
||||
<option v-for="(d, i) in webcam.audioDevices" :value="d.id"
|
||||
v-bind:key="i">
|
||||
<option v-for="(d, i) in webcam.audioDevices" :value="d.id" v-bind:key="i">
|
||||
{{ d.label || `Microphone ${i}` }}
|
||||
</option>
|
||||
</select>
|
||||
|
@ -3483,8 +3561,8 @@ export default {
|
|||
</div>
|
||||
|
||||
<!-- 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
|
||||
go on this time.-->
|
||||
and we are able to enumerate their device names, we can show them here before they
|
||||
go on this time.-->
|
||||
<div class="columns is-mobile" v-if="webcam.videoDevices.length > 0 || webcam.audioDevices.length > 0">
|
||||
|
||||
<div class="column">
|
||||
|
@ -3525,35 +3603,20 @@ export default {
|
|||
</div>
|
||||
|
||||
<!-- NSFW Modal: before user views a NSFW camera the first time -->
|
||||
<ExplicitOpenModal :visible="nsfwModalView.visible"
|
||||
:user="nsfwModalView.user"
|
||||
@accept="openVideo(nsfwModalView.user, true)"
|
||||
@cancel="nsfwModalView.visible=false"
|
||||
<ExplicitOpenModal :visible="nsfwModalView.visible" :user="nsfwModalView.user"
|
||||
@accept="openVideo(nsfwModalView.user, true)" @cancel="nsfwModalView.visible = false"
|
||||
@dont-show-again="setSkipNSFWModal()"></ExplicitOpenModal>
|
||||
|
||||
<!-- Report Modal -->
|
||||
<ReportModal :visible="reportModal.visible"
|
||||
:busy="reportModal.busy"
|
||||
:user="reportModal.user"
|
||||
:message="reportModal.message"
|
||||
@accept="doReport"
|
||||
@cancel="reportModal.visible=false"></ReportModal>
|
||||
<ReportModal :visible="reportModal.visible" :busy="reportModal.busy" :user="reportModal.user"
|
||||
:message="reportModal.message" @accept="doReport" @cancel="reportModal.visible = false"></ReportModal>
|
||||
|
||||
<!-- Profile Modal (profile cards popup) -->
|
||||
<ProfileModal :visible="profileModal.visible"
|
||||
:user="profileModal.user"
|
||||
:username="username"
|
||||
:jwt="jwt.token"
|
||||
:website-url="config.website"
|
||||
: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>
|
||||
<ProfileModal :visible="profileModal.visible" :user="profileModal.user" :username="username" :jwt="jwt.token"
|
||||
:website-url="config.website" :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">
|
||||
|
||||
|
@ -3608,8 +3671,7 @@ export default {
|
|||
<!-- Note: the onclick for the previous div is handled in index.html -->
|
||||
|
||||
<div class="dropdown-trigger">
|
||||
<button type="button" class="button is-small is-link px-2"
|
||||
aria-haspopup="true"
|
||||
<button type="button" class="button is-small is-link px-2" aria-haspopup="true"
|
||||
aria-controls="chat-settings-menu">
|
||||
<span>
|
||||
<i class="fa fa-bars"></i>
|
||||
|
@ -3623,13 +3685,11 @@ export default {
|
|||
<i class="fa fa-gear mr-1"></i> Chat Settings
|
||||
</a>
|
||||
|
||||
<a href="#" class="dropdown-item" v-if="numVideosOpen > 0"
|
||||
@click.prevent="closeOpenVideos()">
|
||||
<a href="#" class="dropdown-item" v-if="numVideosOpen > 0" @click.prevent="closeOpenVideos()">
|
||||
<i class="fa fa-video-slash mr-1"></i> Close all cameras
|
||||
</a>
|
||||
|
||||
<a href="#" class="dropdown-item" v-if="numVideosOpen > 0"
|
||||
@click.prevent="muteAllVideos()">
|
||||
<a href="#" class="dropdown-item" v-if="numVideosOpen > 0" @click.prevent="muteAllVideos()">
|
||||
<i class="fa fa-microphone-slash mr-1"></i> Mute all cameras
|
||||
</a>
|
||||
|
||||
|
@ -3669,7 +3729,8 @@ export default {
|
|||
|
||||
<ul class="menu-list">
|
||||
<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 }}
|
||||
<span v-if="hasUnread(c.ID)" class="tag is-success">
|
||||
{{ hasUnread(c.ID) }}
|
||||
|
@ -3716,9 +3777,7 @@ export default {
|
|||
<!-- Close new DMs toggle -->
|
||||
<div class="tag mt-2">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox"
|
||||
v-model="prefs.closeDMs"
|
||||
:value="true">
|
||||
<input type="checkbox" v-model="prefs.closeDMs" :value="true">
|
||||
Ignore unsolicited DMs
|
||||
|
||||
<a href="#"
|
||||
|
@ -3756,14 +3815,12 @@ export default {
|
|||
|
||||
<!-- Easy video zoom buttons -->
|
||||
<div class="column is-narrow is-hidden-mobile" v-if="anyVideosOpen">
|
||||
<button type="button" class="button is-small is-outlined"
|
||||
:disabled="webcam.videoScale === 'x4'"
|
||||
<button type="button" class="button is-small is-outlined" :disabled="webcam.videoScale === 'x4'"
|
||||
@click="scaleVideoSize(true)">
|
||||
<i class="fa fa-magnifying-glass-plus"></i>
|
||||
</button>
|
||||
|
||||
<button type="button" class="button is-small is-outlined"
|
||||
:disabled="webcam.videoScale === ''"
|
||||
<button type="button" class="button is-small is-outlined" :disabled="webcam.videoScale === ''"
|
||||
@click="scaleVideoSize(false)">
|
||||
<i class="fa fa-magnifying-glass-minus"></i>
|
||||
</button>
|
||||
|
@ -3773,13 +3830,14 @@ export default {
|
|||
<div class="column is-narrow" v-if="channel.indexOf('@') === 0">
|
||||
<!-- If the user has a profile URL -->
|
||||
<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>
|
||||
</button>
|
||||
|
||||
<!-- DMs: Leave convo button -->
|
||||
<button type="button"
|
||||
class="float-right button is-small is-warning is-outlined" @click="leaveDM()">
|
||||
<button type="button" class="float-right button is-small is-warning is-outlined"
|
||||
@click="leaveDM()">
|
||||
<i class="fa fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
@ -3798,56 +3856,41 @@ export default {
|
|||
<!-- Video Feeds-->
|
||||
|
||||
<!-- My video -->
|
||||
<VideoFeed
|
||||
v-show="webcam.active"
|
||||
:local-video="true"
|
||||
: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"
|
||||
<VideoFeed v-show="webcam.active" :local-video="true" :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">
|
||||
</VideoFeed>
|
||||
|
||||
<!-- Others' videos -->
|
||||
<VideoFeed
|
||||
v-for="(stream, username) in WebRTC.streams"
|
||||
v-bind:key="username"
|
||||
:username="username"
|
||||
:popped-out="WebRTC.poppedOut[username]"
|
||||
:is-explicit="isUsernameCamNSFW(username)"
|
||||
: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 v-for="(stream, username) in WebRTC.streams" v-bind:key="username" :username="username"
|
||||
:popped-out="WebRTC.poppedOut[username]" :is-explicit="isUsernameCamNSFW(username)"
|
||||
: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>
|
||||
|
||||
<!-- Debugging - copy a lot of these to simulate more videos -->
|
||||
|
||||
<!-- <div class="feed">
|
||||
hi
|
||||
</div>
|
||||
<div class="feed">
|
||||
hi
|
||||
</div>
|
||||
<div class="feed">
|
||||
hi
|
||||
</div>
|
||||
<div class="feed">
|
||||
hi
|
||||
</div> -->
|
||||
hi
|
||||
</div>
|
||||
<div class="feed">
|
||||
hi
|
||||
</div>
|
||||
<div class="feed">
|
||||
hi
|
||||
</div>
|
||||
<div class="feed">
|
||||
hi
|
||||
</div> -->
|
||||
|
||||
</div>
|
||||
<div class="card-content" id="chatHistory"
|
||||
:class="{ 'has-background-dm': isDM,
|
||||
'p-1 pb-5': messageStyle.indexOf('compact') === 0 }">
|
||||
<div class="card-content" id="chatHistory" :class="{
|
||||
'has-background-dm': isDM,
|
||||
'p-1 pb-5': messageStyle.indexOf('compact') === 0
|
||||
}">
|
||||
|
||||
<div class="autoscroll-field tag">
|
||||
<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">
|
||||
|
||||
<MessageBox
|
||||
:message="msg"
|
||||
:is-presence="msg.action === 'presence'"
|
||||
:appearance="messageStyle"
|
||||
:position="i"
|
||||
:user="getUser(msg.username)"
|
||||
:is-offline="isUserOffline(msg.username)"
|
||||
:username="username"
|
||||
: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>
|
||||
<MessageBox :message="msg" :is-presence="msg.action === 'presence'" :appearance="messageStyle"
|
||||
:position="i" :user="getUser(msg.username)" :is-offline="isUserOffline(msg.username)"
|
||||
:username="username" :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>
|
||||
|
||||
|
@ -3936,11 +3964,7 @@ export default {
|
|||
<form @submit.prevent="sendMessage()">
|
||||
|
||||
<!-- At Mentions -->
|
||||
<Mentionable
|
||||
:keys="['@']"
|
||||
:items="atMentionItems"
|
||||
offset="12"
|
||||
insert-space>
|
||||
<Mentionable :keys="['@']" :items="atMentionItems" offset="12" insert-space>
|
||||
|
||||
<!-- My text box -->
|
||||
<input type="text" class="input" id="messageBox" v-model="message"
|
||||
|
@ -3966,29 +3990,22 @@ export default {
|
|||
</form>
|
||||
</div>
|
||||
<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 -->
|
||||
<div class="dropdown-trigger">
|
||||
<button type="button" class="button"
|
||||
aria-haspopup="true"
|
||||
aria-controls="input-emoji-picker"
|
||||
<button type="button" class="button" aria-haspopup="true" aria-controls="input-emoji-picker"
|
||||
@click="hideEmojiPicker()">
|
||||
<span>
|
||||
<i class="fa-regular fa-smile"></i>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="dropdown-menu" id="input-emoji-picker" role="menu"
|
||||
style="z-index: 9000">
|
||||
<div class="dropdown-menu" id="input-emoji-picker" role="menu" style="z-index: 9000">
|
||||
<!-- 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">
|
||||
<EmojiPicker
|
||||
:native="true"
|
||||
:display-recent="true"
|
||||
:disable-skin-tones="true"
|
||||
theme="auto"
|
||||
@select="onSelectEmoji">
|
||||
<EmojiPicker :native="true" :display-recent="true" :disable-skin-tones="true"
|
||||
theme="auto" @select="onSelectEmoji">
|
||||
</EmojiPicker>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -4091,43 +4108,22 @@ export default {
|
|||
<!-- Who Is Online -->
|
||||
<ul class="menu-list" v-if="whoTab === 'online'">
|
||||
<li v-for="(u, i) in sortedWhoList" v-bind:key="i">
|
||||
<WhoListRow
|
||||
:user="u"
|
||||
:username="username"
|
||||
:website-url="config.website"
|
||||
:is-dnd="isUsernameDND(u.username)"
|
||||
: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>
|
||||
<WhoListRow :user="u" :username="username" :website-url="config.website"
|
||||
:is-dnd="isUsernameDND(u.username)" :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>
|
||||
</ul>
|
||||
|
||||
<!-- Watching My Webcam -->
|
||||
<ul class="menu-list" v-if="whoTab === 'watching'">
|
||||
<li v-for="(u, i) in sortedWatchingList" v-bind:key="username">
|
||||
<WhoListRow
|
||||
:is-watching-tab="true"
|
||||
:user="u"
|
||||
:username="username"
|
||||
:website-url="config.website"
|
||||
: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"
|
||||
<WhoListRow :is-watching-tab="true" :user="u" :username="username" :website-url="config.website"
|
||||
: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>
|
||||
</li>
|
||||
</ul>
|
||||
|
|
|
@ -32,11 +32,6 @@ export default {
|
|||
return this.localVideo ? 'localVideo' : `videofeed-${this.username}`;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
mouseOver() {
|
||||
console.log("mouse over:", this.mouseOver);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
closeVideo() {
|
||||
// Note: closeVideo only available for OTHER peoples cameras.
|
||||
|
@ -85,7 +80,7 @@ export default {
|
|||
<div class="feed" :class="{
|
||||
'popped-out': 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>
|
||||
|
||||
<!-- Caption -->
|
||||
|
@ -93,23 +88,19 @@ export default {
|
|||
'has-text-camera-blue': !isExplicit,
|
||||
'has-text-camera-red': isExplicit,
|
||||
}">
|
||||
<i class="fa fa-microphone-slash mr-1 has-text-grey"
|
||||
v-if="isSourceMuted"></i>
|
||||
<i class="fa fa-microphone-slash mr-1 has-text-grey" v-if="isSourceMuted"></i>
|
||||
{{ username }}
|
||||
<i class="fa fa-people-arrows ml-1 has-text-grey is-size-7"
|
||||
:title="username + ' is watching your camera too'"
|
||||
<i class="fa fa-people-arrows ml-1 has-text-grey is-size-7" :title="username + ' is watching your camera too'"
|
||||
v-if="isWatchingMe"></i>
|
||||
|
||||
<!-- Frozen stream detection -->
|
||||
<a class="fa fa-mountain ml-1" href="#" v-if="!localVideo && isFrozen"
|
||||
style="color: #00FFFF" @click.prevent="reopenVideo()"
|
||||
title="Frozen video detected!"></a>
|
||||
<a class="fa fa-mountain ml-1" href="#" v-if="!localVideo && isFrozen" style="color: #00FFFF"
|
||||
@click.prevent="reopenVideo()" title="Frozen video detected!"></a>
|
||||
</div>
|
||||
|
||||
<!-- Close button (others' videos only) -->
|
||||
<div class="close" v-if="!localVideo">
|
||||
<a href="#" class="has-text-danger" title="Close video"
|
||||
@click.prevent="closeVideo()">
|
||||
<a href="#" class="has-text-danger" title="Close video" @click.prevent="closeVideo()">
|
||||
<i class="fa fa-close"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
@ -117,31 +108,28 @@ export default {
|
|||
<!-- Controls -->
|
||||
<div class="controls">
|
||||
<!-- Mute Button -->
|
||||
<button type="button" v-if="!isMuted"
|
||||
class="button is-small is-success is-outlined ml-1 px-2"
|
||||
<button type="button" v-if="!isMuted" class="button is-small is-success is-outlined ml-1 px-2"
|
||||
@click="muteVideo()">
|
||||
<i class="fa"
|
||||
:class="{'fa-microphone': localVideo,
|
||||
'fa-volume-high': !localVideo}"></i>
|
||||
<i class="fa" :class="{
|
||||
'fa-microphone': localVideo,
|
||||
'fa-volume-high': !localVideo
|
||||
}"></i>
|
||||
</button>
|
||||
<button type="button" v-else
|
||||
class="button is-small is-danger ml-1 px-2"
|
||||
@click="muteVideo()">
|
||||
<i class="fa"
|
||||
:class="{'fa-microphone-slash': localVideo,
|
||||
'fa-volume-xmark': !localVideo}"></i>
|
||||
<button type="button" v-else class="button is-small is-danger ml-1 px-2" @click="muteVideo()">
|
||||
<i class="fa" :class="{
|
||||
'fa-microphone-slash': localVideo,
|
||||
'fa-volume-xmark': !localVideo
|
||||
}"></i>
|
||||
</button>
|
||||
|
||||
<!-- Pop-out Video -->
|
||||
<button type="button" class="button is-small is-light is-outlined p-2 ml-2"
|
||||
title="Pop out"
|
||||
<button type="button" class="button is-small is-light is-outlined p-2 ml-2" title="Pop out"
|
||||
@click="popoutVideo()">
|
||||
<i class="fa fa-up-right-from-square"></i>
|
||||
</button>
|
||||
|
||||
<!-- Full screen -->
|
||||
<button type="button" class="button is-small is-light is-outlined p-2 ml-2"
|
||||
title="Go full screen"
|
||||
<button type="button" class="button is-small is-light is-outlined p-2 ml-2" title="Go full screen"
|
||||
@click="fullscreen()">
|
||||
<i class="fa fa-expand"></i>
|
||||
</button>
|
||||
|
@ -149,16 +137,8 @@ export default {
|
|||
|
||||
<!-- Volume slider -->
|
||||
<div class="volume-slider" v-show="!localVideo && !isMuted && mouseOver">
|
||||
<Slider
|
||||
v-model="volume"
|
||||
color="#00FF00"
|
||||
track-color="#006600"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:step="1"
|
||||
:height="7"
|
||||
orientation="vertical"
|
||||
@change="volumeChanged">
|
||||
<Slider v-model="volume" color="#00FF00" track-color="#006600" :min="0" :max="100" :step="1" :height="7"
|
||||
orientation="vertical" @change="volumeChanged">
|
||||
|
||||
</Slider>
|
||||
</div>
|
||||
|
|
Loading…
Reference in New Issue
Block a user