BareRTC/src/components/VideoFeed.vue

272 lines
8.9 KiB
Vue

<script>
import Slider from 'vue3-slider';
export default {
props: {
localVideo: Boolean, // is our local webcam (not other's camera)
poppedOut: Boolean, // Video is popped-out and draggable
username: String, // username related to this video
isExplicit: Boolean, // camera is marked Explicit
isMuted: Boolean, // camera is muted on our end
isSourceMuted: Boolean, // camera is muted on the broadcaster's end
isWatchingMe: Boolean, // other video is watching us back
isFrozen: Boolean, // video is detected as frozen
isSpeaking: Boolean, // video is registering audio
watermarkImage: Image, // watermark image to overlay (nullable)
},
components: {
Slider,
},
data() {
return {
// Volume slider
volume: 100,
// Volume change debounce
volumeDebounce: null,
// Mouse over status
mouseOver: false,
};
},
computed: {
containerID() {
return this.videoID + '-container';
},
videoID() {
return this.localVideo ? 'localVideo' : `videofeed-${this.username}`;
},
textColorClass() {
return this.isExplicit ? 'has-text-camera-red' : 'has-text-camera-blue';
},
muteButtonClass() {
let classList = [
'button is-small ml-1 p-2',
]
if (this.isMuted) {
classList.push('is-danger');
} else {
classList.push('is-success is-outlined');
}
if (!this.mouseOver) {
classList.push('seethru');
}
return classList.join(' ');
},
muteIconClass() {
if (this.localVideo) {
return this.isMuted ? 'fa-microphone-slash' : 'fa-microphone';
}
return this.isMuted ? 'fa-volume-xmark' : 'fa-volume-high';
}
},
methods: {
closeVideo() {
// Note: closeVideo only available for OTHER peoples cameras.
// Closes the WebRTC connection as the offerer.
this.$emit('close-video', this.username, 'offerer');
},
reopenVideo() {
// Note: goes into openVideo(username, force)
this.$emit('reopen-video', this.username, true);
},
openProfile() {
this.$emit('open-profile', this.username);
},
// Toggle the Mute button
muteVideo() {
this.$emit('mute-video', this.username);
},
popoutVideo() {
this.$emit('popout', this.username);
},
fullscreen(force=false) {
// If we are popped-out, pop back in before full screen.
if (this.poppedOut && !force) {
this.popoutVideo();
window.requestAnimationFrame(() => {
this.fullscreen(true);
});
return;
}
let $elem = document.getElementById(this.containerID);
if ($elem) {
if (document.fullscreenElement) {
document.exitFullscreen();
} else if ($elem.requestFullscreen) {
$elem.requestFullscreen();
} else {
window.alert("Fullscreen not supported by your browser.");
}
}
},
volumeChanged() {
if (this.volumeDebounce !== null) {
clearTimeout(this.volumeDebounce);
}
this.volumeDebounce = setTimeout(() => {
this.$emit('set-volume', this.username, this.volume);
}, 200);
},
// Show info about the watermark when the corner one is clicked.
showWatermarkInfo() {
this.$emit('modal-alert', {
icon: "fa-solid fa-qrcode",
title: "What are the QR codes on webcams?",
message: "This QR code is a safety feature to deter people from wanting to screen record webcams on chat. " +
"The QR code contains the current viewer's username, the website's name, and the current date/time. The " +
"idea is that if a webcam is recorded and then leaked online, the QR code would connect it directly back to who exactly " +
"recorded it, and when.\n\n" +
"There are two QR codes on each video: the one in the bottom-right corner is intentionally made visible and obvious " +
"to everybody, and a second (subtle) copy of the code spans across the center/middle of the video and pulsates in " +
"transparency over time.",
});
}
}
}
</script>
<template>
<div class="feed" :id="containerID" :class="{
'popped-out': poppedOut,
'popped-in': !poppedOut,
}" @mouseover="mouseOver = true" @mouseleave="mouseOver = false">
<video
:id="videoID"
autoplay
disablepictureinpicture
playsinline
oncontextmenu="return false;"
:muted="localVideo"></video>
<!-- Watermark layer -->
<div v-if="watermarkImage">
<img :src="watermarkImage" class="watermark" oncontextmenu="return false">
<img :src="watermarkImage" class="corner-watermark seethru invert-color" @click="showWatermarkInfo()" oncontextmenu="return false">
</div>
<!-- Caption -->
<div class="caption" :class="textColorClass">
<i class="fa fa-microphone-slash mr-1 has-text-grey" v-if="isSourceMuted"></i>
<a href="#" @click.prevent="openProfile" :class="textColorClass">{{ username }}</a>
<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>
<!-- Is speaking -->
<span v-if="isSpeaking" class="ml-1" title="Speaking">
<i class="fa fa-volume-high has-text-info"></i>
</span>
</div>
<!-- Close button (others' videos only) -->
<div class="close" v-if="!localVideo" :class="{'seethru': !mouseOver}">
<a href="#" class="button is-small is-danger is-outlined px-2" title="Close video" @click.prevent="closeVideo()">
<i class="fa fa-close"></i>
</a>
</div>
<!-- Controls -->
<div class="controls">
<!-- Mute Button -->
<button type="button" :class="muteButtonClass"
@click="muteVideo()">
<i class="fa" :class="muteIconClass"></i>
</button>
<!-- Pop-out Video -->
<button type="button" class="button is-small is-light is-outlined p-2 ml-2" title="Pop out"
:class="{'seethru': !mouseOver}"
@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"
:class="{'seethru': !mouseOver}"
@click="fullscreen()">
<i class="fa fa-expand"></i>
</button>
</div>
<!-- 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>
</div>
</div>
</template>
<style scoped>
.volume-slider {
position: absolute;
left: 18px;
top: 30px;
bottom: 44px;
}
/* A background image behind video elements in case they don't load properly */
video {
background-image: url(/static/img/connection-error.png);
background-position: center center;
background-repeat: no-repeat;
}
/* Translucent controls until mouse over */
.seethru {
opacity: 0.4;
}
/* Watermark image */
.watermark {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: auto;
width: 40%;
height: 40%;
opacity: 0.02;
animation-name: subtle-pulsate;
animation-duration: 10s;
animation-iteration-count: infinite;
}
.corner-watermark {
position: absolute;
right: 4px;
bottom: 4px;
width: 20%;
min-width: 32px;
min-height: 32px;
max-height: 20%;
cursor: pointer;
}
.invert-color {
filter: invert(100%);
}
/* Animate the primary watermark to pulsate in opacity */
@keyframes subtle-pulsate {
0% { opacity: 0.02; }
50% { opacity: 0.06; }
100% { opacity: 0.02; }
}
</style>