Archived
2
0

Merge pull request #120 from m1k1o/refactoring

Fix clipboard sync
This commit is contained in:
Miroslav Šedivý 2021-12-11 14:18:46 +01:00 committed by GitHub
commit f08ed0fc28
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 156 additions and 75 deletions

View File

@ -110,7 +110,7 @@
}, },
}) })
export default class extends Vue { export default class extends Vue {
@Ref('context') readonly context!: any @Ref('context') readonly context!: VueContext
get width() { get width() {
return this.$accessor.video.width return this.$accessor.video.width

View File

@ -31,7 +31,7 @@
</div> </div>
<ul v-if="!fullscreen && !hideControls" class="video-menu top"> <ul v-if="!fullscreen && !hideControls" class="video-menu top">
<li><i @click.stop.prevent="requestFullscreen" class="fas fa-expand"></i></li> <li><i @click.stop.prevent="requestFullscreen" class="fas fa-expand"></i></li>
<li v-if="admin"><i @click.stop.prevent="onResolution" class="fas fa-desktop"></i></li> <li v-if="admin"><i @click.stop.prevent="openResolution" class="fas fa-desktop"></i></li>
<li class="request-control"> <li class="request-control">
<i <i
:class="[hosted && !hosting ? 'disabled' : '', !hosted && !hosting ? 'faded' : '', 'fas', 'fa-keyboard']" :class="[hosted && !hosting ? 'disabled' : '', !hosted && !hosting ? 'faded' : '', 'fas', 'fa-keyboard']"
@ -41,7 +41,7 @@
</ul> </ul>
<ul v-if="!fullscreen && !hideControls" class="video-menu bottom"> <ul v-if="!fullscreen && !hideControls" class="video-menu bottom">
<li v-if="hosting && (!clipboard_read_available || !clipboard_write_available)"> <li v-if="hosting && (!clipboard_read_available || !clipboard_write_available)">
<i @click.stop.prevent="onClipboard" class="fas fa-clipboard"></i> <i @click.stop.prevent="openClipboard" class="fas fa-clipboard"></i>
</li> </li>
<li> <li>
<i <i
@ -186,6 +186,7 @@
<script lang="ts"> <script lang="ts">
import { Component, Ref, Watch, Vue, Prop } from 'vue-property-decorator' import { Component, Ref, Watch, Vue, Prop } from 'vue-property-decorator'
import ResizeObserver from 'resize-observer-polyfill' import ResizeObserver from 'resize-observer-polyfill'
import { elementRequestFullscreen } from '~/utils'
import Emote from './emote.vue' import Emote from './emote.vue'
import Resolution from './resolution.vue' import Resolution from './resolution.vue'
@ -211,13 +212,13 @@
@Ref('aspect') readonly _aspect!: HTMLElement @Ref('aspect') readonly _aspect!: HTMLElement
@Ref('player') readonly _player!: HTMLElement @Ref('player') readonly _player!: HTMLElement
@Ref('video') readonly _video!: HTMLVideoElement @Ref('video') readonly _video!: HTMLVideoElement
@Ref('resolution') readonly _resolution!: any @Ref('resolution') readonly _resolution!: Resolution
@Ref('clipboard') readonly _clipboard!: any @Ref('clipboard') readonly _clipboard!: Clipboard
@Prop(Boolean) readonly hideControls!: boolean @Prop(Boolean) readonly hideControls!: boolean
private keyboard = GuacamoleKeyboard() private keyboard = GuacamoleKeyboard()
private observer = new ResizeObserver(this.onResise.bind(this)) private observer = new ResizeObserver(this.onResize.bind(this))
private focused = false private focused = false
private fullscreen = false private fullscreen = false
private startsMuted = true private startsMuted = true
@ -322,12 +323,12 @@
@Watch('width') @Watch('width')
onWidthChanged(width: number) { onWidthChanged(width: number) {
this.onResise() this.onResize()
} }
@Watch('height') @Watch('height')
onHeightChanged(height: number) { onHeightChanged(height: number) {
this.onResise() this.onResize()
} }
@Watch('volume') @Watch('volume')
@ -377,24 +378,29 @@
} }
@Watch('clipboard') @Watch('clipboard')
onClipboardChanged(clipboard: string) { async onClipboardChanged(clipboard: string) {
if (this.clipboard_write_available) { if (this.clipboard_write_available) {
navigator.clipboard.writeText(clipboard).catch(console.error) try {
await navigator.clipboard.writeText(clipboard)
this.$accessor.remote.setClipboard(clipboard)
} catch (err: any) {
this.$log.error(err)
}
} }
} }
mounted() { mounted() {
this._container.addEventListener('resize', this.onResise) this._container.addEventListener('resize', this.onResize)
this.onVolumeChanged(this.volume) this.onVolumeChanged(this.volume)
this.onMutedChanged(this.muted) this.onMutedChanged(this.muted)
this.onStreamChanged(this.stream) this.onStreamChanged(this.stream)
this.onResise() this.onResize()
this.observer.observe(this._component) this.observer.observe(this._component)
this._player.addEventListener('fullscreenchange', () => { this._player.addEventListener('fullscreenchange', () => {
this.fullscreen = document.fullscreenElement !== null this.fullscreen = document.fullscreenElement !== null
this.onResise() this.onResize()
}) })
this._video.addEventListener('canplaythrough', () => { this._video.addEventListener('canplaythrough', () => {
@ -433,8 +439,6 @@
this.$accessor.video.pause() this.$accessor.video.pause()
}) })
document.addEventListener('focusin', this.onFocus.bind(this))
/* Initialize Guacamole Keyboard */ /* Initialize Guacamole Keyboard */
this.keyboard.onkeydown = (key: number) => { this.keyboard.onkeydown = (key: number) => {
if (!this.focused || !this.hosting || this.locked) { if (!this.focused || !this.hosting || this.locked) {
@ -457,7 +461,6 @@
beforeDestroy() { beforeDestroy() {
this.observer.disconnect() this.observer.disconnect()
this.$accessor.video.setPlayable(false) this.$accessor.video.setPlayable(false)
document.removeEventListener('focusin', this.onFocus.bind(this))
/* Guacamole Keyboard does not provide destroy functions */ /* Guacamole Keyboard does not provide destroy functions */
} }
@ -513,7 +516,7 @@
try { try {
await this._video.play() await this._video.play()
this.onResise() this.onResize()
} catch (err: any) { } catch (err: any) {
this.$log.error(err) this.$log.error(err)
} }
@ -551,38 +554,20 @@
this.$accessor.remote.toggle() this.$accessor.remote.toggle()
} }
_elementRequestFullscreen(el: HTMLElement) { requestControl() {
if (typeof el.requestFullscreen === 'function') { this.$accessor.remote.request()
el.requestFullscreen()
//@ts-ignore
} else if (typeof el.webkitRequestFullscreen === 'function') {
//@ts-ignore
el.webkitRequestFullscreen()
//@ts-ignore
} else if (typeof el.webkitEnterFullscreen === 'function') {
//@ts-ignore
el.webkitEnterFullscreen()
//@ts-ignore
} else if (typeof el.msRequestFullScreen === 'function') {
//@ts-ignore
el.msRequestFullScreen()
} else {
return false
}
return true
} }
requestFullscreen() { requestFullscreen() {
// try to fullscreen player element // try to fullscreen player element
if (this._elementRequestFullscreen(this._player)) { if (elementRequestFullscreen(this._player)) {
this.onResise() this.onResize()
return return
} }
// fallback to fullscreen video itself (on mobile devices) // fallback to fullscreen video itself (on mobile devices)
if (this._elementRequestFullscreen(this._video)) { if (elementRequestFullscreen(this._video)) {
this.onResise() this.onResize()
return return
} }
} }
@ -590,15 +575,19 @@
requestPictureInPicture() { requestPictureInPicture() {
//@ts-ignore //@ts-ignore
this._video.requestPictureInPicture() this._video.requestPictureInPicture()
this.onResise() this.onResize()
} }
async onFocus() { openResolution(event: MouseEvent) {
if (!document.hasFocus() || !this.$accessor.active) { this._resolution.open(event)
return }
}
if (this.hosting && this.clipboard_read_available) { openClipboard() {
this._clipboard.open()
}
async syncClipboard() {
if (this.clipboard_read_available) {
try { try {
const text = await navigator.clipboard.readText() const text = await navigator.clipboard.readText()
if (this.clipboard !== text) { if (this.clipboard !== text) {
@ -611,9 +600,10 @@
} }
} }
onMousePos(e: MouseEvent) { sendMousePos(e: MouseEvent) {
const { w, h } = this.$accessor.video.resolution const { w, h } = this.$accessor.video.resolution
const rect = this._overlay.getBoundingClientRect() const rect = this._overlay.getBoundingClientRect()
this.$client.sendData('mousemove', { this.$client.sendData('mousemove', {
x: Math.round((w / rect.width) * (e.clientX - rect.left)), x: Math.round((w / rect.width) * (e.clientX - rect.left)),
y: Math.round((h / rect.height) * (e.clientY - rect.top)), y: Math.round((h / rect.height) * (e.clientY - rect.top)),
@ -625,7 +615,6 @@
if (!this.hosting || this.locked) { if (!this.hosting || this.locked) {
return return
} }
this.onMousePos(e)
let x = e.deltaX let x = e.deltaX
let y = e.deltaY let y = e.deltaY
@ -648,6 +637,8 @@
x = Math.min(Math.max(x, -this.scroll), this.scroll) x = Math.min(Math.max(x, -this.scroll), this.scroll)
y = Math.min(Math.max(y, -this.scroll), this.scroll) y = Math.min(Math.max(y, -this.scroll), this.scroll)
this.sendMousePos(e)
if (!this.wheelThrottle) { if (!this.wheelThrottle) {
this.wheelThrottle = true this.wheelThrottle = true
this.$client.sendData('wheel', { x, y }) this.$client.sendData('wheel', { x, y })
@ -667,7 +658,7 @@
return return
} }
this.onMousePos(e) this.sendMousePos(e)
this.$client.sendData('mousedown', { key: e.button + 1 }) this.$client.sendData('mousedown', { key: e.button + 1 })
} }
@ -676,7 +667,7 @@
return return
} }
this.onMousePos(e) this.sendMousePos(e)
this.$client.sendData('mouseup', { key: e.button + 1 }) this.$client.sendData('mouseup', { key: e.button + 1 })
} }
@ -685,7 +676,7 @@
return return
} }
this.onMousePos(e) this.sendMousePos(e)
} }
onMouseEnter(e: MouseEvent) { onMouseEnter(e: MouseEvent) {
@ -695,10 +686,11 @@
numLock: e.getModifierState('NumLock'), numLock: e.getModifierState('NumLock'),
scrollLock: e.getModifierState('ScrollLock'), scrollLock: e.getModifierState('ScrollLock'),
}) })
this.syncClipboard()
} }
this._overlay.focus() this._overlay.focus()
this.onFocus()
this.focused = true this.focused = true
} }
@ -715,7 +707,7 @@
this.focused = false this.focused = false
} }
onResise() { onResize() {
let height = 0 let height = 0
if (!this.fullscreen) { if (!this.fullscreen) {
const { offsetWidth, offsetHeight } = this._component const { offsetWidth, offsetHeight } = this._component
@ -730,13 +722,5 @@
this._container.style.maxWidth = `${(this.horizontal / this.vertical) * height}px` this._container.style.maxWidth = `${(this.horizontal / this.vertical) * height}px`
this._aspect.style.paddingBottom = `${(this.vertical / this.horizontal) * 100}%` this._aspect.style.paddingBottom = `${(this.vertical / this.horizontal) * 100}%`
} }
onResolution(event: MouseEvent) {
this._resolution.open(event)
}
onClipboard(event: MouseEvent) {
this._clipboard.open(event)
}
} }
</script> </script>

