Volume sliders, fullscreen video, misc tweaks
This commit is contained in:
parent
489f5b6aad
commit
2810169ce9
14
package-lock.json
generated
14
package-lock.json
generated
|
@ -12,7 +12,8 @@
|
||||||
"interactjs": "^1.10.18",
|
"interactjs": "^1.10.18",
|
||||||
"vue": "^3.3.4",
|
"vue": "^3.3.4",
|
||||||
"vue-mention": "^2.0.0-alpha.3",
|
"vue-mention": "^2.0.0-alpha.3",
|
||||||
"vue3-emoji-picker": "^1.1.7"
|
"vue3-emoji-picker": "^1.1.7",
|
||||||
|
"vue3-slider": "^1.9.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-vue": "^4.3.1",
|
"@vitejs/plugin-vue": "^4.3.1",
|
||||||
|
@ -2023,6 +2024,17 @@
|
||||||
"vue": "^3.2.23"
|
"vue": "^3.2.23"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/vue3-slider": {
|
||||||
|
"version": "1.9.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/vue3-slider/-/vue3-slider-1.9.0.tgz",
|
||||||
|
"integrity": "sha512-S+ojp3A21FiOM389Xp/bdUKSzjxcsTaDwI4N1aWVPoNh9//E8M+dqs1Ufjh86LulPAWLCN0ZR6eaD8jLZZ574g==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"vue": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/which": {
|
"node_modules/which": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||||
|
|
|
@ -14,7 +14,8 @@
|
||||||
"interactjs": "^1.10.18",
|
"interactjs": "^1.10.18",
|
||||||
"vue": "^3.3.4",
|
"vue": "^3.3.4",
|
||||||
"vue-mention": "^2.0.0-alpha.3",
|
"vue-mention": "^2.0.0-alpha.3",
|
||||||
"vue3-emoji-picker": "^1.1.7"
|
"vue3-emoji-picker": "^1.1.7",
|
||||||
|
"vue3-slider": "^1.9.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-vue": "^4.3.1",
|
"@vitejs/plugin-vue": "^4.3.1",
|
||||||
|
|
|
@ -21,7 +21,7 @@ type Server struct {
|
||||||
// NewServer initializes the Server.
|
// NewServer initializes the Server.
|
||||||
func NewServer() *Server {
|
func NewServer() *Server {
|
||||||
return &Server{
|
return &Server{
|
||||||
subscriberMessageBuffer: 16,
|
subscriberMessageBuffer: 32,
|
||||||
subscribers: make(map[*Subscriber]struct{}),
|
subscribers: make(map[*Subscriber]struct{}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
137
src/App.vue
137
src/App.vue
|
@ -10,6 +10,7 @@ import ExplicitOpenModal from './components/ExplicitOpenModal.vue';
|
||||||
import ReportModal from './components/ReportModal.vue';
|
import ReportModal from './components/ReportModal.vue';
|
||||||
import MessageBox from './components/MessageBox.vue';
|
import MessageBox from './components/MessageBox.vue';
|
||||||
import WhoListRow from './components/WhoListRow.vue';
|
import WhoListRow from './components/WhoListRow.vue';
|
||||||
|
import VideoFeed from './components/VideoFeed.vue';
|
||||||
|
|
||||||
import LocalStorage from './lib/LocalStorage';
|
import LocalStorage from './lib/LocalStorage';
|
||||||
import VideoFlag from './lib/VideoFlag';
|
import VideoFlag from './lib/VideoFlag';
|
||||||
|
@ -48,6 +49,7 @@ export default {
|
||||||
ReportModal,
|
ReportModal,
|
||||||
MessageBox,
|
MessageBox,
|
||||||
WhoListRow,
|
WhoListRow,
|
||||||
|
VideoFeed,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -2313,6 +2315,13 @@ export default {
|
||||||
$ref.muted = this.WebRTC.muted[username];
|
$ref.muted = this.WebRTC.muted[username];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
setVideoVolume(username, volume) {
|
||||||
|
// Set the volume on their video.
|
||||||
|
let $ref = document.getElementById(`videofeed-${username}`);
|
||||||
|
if ($ref) {
|
||||||
|
$ref.volume = volume / 100;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// Pop out a user's video.
|
// Pop out a user's video.
|
||||||
popoutVideo(username) {
|
popoutVideo(username) {
|
||||||
|
@ -3123,7 +3132,8 @@ 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">
|
||||||
{{ d.label || `Camera ${i}` }}
|
{{ d.label || `Camera ${i}` }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
|
@ -3135,7 +3145,8 @@ 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">
|
||||||
{{ d.label || `Microphone ${i}` }}
|
{{ d.label || `Microphone ${i}` }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
|
@ -3448,7 +3459,7 @@ export default {
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<p class="menu-label">
|
<p class="menu-label">
|
||||||
Private Messages
|
Direct Messages
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<ul class="menu-list">
|
<ul class="menu-list">
|
||||||
|
@ -3482,6 +3493,21 @@ export default {
|
||||||
</ul>
|
</ul>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
|
<!-- Close new DMs toggle -->
|
||||||
|
<div class="tag mt-2">
|
||||||
|
<label class="checkbox">
|
||||||
|
<input type="checkbox"
|
||||||
|
v-model="prefs.closeDMs"
|
||||||
|
:value="true">
|
||||||
|
Ignore unsolicited DMs
|
||||||
|
|
||||||
|
<a href="#"
|
||||||
|
onclick="alert('When checked, your DMs will be closed to new conversations. You may still initiate new DMs with others.'); return false"
|
||||||
|
class="fa fa-info-circle ml-2">
|
||||||
|
</a>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -3535,85 +3561,36 @@ export default {
|
||||||
<!-- Video Feeds-->
|
<!-- Video Feeds-->
|
||||||
|
|
||||||
<!-- My video -->
|
<!-- My video -->
|
||||||
<div class="feed" v-show="webcam.active" :class="{
|
<VideoFeed
|
||||||
'popped-out': WebRTC.poppedOut[username],
|
v-show="webcam.active"
|
||||||
'popped-in': !WebRTC.poppedOut[username]
|
:local-video="true"
|
||||||
}">
|
:username="username"
|
||||||
<video class="feed" id="localVideo" autoplay muted>
|
:popped-out="WebRTC.poppedOut[username]"
|
||||||
</video>
|
:is-explicit="webcam.nsfw"
|
||||||
|
:is-muted="webcam.muted"
|
||||||
<div class="caption" :class="{
|
:is-source-muted="webcam.muted"
|
||||||
'has-text-camera-blue': !webcam.nsfw,
|
@mute-video="muteMe()"
|
||||||
'has-text-camera-red': webcam.nsfw
|
@popout="popoutVideo"
|
||||||
}">
|
@set-volume="setVideoVolume">
|
||||||
<i class="fa fa-microphone-slash mr-1 has-text-grey" v-if="webcam.muted"></i>
|
</VideoFeed>
|
||||||
{{ username }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="controls">
|
|
||||||
<!-- MY Mute button -->
|
|
||||||
<button type="button" v-if="webcam.active && !webcam.muted"
|
|
||||||
class="button is-small is-success is-outlined ml-1 px-2" @click="muteMe()">
|
|
||||||
<i class="fa fa-microphone"></i>
|
|
||||||
</button>
|
|
||||||
<button type="button" v-if="webcam.active && webcam.muted"
|
|
||||||
class="button is-small is-danger ml-1 px-2" @click="muteMe()">
|
|
||||||
<i class="fa fa-microphone-slash"></i>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- Pop-out MY video -->
|
|
||||||
<button type="button" class="button is-small is-light is-outlined p-2 ml-2" title="Pop out"
|
|
||||||
@click="popoutVideo(username)">
|
|
||||||
<i class="fa fa-up-right-from-square"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Others' videos -->
|
<!-- Others' videos -->
|
||||||
<div class="feed" v-for="(stream, username) in WebRTC.streams" v-bind:key="username" :class="{
|
<VideoFeed
|
||||||
'popped-out': WebRTC.poppedOut[username],
|
v-for="(stream, username) in WebRTC.streams"
|
||||||
'popped-in': !WebRTC.poppedOut[username]
|
v-bind:key="username"
|
||||||
}">
|
:username="username"
|
||||||
<video class="feed" :id="'videofeed-' + username" autoplay>
|
:popped-out="WebRTC.poppedOut[username]"
|
||||||
</video>
|
:is-explicit="isUsernameCamNSFW(username)"
|
||||||
<div class="caption" :class="{
|
:is-source-muted="isSourceMuted(username)"
|
||||||
'has-text-camera-blue': !isUsernameCamNSFW(username),
|
:is-muted="isMuted(username)"
|
||||||
'has-text-camera-red': isUsernameCamNSFW(username)
|
:is-watching-me="isWatchingMe(username)"
|
||||||
}">
|
:is-frozen="WebRTC.frozenStreamDetected[username]"
|
||||||
<i class="fa fa-microphone-slash mr-1 has-text-grey" v-if="isSourceMuted(username)"></i>
|
@reopen-video="openVideoByUsername"
|
||||||
{{ username }}
|
@mute-video="muteVideo"
|
||||||
<i class="fa fa-people-arrows ml-1 has-text-grey is-size-7"
|
@popout="popoutVideo"
|
||||||
:title="username + ' is watching your camera too'" v-if="isWatchingMe(username)"></i>
|
@close-video="closeVideo"
|
||||||
|
@set-volume="setVideoVolume">
|
||||||
<!-- Frozen stream detection -->
|
</VideoFeed>
|
||||||
<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 class="close">
|
|
||||||
<a href="#" class="has-text-danger" title="Close video"
|
|
||||||
@click.prevent="closeVideo(username, 'offerer')">
|
|
||||||
<i class="fa fa-close"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="controls">
|
|
||||||
<!-- Mute button -->
|
|
||||||
<button type="button" v-if="isMuted(username)" class="button is-small is-danger p-2"
|
|
||||||
title="Unmute this video" @click="muteVideo(username)">
|
|
||||||
<i class="fa fa-volume-xmark"></i>
|
|
||||||
</button>
|
|
||||||
<button type="button" v-else class="button is-small is-success is-outlined p-2"
|
|
||||||
title="Mute this video" @click="muteVideo(username)">
|
|
||||||
<i class="fa fa-volume-high"></i>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- Pop-out -->
|
|
||||||
<button type="button" class="button is-small is-light is-outlined p-2 ml-2" title="Pop out"
|
|
||||||
@click="popoutVideo(username)">
|
|
||||||
<i class="fa fa-up-right-from-square"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Debugging - copy a lot of these to simulate more videos -->
|
<!-- Debugging - copy a lot of these to simulate more videos -->
|
||||||
|
|
||||||
|
|
167
src/components/VideoFeed.vue
Normal file
167
src/components/VideoFeed.vue
Normal file
|
@ -0,0 +1,167 @@
|
||||||
|
<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
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
Slider,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
// Volume slider
|
||||||
|
volume: 100,
|
||||||
|
|
||||||
|
// Volume change debounce
|
||||||
|
volumeDebounce: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
videoID() {
|
||||||
|
return this.localVideo ? 'localVideo' : `videofeed-${this.username}`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Toggle the Mute button
|
||||||
|
muteVideo() {
|
||||||
|
this.$emit('mute-video', this.username);
|
||||||
|
},
|
||||||
|
|
||||||
|
popoutVideo() {
|
||||||
|
this.$emit('popout', this.username);
|
||||||
|
},
|
||||||
|
|
||||||
|
fullscreen() {
|
||||||
|
let $elem = document.getElementById(this.videoID);
|
||||||
|
if ($elem) {
|
||||||
|
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);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="feed" :class="{
|
||||||
|
'popped-out': poppedOut,
|
||||||
|
'popped-in': !poppedOut,
|
||||||
|
}">
|
||||||
|
<video class="feed" :id="videoID" autoplay :muted="localVideo"></video>
|
||||||
|
|
||||||
|
<!-- Caption -->
|
||||||
|
<div class="caption" :class="{
|
||||||
|
'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>
|
||||||
|
{{ username }}
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Close button (others' videos only) -->
|
||||||
|
<div class="close" v-if="!localVideo">
|
||||||
|
<a href="#" class="has-text-danger" title="Close video"
|
||||||
|
@click.prevent="closeVideo()">
|
||||||
|
<i class="fa fa-close"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Controls -->
|
||||||
|
<div class="controls">
|
||||||
|
<!-- Mute Button -->
|
||||||
|
<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>
|
||||||
|
</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>
|
||||||
|
|
||||||
|
<!-- Pop-out Video -->
|
||||||
|
<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"
|
||||||
|
@click="fullscreen()">
|
||||||
|
<i class="fa fa-expand"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Volume slider -->
|
||||||
|
<div class="volume-slider" v-if="!localVideo && !isMuted">
|
||||||
|
<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;
|
||||||
|
}
|
||||||
|
</style>
|
Loading…
Reference in New Issue
Block a user