live change resolution (WIP)
This commit is contained in:
121
client/src/components/resolution.vue
Normal file
121
client/src/components/resolution.vue
Normal file
@ -0,0 +1,121 @@
|
||||
<template>
|
||||
<vue-context class="context" ref="context">
|
||||
<template v-for="(conf, i) in configurations">
|
||||
<li v-for="(rate, j) in conf.rates" :key="`${i}-${j}`" @click="screenSet(conf.width, conf.height, rate)">
|
||||
<i class="fas fa-desktop"></i>
|
||||
<span>{{ conf.width }}x{{ conf.height }}</span>
|
||||
<small>{{ rate }}</small>
|
||||
</li>
|
||||
</template>
|
||||
</vue-context>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.context {
|
||||
background-color: $background-floating;
|
||||
background-clip: padding-box;
|
||||
border-radius: 0.25rem;
|
||||
display: block;
|
||||
margin: 0;
|
||||
padding: 5px;
|
||||
min-width: 150px;
|
||||
z-index: 1500;
|
||||
position: fixed;
|
||||
list-style: none;
|
||||
box-sizing: border-box;
|
||||
max-height: calc(100% - 50px);
|
||||
overflow-y: auto;
|
||||
color: $interactive-normal;
|
||||
user-select: none;
|
||||
box-shadow: $elevation-high;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: $background-secondary transparent;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: $background-secondary;
|
||||
border: 2px solid $background-floating;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background-color: $background-floating;
|
||||
}
|
||||
|
||||
> li {
|
||||
margin: 0;
|
||||
position: relative;
|
||||
align-content: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: 8px 5px;
|
||||
cursor: pointer;
|
||||
|
||||
i {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
span {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
small {
|
||||
font-size: 0.7em;
|
||||
justify-self: flex-end;
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
text-decoration: none;
|
||||
background-color: $background-modifier-hover;
|
||||
color: $interactive-hover;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Ref, Watch, Vue } from 'vue-property-decorator'
|
||||
import { Member } from '~/neko/types'
|
||||
|
||||
// @ts-ignore
|
||||
import { VueContext } from 'vue-context'
|
||||
|
||||
@Component({
|
||||
name: 'neko-resolution',
|
||||
components: {
|
||||
'vue-context': VueContext,
|
||||
},
|
||||
})
|
||||
export default class extends Vue {
|
||||
@Ref('context') readonly context!: any
|
||||
|
||||
get configurations() {
|
||||
return this.$accessor.video.configurations
|
||||
}
|
||||
|
||||
open(event: MouseEvent) {
|
||||
this.context.open(event)
|
||||
}
|
||||
|
||||
screenSet(width: number, height: number, rate: number) {
|
||||
this.$accessor.video.screenSet({ width, height, rate })
|
||||
}
|
||||
}
|
||||
</script>
|
@ -28,7 +28,11 @@
|
||||
</div>
|
||||
<div ref="aspect" class="player-aspect" />
|
||||
</div>
|
||||
<i v-if="!fullscreen" @click.stop.prevent="requestFullscreen" class="expand fas fa-expand"></i>
|
||||
<ul v-if="!fullscreen" class="video-menu">
|
||||
<li><i @click.stop.prevent="requestFullscreen" class="fas fa-expand"></i></li>
|
||||
<li v-if="admin"><i @click.stop.prevent="onResolution" class="fas fa-cog"></i></li>
|
||||
</ul>
|
||||
<neko-resolution ref="resolution" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -44,19 +48,26 @@
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.expand {
|
||||
.video-menu {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
top: 15px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
background: rgba($color: #fff, $alpha: 0.2);
|
||||
border-radius: 5px;
|
||||
line-height: 30px;
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
color: rgba($color: #fff, $alpha: 0.6);
|
||||
cursor: pointer;
|
||||
|
||||
li {
|
||||
margin: 0 0 10px 0;
|
||||
|
||||
i {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
background: rgba($color: #fff, $alpha: 0.2);
|
||||
border-radius: 5px;
|
||||
line-height: 30px;
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
color: rgba($color: #fff, $alpha: 0.6);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.player-container {
|
||||
@ -129,11 +140,13 @@
|
||||
import ResizeObserver from 'resize-observer-polyfill'
|
||||
|
||||
import Emote from './emote.vue'
|
||||
import Resolution from './resolution.vue'
|
||||
|
||||
@Component({
|
||||
name: 'neko-video',
|
||||
components: {
|
||||
'neko-emote': Emote,
|
||||
'neko-resolution': Resolution,
|
||||
},
|
||||
})
|
||||
export default class extends Vue {
|
||||
@ -143,11 +156,16 @@
|
||||
@Ref('aspect') readonly _aspect!: HTMLElement
|
||||
@Ref('player') readonly _player!: HTMLElement
|
||||
@Ref('video') readonly _video!: HTMLVideoElement
|
||||
@Ref('resolution') readonly _resolution!: any
|
||||
|
||||
private observer = new ResizeObserver(this.onResise.bind(this))
|
||||
private focused = false
|
||||
private fullscreen = false
|
||||
|
||||
get admin() {
|
||||
return this.$accessor.user.admin
|
||||
}
|
||||
|
||||
get connected() {
|
||||
return this.$accessor.connected
|
||||
}
|
||||
@ -200,6 +218,38 @@
|
||||
return this.$accessor.remote.clipboard
|
||||
}
|
||||
|
||||
get width() {
|
||||
return this.$accessor.video.width
|
||||
}
|
||||
|
||||
get height() {
|
||||
return this.$accessor.video.height
|
||||
}
|
||||
|
||||
get rate() {
|
||||
return this.$accessor.video.rate
|
||||
}
|
||||
|
||||
get vertical() {
|
||||
return this.$accessor.video.vertical
|
||||
}
|
||||
|
||||
get horizontal() {
|
||||
return this.$accessor.video.horizontal
|
||||
}
|
||||
|
||||
@Watch('width')
|
||||
onWidthChanged(width: number) {
|
||||
const { videoWidth, videoHeight } = this._video
|
||||
console.log({ videoWidth, videoHeight })
|
||||
this.onResise()
|
||||
}
|
||||
|
||||
@Watch('height')
|
||||
onHeightChanged(height: number) {
|
||||
this.onResise()
|
||||
}
|
||||
|
||||
@Watch('volume')
|
||||
onVolumeChanged(volume: number) {
|
||||
if (this._video) {
|
||||
@ -292,8 +342,6 @@
|
||||
this._video
|
||||
.play()
|
||||
.then(() => {
|
||||
const { videoWidth, videoHeight } = this._video
|
||||
this.$accessor.video.setResolution({ width: videoWidth, height: videoHeight })
|
||||
this.onResise()
|
||||
})
|
||||
.catch(err => console.log(err))
|
||||
@ -418,8 +466,6 @@
|
||||
}
|
||||
|
||||
onResise() {
|
||||
const { horizontal, vertical } = this.$accessor.video
|
||||
|
||||
let height = 0
|
||||
if (!this.fullscreen) {
|
||||
const { offsetWidth, offsetHeight } = this._component
|
||||
@ -431,8 +477,12 @@
|
||||
height = offsetHeight
|
||||
}
|
||||
|
||||
this._container.style.maxWidth = `${(horizontal / vertical) * height}px`
|
||||
this._aspect.style.paddingBottom = `${(vertical / horizontal) * 100}%`
|
||||
this._container.style.maxWidth = `${(this.horizontal / this.vertical) * height}px`
|
||||
this._aspect.style.paddingBottom = `${(this.vertical / this.horizontal) * 100}%`
|
||||
}
|
||||
|
||||
onResolution(event: MouseEvent) {
|
||||
this._resolution.open(event)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -36,6 +36,11 @@ export const EVENT = {
|
||||
MESSAGE: 'chat/message',
|
||||
EMOTE: 'chat/emote',
|
||||
},
|
||||
SCREEN: {
|
||||
CONFIGURATIONS: 'screen/configurations',
|
||||
RESOLUTION: 'screen/resolution',
|
||||
SET: 'screen/set',
|
||||
},
|
||||
ADMIN: {
|
||||
BAN: 'admin/ban',
|
||||
KICK: 'admin/kick',
|
||||
@ -58,6 +63,7 @@ export type WebSocketEvents =
|
||||
| MemberEvents
|
||||
| SignalEvents
|
||||
| ChatEvents
|
||||
| ScreenEvents
|
||||
| AdminEvents
|
||||
|
||||
export type ControlEvents =
|
||||
@ -72,6 +78,8 @@ export type IdentityEvents = typeof EVENT.IDENTITY.PROVIDE | typeof EVENT.IDENTI
|
||||
export type MemberEvents = typeof EVENT.MEMBER.LIST | typeof EVENT.MEMBER.CONNECTED | typeof EVENT.MEMBER.DISCONNECTED
|
||||
export type SignalEvents = typeof EVENT.SIGNAL.ANSWER | typeof EVENT.SIGNAL.PROVIDE
|
||||
export type ChatEvents = typeof EVENT.CHAT.MESSAGE | typeof EVENT.CHAT.EMOTE
|
||||
export type ScreenEvents = typeof EVENT.SCREEN.CONFIGURATIONS | typeof EVENT.SCREEN.RESOLUTION | typeof EVENT.SCREEN.SET
|
||||
|
||||
export type AdminEvents =
|
||||
| typeof EVENT.ADMIN.BAN
|
||||
| typeof EVENT.ADMIN.KICK
|
||||
|
@ -15,9 +15,11 @@ import {
|
||||
ControlTargetPayload,
|
||||
ChatPayload,
|
||||
EmotePayload,
|
||||
ControlClipboardPayload,
|
||||
ScreenConfigurationsPayload,
|
||||
ScreenResolutionPayload,
|
||||
AdminPayload,
|
||||
AdminTargetPayload,
|
||||
ControlClipboardPayload,
|
||||
} from './messages'
|
||||
|
||||
interface NekoEvents extends BaseEvents {}
|
||||
@ -285,6 +287,33 @@ export class NekoClient extends BaseClient implements EventEmitter<NekoEvents> {
|
||||
this.$accessor.chat.newEmote({ type: emote })
|
||||
}
|
||||
|
||||
/////////////////////////////
|
||||
// Screen Events
|
||||
/////////////////////////////
|
||||
protected [EVENT.SCREEN.CONFIGURATIONS]({ configurations }: ScreenConfigurationsPayload) {
|
||||
this.$accessor.video.setConfigurations(configurations)
|
||||
}
|
||||
|
||||
protected [EVENT.SCREEN.RESOLUTION]({ id, width, height, rate }: ScreenResolutionPayload) {
|
||||
this.$accessor.video.setResolution({ width, height, rate })
|
||||
|
||||
if (!id) {
|
||||
return
|
||||
}
|
||||
|
||||
const member = this.member(id)
|
||||
if (!member || member.ignored) {
|
||||
return
|
||||
}
|
||||
|
||||
this.$accessor.chat.newMessage({
|
||||
id,
|
||||
content: `chaned the resolution to ${width}x${height}@${rate}`,
|
||||
type: 'event',
|
||||
created: new Date(),
|
||||
})
|
||||
}
|
||||
|
||||
/////////////////////////////
|
||||
// Admin Events
|
||||
/////////////////////////////
|
||||
|
@ -7,9 +7,10 @@ import {
|
||||
MemberEvents,
|
||||
SignalEvents,
|
||||
ChatEvents,
|
||||
ScreenEvents,
|
||||
AdminEvents,
|
||||
} from './events'
|
||||
import { Member } from './types'
|
||||
import { Member, ScreenConfigurations } from './types'
|
||||
|
||||
export type WebSocketMessages =
|
||||
| WebSocketMessage
|
||||
@ -19,6 +20,8 @@ export type WebSocketMessages =
|
||||
| MembeConnectMessage
|
||||
| MembeDisconnectMessage
|
||||
| ControlMessage
|
||||
| ScreenResolutionMessage
|
||||
| ScreenConfigurationsMessage
|
||||
| ChatMessage
|
||||
|
||||
export type WebSocketPayloads =
|
||||
@ -31,6 +34,8 @@ export type WebSocketPayloads =
|
||||
| ChatPayload
|
||||
| ChatSendPayload
|
||||
| EmojiSendPayload
|
||||
| ScreenResolutionPayload
|
||||
| ScreenConfigurationsPayload
|
||||
| AdminPayload
|
||||
|
||||
export interface WebSocketMessage {
|
||||
@ -145,6 +150,28 @@ export interface EmojiSendPayload {
|
||||
emote: string
|
||||
}
|
||||
|
||||
/*
|
||||
SCREEN PAYLOADS
|
||||
*/
|
||||
export interface ScreenResolutionMessage extends WebSocketMessage, ScreenResolutionPayload {
|
||||
event: ScreenEvents
|
||||
}
|
||||
|
||||
export interface ScreenResolutionPayload {
|
||||
id?: string
|
||||
width: number
|
||||
height: number
|
||||
rate: number
|
||||
}
|
||||
|
||||
export interface ScreenConfigurationsMessage extends WebSocketMessage, ScreenConfigurationsPayload {
|
||||
event: ScreenEvents
|
||||
}
|
||||
|
||||
export interface ScreenConfigurationsPayload {
|
||||
configurations: ScreenConfigurations
|
||||
}
|
||||
|
||||
/*
|
||||
ADMIN PAYLOADS
|
||||
*/
|
||||
|
@ -6,3 +6,13 @@ export interface Member {
|
||||
connected?: boolean
|
||||
ignored?: boolean
|
||||
}
|
||||
|
||||
export interface ScreenConfigurations {
|
||||
[index: number]: ScreenConfiguration
|
||||
}
|
||||
|
||||
export interface ScreenConfiguration {
|
||||
width: string
|
||||
height: string
|
||||
rates: { [index: number]: number }
|
||||
}
|
||||
|
@ -1,5 +1,8 @@
|
||||
import { getterTree, mutationTree, actionTree } from 'typed-vuex'
|
||||
import { get, set } from '~/utils/localstorage'
|
||||
import { EVENT } from '~/neko/events'
|
||||
import { ScreenConfigurations } from '~/neko/types'
|
||||
import { accessor } from '~/store'
|
||||
|
||||
export const namespaced = true
|
||||
|
||||
@ -7,8 +10,10 @@ export const state = () => ({
|
||||
index: -1,
|
||||
tracks: [] as MediaStreamTrack[],
|
||||
streams: [] as MediaStream[],
|
||||
configurations: {} as ScreenConfigurations,
|
||||
width: 1280,
|
||||
height: 720,
|
||||
rate: 30,
|
||||
horizontal: 16,
|
||||
vertical: 9,
|
||||
volume: get<number>('volume', 100),
|
||||
@ -54,9 +59,10 @@ export const mutations = mutationTree(state, {
|
||||
state.playable = playable
|
||||
},
|
||||
|
||||
setResolution(state, { width, height }: { width: number; height: number }) {
|
||||
setResolution(state, { width, height, rate }: { width: number; height: number; rate: number }) {
|
||||
state.width = width
|
||||
state.height = height
|
||||
state.rate = rate
|
||||
|
||||
if ((height == 0 && width == 0) || (height == 0 && width != 0) || (height != 0 && width == 0)) {
|
||||
return
|
||||
@ -92,6 +98,10 @@ export const mutations = mutationTree(state, {
|
||||
state.vertical = height / gcd
|
||||
},
|
||||
|
||||
setConfigurations(state, configurations: ScreenConfigurations) {
|
||||
state.configurations = configurations
|
||||
},
|
||||
|
||||
setVolume(state, volume: number) {
|
||||
state.volume = volume
|
||||
set('volume', volume)
|
||||
@ -115,11 +125,42 @@ export const mutations = mutationTree(state, {
|
||||
state.index = -1
|
||||
state.tracks = []
|
||||
state.streams = []
|
||||
state.configurations = []
|
||||
state.width = 1280
|
||||
state.height = 720
|
||||
state.rate = 30
|
||||
state.horizontal = 16
|
||||
state.vertical = 9
|
||||
state.playing = false
|
||||
state.playable = false
|
||||
},
|
||||
})
|
||||
|
||||
export const actions = actionTree(
|
||||
{ state, getters, mutations },
|
||||
{
|
||||
screenConfiguations({ state }) {
|
||||
if (!accessor.connected || !accessor.user.admin) {
|
||||
return
|
||||
}
|
||||
|
||||
$client.sendMessage(EVENT.SCREEN.CONFIGURATIONS)
|
||||
},
|
||||
|
||||
screenGet({ state }) {
|
||||
if (!accessor.connected) {
|
||||
return
|
||||
}
|
||||
|
||||
$client.sendMessage(EVENT.SCREEN.RESOLUTION)
|
||||
},
|
||||
|
||||
screenSet({ state }, { width, height, rate }: { width: number; height: number; rate: number }) {
|
||||
if (!accessor.connected || !accessor.user.admin) {
|
||||
return
|
||||
}
|
||||
|
||||
$client.sendMessage(EVENT.SCREEN.SET, { width, height, rate })
|
||||
},
|
||||
},
|
||||
)
|
||||
|
Reference in New Issue
Block a user