Video freeze detection + Other tweaks

* Color code usernames on video windows to be blue or red depending on
  their local cam explicit setting
* Attempt to detect freezes on RTCPeerConnection videos by registering a
  video onmute handler. If a freeze is detected, show a cyan mountain
  icon by their name. Clicking the icon will re-connect their video.
* Update the video buttons on the Who List to always re-connect video
  instead of toggling it opened and closed. The X buttons on videos are
  now how you close a video.
This commit is contained in:
Noah 2023-08-08 17:51:52 -07:00
parent 37360211e7
commit b3d4b375ed
3 changed files with 98 additions and 4 deletions

View File

@ -28,6 +28,14 @@ body {
text-overflow: ellipsis; text-overflow: ellipsis;
} }
/* Color coded webcam usernames per camera setting */
.has-text-camera-blue {
color: #00ccff !important;
}
.has-text-camera-red {
color: #ff9999 !important;
}
/************************ /************************
* Main CSS Grid Layout * * Main CSS Grid Layout *
************************/ ************************/

View File

@ -161,6 +161,13 @@ const app = Vue.createApp({
// RTCPeerConnections per username. // RTCPeerConnections per username.
pc: {}, pc: {},
// Video stream freeze detection.
frozenStreamInterval: {}, // map usernames to intervals
frozenStreamDetected: {}, // map usernames to bools
// Debounce connection attempts since now every click = try to connect.
debounceOpens: {}, // map usernames to bools
}, },
// Chat history. // Chat history.
@ -398,6 +405,7 @@ const app = Vue.createApp({
myVideoFlag() { myVideoFlag() {
// Compute the current user's video status flags. // Compute the current user's video status flags.
let status = 0; let status = 0;
if (!this.webcam.active) return 0; // unset all flags if not active now
if (this.webcam.active) status |= this.VideoFlag.Active; if (this.webcam.active) status |= this.VideoFlag.Active;
if (this.webcam.muted) status |= this.VideoFlag.Muted; if (this.webcam.muted) status |= this.VideoFlag.Muted;
if (this.webcam.nsfw) status |= this.VideoFlag.NSFW; if (this.webcam.nsfw) status |= this.VideoFlag.NSFW;
@ -507,6 +515,16 @@ const app = Vue.createApp({
return; return;
} }
// DEBUGGING: fake set the freeze indicator.
let match = this.message.match(/^\/freeze (.+?)$/i);
if (match) {
let username = match[1];
this.WebRTC.frozenStreamDetected[username] = true;
this.ChatClient(`DEBUG: Marked ${username} stream as frozen.`);
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",
@ -987,6 +1005,28 @@ const app = Vue.createApp({
// Inform them they are being watched. // Inform them they are being watched.
this.sendWatch(username, true); this.sendWatch(username, true);
// Set a mute video handler to detect freezes.
stream.getVideoTracks().forEach(videoTrack => {
let freezeDetected = () => {
console.log("FREEZE DETECTED:", username);
// Wait 3 seconds to see if the stream has recovered on its own
setTimeout(() => {
// Flag it as likely frozen.
if (videoTrack.muted) {
this.WebRTC.frozenStreamDetected[username] = true;
}
}, 3000);
};
console.log("Apply onmute handler for", username);
videoTrack.onmute = freezeDetected;
// Double check for frozen streams on an interval.
this.WebRTC.frozenStreamInterval[username] = setInterval(() => {
if (videoTrack.muted) freezeDetected();
}, 3000);
})
}; };
// If we were already broadcasting video, send our stream to // If we were already broadcasting video, send our stream to
@ -1212,6 +1252,14 @@ const app = Vue.createApp({
} }
return username; return username;
}, },
isUsernameCamNSFW(username) {
// returns true if the username is broadcasting and NSFW, false otherwise.
// (used for the color coding of their nickname on their video box - so assumed they are broadcasting)
if (this.whoMap[username] != undefined && this.whoMap[username].video & this.VideoFlag.NSFW) {
return true;
}
return false;
},
leaveDM() { leaveDM() {
// Validate we're in a DM currently. // Validate we're in a DM currently.
if (this.channel.indexOf("@") !== 0) return; if (this.channel.indexOf("@") !== 0) return;
@ -1407,7 +1455,21 @@ const app = Vue.createApp({
}, },
// Begin connecting to someone else's webcam. // Begin connecting to someone else's webcam.
openVideoByUsername(username, force) {
if (this.whoMap[username] != undefined) {
this.openVideo(this.whoMap[username], force);
return;
}
this.ChatClient("Couldn't open video by username: not found.");
},
openVideo(user, force) { openVideo(user, force) {
// Debounce so we don't spam too much for the same user.
if (this.WebRTC.debounceOpens[user.username]) return;
this.WebRTC.debounceOpens[user.username] = true;
setTimeout(() => {
delete(this.WebRTC.debounceOpens[user.username]);
}, 5000);
if (user.username === this.username) { if (user.username === this.username) {
this.ChatClient("You can already see your own webcam."); this.ChatClient("You can already see your own webcam.");
return; return;
@ -1434,7 +1496,6 @@ const app = Vue.createApp({
// Camera is already open? Then disconnect the connection. // Camera is already open? Then disconnect the connection.
if (this.WebRTC.pc[user.username] != undefined && this.WebRTC.pc[user.username].offerer != undefined) { if (this.WebRTC.pc[user.username] != undefined && this.WebRTC.pc[user.username].offerer != undefined) {
this.closeVideo(user.username, "offerer"); this.closeVideo(user.username, "offerer");
return;
} }
// If this user requests mutual viewership... // If this user requests mutual viewership...
@ -1466,6 +1527,13 @@ const app = Vue.createApp({
delete (this.WebRTC.pc[username]); delete (this.WebRTC.pc[username]);
} }
// Clean up any lingering camera freeze states.
delete (this.WebRTC.frozenStreamDetected[username]);
if (this.WebRTC.frozenStreamInterval[username]) {
clearInterval(this.WebRTC.frozenStreamInterval);
delete(this.WebRTC.frozenStreamInterval[username]);
}
// Inform backend we have closed it. // Inform backend we have closed it.
this.sendWatch(username, false); this.sendWatch(username, false);
return; return;
@ -1493,6 +1561,13 @@ const app = Vue.createApp({
delete (this.WebRTC.poppedOut[username]); delete (this.WebRTC.poppedOut[username]);
} }
// Clean up any lingering camera freeze states.
delete (this.WebRTC.frozenStreamDetected[username]);
if (this.WebRTC.frozenStreamInterval[username]) {
clearInterval(this.WebRTC.frozenStreamInterval);
delete(this.WebRTC.frozenStreamInterval[username]);
}
// Inform backend we have closed it. // Inform backend we have closed it.
this.sendWatch(username, false); this.sendWatch(username, false);
}, },

View File

@ -591,7 +591,9 @@
autoplay muted> autoplay muted>
</video> </video>
<div class="caption"> <div class="caption"
:class="{'has-text-camera-blue': !webcam.nsfw,
'has-text-camera-red': webcam.nsfw}">
<i class="fa fa-microphone-slash mr-1 has-text-grey" <i class="fa fa-microphone-slash mr-1 has-text-grey"
v-if="webcam.muted"></i> v-if="webcam.muted"></i>
[[username]] [[username]]
@ -631,19 +633,28 @@
:id="'videofeed-'+username" :id="'videofeed-'+username"
autoplay> autoplay>
</video> </video>
<div class="caption"> <div class="caption"
:class="{'has-text-camera-blue': !isUsernameCamNSFW(username),
'has-text-camera-red': isUsernameCamNSFW(username)}">
<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(username)"></i> v-if="isSourceMuted(username)"></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(username)"></i> v-if="isWatchingMe(username)"></i>
<!-- Frozen stream detection -->
<a class="fa fa-mountain ml-1" href="#"
v-if="WebRTC.frozenStreamDetected[username]"
style="color: #00FFFF"
@click.prevent="openVideoByUsername(username, true)"
title="Frozen video detected!"></a>
</div> </div>
<div class="close"> <div class="close">
<a href="#" <a href="#"
class="has-text-danger" class="has-text-danger"
title="Close video" title="Close video"
@click="closeVideo(username, 'offerer')"> @click.prevent="closeVideo(username, 'offerer')">
<i class="fa fa-close"></i> <i class="fa fa-close"></i>
</a> </a>
</div> </div>