mirror of
https://github.com/m1k1o/neko.git
synced 2024-07-24 14:40:50 +12:00
commit
f08ed0fc28
@ -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
|
||||
|
@ -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>
|
||||
|
@ -10,6 +10,7 @@ export const EVENT = {
|
||||
|
||||
// Websocket Events
|
||||
SYSTEM: {
|
||||
INIT: 'system/init',
|
||||
DISCONNECT: 'system/disconnect',
|
||||
ERROR: 'system/error',
|
||||
},
|
||||
|
@ -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()
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
},
|
||||
})
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
package event
|
||||
|
||||
const (
|
||||
SYSTEM_INIT = "system/init"
|
||||
SYSTEM_DISCONNECT = "system/disconnect"
|
||||
SYSTEM_ERROR = "system/error"
|
||||
)
|
||||
|
@ -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"`
|
||||
|
@ -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))
|
||||
|
@ -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
|
||||
|
@ -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() {
|
||||
|
Loading…
Reference in New Issue
Block a user