View File

@ -10,6 +10,7 @@ export const EVENT = {
// Websocket Events // Websocket Events
SYSTEM: { SYSTEM: {
INIT: 'system/init',
DISCONNECT: 'system/disconnect', DISCONNECT: 'system/disconnect',
ERROR: 'system/error', ERROR: 'system/error',
}, },

View File

@ -22,6 +22,8 @@ import {
AdminPayload, AdminPayload,
AdminTargetPayload, AdminTargetPayload,
AdminLockMessage, AdminLockMessage,
SystemInitPayload,
AdminLockResource,
} from './messages' } from './messages'
interface NekoEvents extends BaseEvents {} interface NekoEvents extends BaseEvents {}
@ -131,6 +133,18 @@ export class NekoClient extends BaseClient implements EventEmitter<NekoEvents> {
///////////////////////////// /////////////////////////////
// System Events // System Events
///////////////////////////// /////////////////////////////
protected [EVENT.SYSTEM.INIT]({ implicit_hosting, locks }: SystemInitPayload) {
this.$accessor.remote.setImplicitHosting(implicit_hosting)
for (const resource in locks) {
this[EVENT.ADMIN.LOCK]({
event: EVENT.ADMIN.LOCK,
resource: resource as AdminLockResource,
id: locks[resource],
})
}
}
protected [EVENT.SYSTEM.DISCONNECT]({ message }: SystemMessagePayload) { protected [EVENT.SYSTEM.DISCONNECT]({ message }: SystemMessagePayload) {
if (message == 'kicked') { if (message == 'kicked') {
this.$accessor.logout() this.$accessor.logout()

View File

@ -52,6 +52,15 @@ export interface WebSocketMessage {
/* /*
SYSTEM MESSAGES/PAYLOADS SYSTEM MESSAGES/PAYLOADS
*/ */
// system/init
export interface SystemInit extends WebSocketMessage, SystemInitPayload {
event: typeof EVENT.SYSTEM.INIT
}
export interface SystemInitPayload {
implicit_hosting: boolean
locks: Record<string, string>
}
// system/disconnect // system/disconnect
// system/error // system/error
export interface SystemMessage extends WebSocketMessage, SystemMessagePayload { export interface SystemMessage extends WebSocketMessage, SystemMessagePayload {

View File

@ -12,7 +12,7 @@ export const state = () => ({
id: '', id: '',
clipboard: '', clipboard: '',
locked: false, locked: false,
implicitHosting: true,
keyboardModifierState: -1, keyboardModifierState: -1,
}) })
@ -49,10 +49,15 @@ export const mutations = mutationTree(state, {
state.locked = locked state.locked = locked
}, },
setImplicitHosting(state, val: boolean) {
state.implicitHosting = val
},
reset(state) { reset(state) {
state.id = '' state.id = ''
state.clipboard = '' state.clipboard = ''
state.locked = false state.locked = false
state.implicitHosting = false
state.keyboardModifierState = -1 state.keyboardModifierState = -1
}, },
}) })

View File

@ -7,3 +7,24 @@ export function makeid(length: number) {
} }
return result return result
} }
export function elementRequestFullscreen(el: HTMLElement) {
if (typeof el.requestFullscreen === 'function') {
el.requestFullscreen()
//@ts-ignore
} else if (typeof el.webkitRequestFullscreen === 'function') {
//@ts-ignore
el.webkitRequestFullscreen()
//@ts-ignore
} else if (typeof el.webkitEnterFullscreen === 'function') {
//@ts-ignore
el.webkitEnterFullscreen()
//@ts-ignore
} else if (typeof el.msRequestFullScreen === 'function') {
//@ts-ignore
el.msRequestFullScreen()
} else {
return false
}
return true
}

