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",
|
||||
"vue": "^3.3.4",
|
||||
"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": {
|
||||
"@vitejs/plugin-vue": "^4.3.1",
|
||||
|
@ -2023,6 +2024,17 @@
|
|||
"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": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
|
|
|
@ -14,7 +14,8 @@
|
|||
"interactjs": "^1.10.18",
|
||||
"vue": "^3.3.4",
|
||||
"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": {
|
||||
"@vitejs/plugin-vue": "^4.3.1",
|
||||
|
|
|
@ -21,7 +21,7 @@ type Server struct {
|
|||
// NewServer initializes the Server.
|
||||
func NewServer() *Server {
|
||||
return &Server{
|
||||
subscriberMessageBuffer: 16,
|
||||
subscriberMessageBuffer: 32,
|
||||
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 MessageBox from './components/MessageBox.vue';
|
||||
import WhoListRow from './components/WhoListRow.vue';
|
||||
import VideoFeed from './components/VideoFeed.vue';
|
||||
|
||||
import LocalStorage from './lib/LocalStorage';
|
||||
import VideoFlag from './lib/VideoFlag';
|
||||
|
@ -48,6 +49,7 @@ export default {
|
|||
ReportModal,
|
||||
MessageBox,
|
||||
WhoListRow,
|
||||
VideoFeed,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -2313,6 +2315,13 @@ export default {
|
|||
$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.
|
||||
popoutVideo(username) {
|
||||
|
@ -3123,7 +3132,8 @@ 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">
|
||||
<option v-for="(d, i) in webcam.videoDevices" :value="d.id"
|
||||
v-bind:key="i">
|
||||
{{ d.label || `Camera ${i}` }}
|
||||
</option>
|
||||
</select>
|
||||
|
@ -3135,7 +3145,8 @@ 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">
|
||||
<option v-for="(d, i) in webcam.audioDevices" :value="d.id"
|
||||
v-bind:key="i">
|
||||
{{ d.label || `Microphone ${i}` }}
|
||||
</option>
|
||||
</select>
|
||||
|
@ -3448,7 +3459,7 @@ export default {
|
|||
</ul>
|
||||
|
||||
<p class="menu-label">
|
||||
Private Messages
|
||||
Direct Messages
|
||||
</p>
|
||||
|
||||
<ul class="menu-list">
|
||||
|
@ -3482,6 +3493,21 @@ export default {
|
|||
</ul>
|
||||
</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>
|
||||
|
@ -3535,85 +3561,36 @@ export default {
|
|||
<!-- Video Feeds-->
|
||||
|
||||
<!-- My video -->
|
||||
<div class="feed" v-show="webcam.active" :class="{
|
||||
'popped-out': WebRTC.poppedOut[username],
|
||||
'popped-in': !WebRTC.poppedOut[username]
|
||||
}">
|
||||
<video class="feed" id="localVideo" autoplay muted>
|
||||
</video>
|
||||
|
||||
<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" v-if="webcam.muted"></i>
|
||||
{{ 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>
|
||||
<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 -->
|
||||
<div class="feed" v-for="(stream, username) in WebRTC.streams" v-bind:key="username" :class="{
|
||||
'popped-out': WebRTC.poppedOut[username],
|
||||
'popped-in': !WebRTC.poppedOut[username]
|
||||
}">
|
||||
<video class="feed" :id="'videofeed-' + username" autoplay>
|
||||
</video>
|
||||
<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" v-if="isSourceMuted(username)"></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(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 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>
|
||||
<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="closeVideo"
|
||||
@set-volume="setVideoVolume">
|
||||
</VideoFeed>
|
||||
|
||||
<!-- 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