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.
|
// 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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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()
|
||||||
|
|
300
src/App.vue
300
src/App.vue
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user