View File

@ -4,10 +4,12 @@
### New Features ### New Features
- Added `m1k1o/neko:microsoft-edge` tag. - Added `m1k1o/neko:microsoft-edge` tag.
- Fixed clipboard sync in chromium based browsers.
### Misc ### Misc
- Automatic WebRTC SDP negotiation using onnegotiationneeded handlers. This allows adding/removing track on demand in a session. - Automatic WebRTC SDP negotiation using onnegotiationneeded handlers. This allows adding/removing track on demand in a session.
- Added UDP and TCP mux for WebRTC connection. It should handle multiple peers. - Added UDP and TCP mux for WebRTC connection. It should handle multiple peers.
- Broadcast status change is sent to all admins now.
## [n.eko v2.5](https://github.com/m1k1o/neko/releases/tag/v2.5) ## [n.eko v2.5](https://github.com/m1k1o/neko/releases/tag/v2.5)

View File

@ -184,6 +184,30 @@ func (manager *SessionManager) Broadcast(v interface{}, exclude interface{}) err
return err return err
} }
} }
return nil
}
func (manager *SessionManager) AdminBroadcast(v interface{}, exclude interface{}) error {
manager.mu.Lock()
defer manager.mu.Unlock()
for id, session := range manager.members {
if !session.connected || !session.admin {
continue
}
if exclude != nil {
if in, _ := utils.ArrayIn(id, exclude); in {
continue
}
}
if err := session.Send(v); err != nil {
return err
}
}
return nil return nil
} }

