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 {
@Ref('context') readonly context!: any
@Ref('context') readonly context!: VueContext
get width() {
return this.$accessor.video.width

View File

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

View File

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

View File

@ -22,6 +22,8 @@ import {
AdminPayload,
AdminTargetPayload,
AdminLockMessage,
SystemInitPayload,
AdminLockResource,
} from './messages'
interface NekoEvents extends BaseEvents {}
@ -131,6 +133,18 @@ export class NekoClient extends BaseClient implements EventEmitter<NekoEvents> {
/////////////////////////////
// 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) {
if (message == 'kicked') {
this.$accessor.logout()

View File

@ -52,6 +52,15 @@ export interface WebSocketMessage {
/*
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/error
export interface SystemMessage extends WebSocketMessage, SystemMessagePayload {

View File

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

View File

@ -7,3 +7,24 @@ export function makeid(length: number) {
}
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
- Added `m1k1o/neko:microsoft-edge` tag.
- Fixed clipboard sync in chromium based browsers.
### Misc
- 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.
- Broadcast status change is sent to all admins now.
## [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 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
}

View File

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

View File

@ -10,6 +10,12 @@ type Message struct {
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 {
Event string `json:"event"`
Title string `json:"title"`

View File

@ -43,6 +43,7 @@ type SessionManager interface {
Destroy(id string)
Clear() error
Broadcast(v interface{}, exclude interface{}) error
AdminBroadcast(v interface{}, exclude interface{}) error
OnHost(listener func(id string))
OnHostCleared(listener func(id string))
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
}
@ -40,7 +40,7 @@ func (h *MessageHandler) boradcastDestroy(session types.Session) error {
h.broadcast.Destroy()
if err := h.boradcastStatus(session); err != nil {
if err := h.boradcastStatus(nil); err != nil {
return err
}
@ -48,6 +48,21 @@ func (h *MessageHandler) boradcastDestroy(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() {
h.logger.Debug().Msg("user not admin")
return nil

View File

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