Fix webcam freezing issues with mutualOpen video connections

polling-api
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.
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
}

View File

@ -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.

View File

@ -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()

View File

@ -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>

View File

@ -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>