View File

@ -1,6 +1,7 @@
package event package event
const ( const (
SYSTEM_INIT = "system/init"
SYSTEM_DISCONNECT = "system/disconnect" SYSTEM_DISCONNECT = "system/disconnect"
SYSTEM_ERROR = "system/error" SYSTEM_ERROR = "system/error"
) )

View File

@ -10,6 +10,12 @@ type Message struct {
Event string `json:"event"` Event string `json:"event"`
} }
type SystemInit struct {
Event string `json:"event"`
ImplicitHosting bool `json:"implicit_hosting"`
Locks map[string]string `json:"locks"`
}
type SystemMessage struct { type SystemMessage struct {
Event string `json:"event"` Event string `json:"event"`
Title string `json:"title"` Title string `json:"title"`

View File

@ -43,6 +43,7 @@ type SessionManager interface {
Destroy(id string) Destroy(id string)
Clear() error Clear() error
Broadcast(v interface{}, exclude interface{}) error Broadcast(v interface{}, exclude interface{}) error
AdminBroadcast(v interface{}, exclude interface{}) error
OnHost(listener func(id string)) OnHost(listener func(id string))
OnHostCleared(listener func(id string)) OnHostCleared(listener func(id string))
OnDestroy(listener func(id string, session Session)) OnDestroy(listener func(id string, session Session))

View File

@ -25,7 +25,7 @@ func (h *MessageHandler) boradcastCreate(session types.Session, payload *message
} }
} }
if err := h.boradcastStatus(session); err != nil { if err := h.boradcastStatus(nil); err != nil {
return err return err
} }
@ -40,7 +40,7 @@ func (h *MessageHandler) boradcastDestroy(session types.Session) error {
h.broadcast.Destroy() h.broadcast.Destroy()
if err := h.boradcastStatus(session); err != nil { if err := h.boradcastStatus(nil); err != nil {
return err return err
} }
@ -48,6 +48,21 @@ func (h *MessageHandler) boradcastDestroy(session types.Session) error {
} }
func (h *MessageHandler) boradcastStatus(session types.Session) error { func (h *MessageHandler) boradcastStatus(session types.Session) error {
// if no session, broadcast change
if session == nil {
if err := h.sessions.AdminBroadcast(
message.BroadcastStatus{
Event: event.BORADCAST_STATUS,
IsActive: h.broadcast.IsActive(),
URL: h.broadcast.GetUrl(),
}, nil); err != nil {
h.logger.Warn().Err(err).Msgf("broadcasting event %s has failed", event.BORADCAST_STATUS)
return err
}
return nil
}
if !session.Admin() { if !session.Admin() {
h.logger.Debug().Msg("user not admin") h.logger.Debug().Msg("user not admin")
return nil return nil

View File

@ -12,16 +12,14 @@ func (h *MessageHandler) SessionCreated(id string, session types.Session) error
return err return err
} }
// notify all about what is locked // send initialization information
for resource, id := range h.locked { if err := session.Send(message.SystemInit{
if err := session.Send(message.AdminLock{ Event: event.SYSTEM_INIT,
Event: event.ADMIN_LOCK, ImplicitHosting: true,
ID: id, Locks: h.locked,
Resource: resource, }); err != nil {
}); err != nil { h.logger.Warn().Str("id", id).Err(err).Msgf("sending event %s has failed", event.SYSTEM_INIT)
h.logger.Warn().Str("id", id).Err(err).Msgf("sending event %s has failed", event.ADMIN_LOCK) return err
return err
}
} }
if session.Admin() { if session.Admin() {