From 9e995233afff8f987a175ffdbb81c1d60fc40735 Mon Sep 17 00:00:00 2001 From: Craig Date: Tue, 11 Feb 2020 05:15:59 +0000 Subject: [PATCH] live change resolution (WIP) --- .devcontainer/Dockerfile | 4 +- .docker/files/xorg.conf | 28 +++- Dockerfile | 2 +- client/src/components/resolution.vue | 121 ++++++++++++++++++ client/src/components/video.vue | 84 +++++++++--- client/src/neko/events.ts | 8 ++ client/src/neko/index.ts | 31 ++++- client/src/neko/messages.ts | 29 ++++- client/src/neko/types.ts | 10 ++ client/src/store/video.ts | 43 ++++++- server/internal/gst/gst.go | 105 +++++++++------ server/internal/types/event/events.go | 70 ++++++---- server/internal/types/message/messages.go | 13 ++ server/internal/types/webrtc.go | 1 + server/internal/types/xorg.go | 13 ++ server/internal/webrtc/handle.go | 14 +- server/internal/webrtc/webrtc.go | 48 +++++-- server/internal/websocket/control.go | 4 +- server/internal/websocket/handler.go | 12 ++ server/internal/websocket/screen.go | 67 ++++++++++ server/internal/websocket/session.go | 12 ++ server/internal/websocket/websocket.go | 6 +- .../internal/{hid => xorg}/keycode/button.go | 0 server/internal/{hid => xorg}/keycode/keys.go | 0 server/internal/{hid/hid.c => xorg/xorg.c} | 48 ++++++- server/internal/{hid/hid.go => xorg/xorg.go} | 83 ++++++++++-- server/internal/{hid/hid.h => xorg/xorg.h} | 18 ++- 27 files changed, 747 insertions(+), 127 deletions(-) create mode 100644 client/src/components/resolution.vue create mode 100644 server/internal/types/xorg.go create mode 100644 server/internal/websocket/screen.go rename server/internal/{hid => xorg}/keycode/button.go (100%) rename server/internal/{hid => xorg}/keycode/keys.go (100%) rename server/internal/{hid/hid.c => xorg/xorg.c} (62%) rename server/internal/{hid/hid.go => xorg/xorg.go} (80%) rename server/internal/{hid/hid.h => xorg/xorg.h} (67%) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 2fa781ff..718f5927 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -10,9 +10,9 @@ RUN set -eux; apt-get update; apt-get install -y --no-install-recommends \ libpng-dev libpq-dev libreadline-dev libsqlite3-dev libssl-dev libtool libwebp-dev libxml2-dev libxslt-dev libyaml-dev \ make patch unzip xz-utils zlib1g-dev pkg-config \ build-essential perl python autopoint bison flex \ - gettext openssl libopus-dev libvpx-dev libpulse-dev libx11-dev libxv-dev libxt-dev \ + gettext openssl libopus-dev libvpx-dev libpulse-dev libx11-dev libxv-dev libxt-dev libxrandr-dev \ libxfixes-dev apt-utils x11vnc libxtst-dev dialog \ - pulseaudio openbox chromium firefox-esr dbus-x11 xvfb xclip supervisor; \ + pulseaudio openbox chromium firefox-esr dbus-x11 xserver-xorg-video-dummy supervisor; \ if ! command -v gpg > /dev/null; then \ apt-get install -y --no-install-recommends gnupg dirmngr; \ fi diff --git a/.docker/files/xorg.conf b/.docker/files/xorg.conf index 89cee560..c803041c 100644 --- a/.docker/files/xorg.conf +++ b/.docker/files/xorg.conf @@ -1,7 +1,7 @@ # This xorg configuration file is meant to be used by xpra # to start a dummy X11 server. # For details, please see: -# https://xpra.org/Xdummy.html +# https://xpra.org/trac/wiki/Xdummy Section "ServerFlags" Option "DontVTSwitch" "true" @@ -41,8 +41,31 @@ Section "Monitor" #NOTE: the highest modes will not work without increasing the VideoRam # for the dummy video card. # https://arachnoid.com/modelines/ + # 1280x720 @ 30.00 Hz (GTF) hsync: 21.99 kHz; pclk: 33.78 MHz Modeline "1280x720_30.00" 33.78 1280 1288 1408 1536 720 721 724 733 -HSync +Vsync + + # 1280x720 @ 60.00 Hz (GTF) hsync: 44.76 kHz; pclk: 74.48 MHz + Modeline "1280x720_60.00" 74.48 1280 1336 1472 1664 720 721 724 746 -HSync +Vsync + # 1152x648 @ 60.00 Hz (GTF) hsync: 40.26 kHz; pclk: 59.91 MHz + Modeline "1152x648_60.00" 59.91 1152 1200 1320 1488 648 649 652 671 -HSync +Vsync + # 1024x576 @ 60.00 Hz (GTF) hsync: 35.82 kHz; pclk: 47.00 MHz + Modeline "1024x576_60.00" 47.00 1024 1064 1168 1312 576 577 580 597 -HSync +Vsync + # 960x720 @ 60.00 Hz (GTF) hsync: 44.76 kHz; pclk: 55.86 MHz + Modeline "960x720_60.00" 55.86 960 1008 1104 1248 720 721 724 746 -HSync +Vsync + # 800x600 @ 60.00 Hz (GTF) hsync: 37.32 kHz; pclk: 38.22 MHz + Modeline "800x600_60.00" 38.22 800 832 912 1024 600 601 604 622 -HSync +Vsync + + # 1920x1080 @ 30.00 Hz (GTF) hsync: 32.97 kHz; pclk: 80.18 MHz + Modeline "1920x1080_30.00" 80.18 1920 1984 2176 2432 1080 1081 1084 1099 -HSync +Vsync + # 1152x648 @ 30.00 Hz (GTF) hsync: 19.80 kHz; pclk: 26.93 MHz + Modeline "1152x648_30.00" 26.93 1152 1144 1256 1360 648 649 652 660 -HSync +Vsync + # 1024x576 @ 30.00 Hz (GTF) hsync: 17.61 kHz; pclk: 20.85 MHz + Modeline "1024x576_30.00" 20.85 1024 1008 1104 1184 576 577 580 587 -HSync +Vsync + # 960x720 @ 30.00 Hz (GTF) hsync: 21.99 kHz; pclk: 25.33 MHz + Modeline "960x720_30.00" 25.33 960 960 1056 1152 720 721 724 733 -HSync +Vsync + # 800x600 @ 30.00 Hz (GTF) hsync: 18.33 kHz; pclk: 17.01 MHz + Modeline "800x600_30.00" 17.01 800 792 864 928 600 601 604 611 -HSync +Vsync EndSection Section "Screen" @@ -53,8 +76,7 @@ Section "Screen" SubSection "Display" Viewport 0 0 Depth 24 - Modes "1280x720_30.00" - Virtual 1280 720 + Modes "1280x720_30.00" "1920x1080_60.00" "1280x720_60.00" "1152x648_60.00" "1024x576_60.00" "960x720_60.00" "800x600_60.00" "1920x1080_30.00" "1152x648_30.00" "1024x576_30.00" "960x720_30.00" "800x600_30.00" EndSubSection EndSection diff --git a/Dockerfile b/Dockerfile index 5ab2c9dd..bc37bc8b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -29,7 +29,7 @@ RUN set -eux; apt-get update; \ # install neko dependencies RUN set -eux; apt-get update; \ apt-get install -y --no-install-recommends wget ca-certificates pulseaudio openbox dbus-x11 xserver-xorg-video-dummy supervisor; \ - apt-get install -y --no-install-recommends libxv1 libopus0 libvpx4; \ + apt-get install -y --no-install-recommends libxrandr2 libxv1 libopus0 libvpx4; \ # # create a non-root user groupadd --gid $USER_GID $USERNAME; \ diff --git a/client/src/components/resolution.vue b/client/src/components/resolution.vue new file mode 100644 index 00000000..907f8c38 --- /dev/null +++ b/client/src/components/resolution.vue @@ -0,0 +1,121 @@ + + + + + diff --git a/client/src/components/video.vue b/client/src/components/video.vue index e3f03225..c1f70c99 100644 --- a/client/src/components/video.vue +++ b/client/src/components/video.vue @@ -28,7 +28,11 @@
- + + @@ -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) } } diff --git a/client/src/neko/events.ts b/client/src/neko/events.ts index 6791c1ea..2bce4918 100644 --- a/client/src/neko/events.ts +++ b/client/src/neko/events.ts @@ -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 diff --git a/client/src/neko/index.ts b/client/src/neko/index.ts index d83a79c7..7643e585 100644 --- a/client/src/neko/index.ts +++ b/client/src/neko/index.ts @@ -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 { 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 ///////////////////////////// diff --git a/client/src/neko/messages.ts b/client/src/neko/messages.ts index 42c68c8a..f08e61f6 100644 --- a/client/src/neko/messages.ts +++ b/client/src/neko/messages.ts @@ -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 */ diff --git a/client/src/neko/types.ts b/client/src/neko/types.ts index 8670f47b..ec3e4559 100644 --- a/client/src/neko/types.ts +++ b/client/src/neko/types.ts @@ -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 } +} diff --git a/client/src/store/video.ts b/client/src/store/video.ts index cc95da50..2c1adac3 100644 --- a/client/src/store/video.ts +++ b/client/src/store/video.ts @@ -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('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 }) + }, + }, +) diff --git a/server/internal/gst/gst.go b/server/internal/gst/gst.go index 83038665..2471c2fa 100644 --- a/server/internal/gst/gst.go +++ b/server/internal/gst/gst.go @@ -44,6 +44,7 @@ type Pipeline struct { Sample chan types.Sample CodecName string ClockRate float32 + Src string id int } @@ -55,6 +56,8 @@ const ( videoClockRate = 90000 audioClockRate = 48000 pcmClockRate = 8000 + videoSrc = "ximagesrc xid=%s show-pointer=true use-damage=false ! video/x-raw ! videoconvert ! queue ! " + audioSrc = "pulsesrc device=%s ! audioconvert ! " ) func init() { @@ -63,8 +66,9 @@ func init() { } // CreatePipeline creates a GStreamer Pipeline -func CreatePipeline(codecName string, pipelineSrc string) (*Pipeline, error) { - pipelineStr := "appsink name=appsink" +func CreatePipeline(codecName string, pipelineDevice string, pipelineSrc string) (*Pipeline, error) { + pipelineStr := " ! appsink name=appsink" + var clockRate float32 switch codecName { @@ -72,93 +76,119 @@ func CreatePipeline(codecName string, pipelineSrc string) (*Pipeline, error) { // https://gstreamer.freedesktop.org/documentation/vpx/vp8enc.html?gi-language=c // gstreamer1.0-plugins-good // vp8enc error-resilient=partitions keyframe-max-dist=10 auto-alt-ref=true cpu-used=5 deadline=1 - pipelineStr = pipelineSrc + " ! vp8enc cpu-used=8 threads=2 deadline=1 error-resilient=partitions keyframe-max-dist=10 auto-alt-ref=true ! " + pipelineStr - clockRate = videoClockRate - if err := CheckPlugins([]string{"ximagesrc", "vpx"}); err != nil { return nil, err } + clockRate = videoClockRate + + if pipelineSrc != "" { + pipelineStr = fmt.Sprintf(pipelineSrc+pipelineStr, pipelineDevice) + } else { + pipelineStr = fmt.Sprintf(videoSrc+"vp8enc cpu-used=8 threads=2 deadline=1 error-resilient=partitions keyframe-max-dist=10 auto-alt-ref=true"+pipelineStr, pipelineDevice) + } case webrtc.VP9: // https://gstreamer.freedesktop.org/documentation/vpx/vp9enc.html?gi-language=c // gstreamer1.0-plugins-good // vp9enc - - // Causes panic! not sure why... - pipelineStr = pipelineSrc + " ! vp9enc ! " + pipelineStr - clockRate = videoClockRate - if err := CheckPlugins([]string{"ximagesrc", "vpx"}); err != nil { return nil, err } + clockRate = videoClockRate + + // Causes panic! not sure why... + if pipelineSrc != "" { + pipelineStr = fmt.Sprintf(pipelineSrc+pipelineStr, pipelineDevice) + } else { + pipelineStr = fmt.Sprintf(videoSrc+"vp9enc"+pipelineStr, pipelineDevice) + } case webrtc.H264: // https://gstreamer.freedesktop.org/documentation/openh264/openh264enc.html?gi-language=c#openh264enc // gstreamer1.0-plugins-bad // openh264enc multi-thread=4 complexity=high bitrate=3072000 max-bitrate=4096000 - pipelineStr = pipelineSrc + " ! openh264enc multi-thread=4 complexity=high bitrate=3072000 max-bitrate=4096000 ! video/x-h264,stream-format=byte-stream ! " + pipelineStr - clockRate = videoClockRate - if err := CheckPlugins([]string{"ximagesrc"}); err != nil { return nil, err } - // https://gstreamer.freedesktop.org/documentation/x264/index.html?gi-language=c - // gstreamer1.0-plugins-ugly - // video/x-raw,format=I420 ! x264enc bframes=0 key-int-max=60 byte-stream=true tune=zerolatency speed-preset=veryfast ! video/x-h264,stream-format=byte-stream - if err := CheckPlugins([]string{"openh264"}); err != nil { - pipelineStr = pipelineSrc + " ! video/x-raw,format=I420 ! x264enc bframes=0 key-int-max=60 byte-stream=true tune=zerolatency speed-preset=veryfast ! video/x-h264,stream-format=byte-stream ! " + pipelineStr + clockRate = videoClockRate - if err := CheckPlugins([]string{"x264"}); err != nil { - return nil, err + if pipelineSrc != "" { + pipelineStr = fmt.Sprintf(pipelineSrc+pipelineStr, pipelineDevice) + } else { + pipelineStr = fmt.Sprintf(videoSrc+"openh264enc multi-thread=4 complexity=high bitrate=3072000 max-bitrate=4096000 ! video/x-h264,stream-format=byte-stream"+pipelineStr, pipelineDevice) + + // https://gstreamer.freedesktop.org/documentation/x264/index.html?gi-language=c + // gstreamer1.0-plugins-ugly + // video/x-raw,format=I420 ! x264enc bframes=0 key-int-max=60 byte-stream=true tune=zerolatency speed-preset=veryfast ! video/x-h264,stream-format=byte-stream + if err := CheckPlugins([]string{"openh264"}); err != nil { + pipelineStr = fmt.Sprintf(videoSrc+"video/x-raw,format=I420 ! x264enc bframes=0 key-int-max=60 byte-stream=true tune=zerolatency speed-preset=veryfast ! video/x-h264,stream-format=byte-stream"+pipelineStr, pipelineDevice) + + if err := CheckPlugins([]string{"x264"}); err != nil { + return nil, err + } } } - case webrtc.Opus: // https://gstreamer.freedesktop.org/documentation/opus/opusenc.html // gstreamer1.0-plugins-base // opusenc - pipelineStr = pipelineSrc + " ! opusenc ! " + pipelineStr - clockRate = audioClockRate - if err := CheckPlugins([]string{"pulseaudio", "opus"}); err != nil { return nil, err } + clockRate = audioClockRate + + if pipelineSrc != "" { + pipelineStr = fmt.Sprintf(pipelineSrc+pipelineStr, pipelineDevice) + } else { + pipelineStr = fmt.Sprintf(audioSrc+"opusenc"+pipelineStr, pipelineDevice) + } case webrtc.G722: // https://gstreamer.freedesktop.org/documentation/libav/avenc_g722.html?gi-language=c // gstreamer1.0-libav // avenc_g722 - pipelineStr = pipelineSrc + " ! avenc_g722 ! " + pipelineStr - clockRate = audioClockRate - if err := CheckPlugins([]string{"pulseaudio", "libav"}); err != nil { return nil, err } + clockRate = audioClockRate + + if pipelineSrc != "" { + pipelineStr = fmt.Sprintf(pipelineSrc+pipelineStr, pipelineDevice) + } else { + pipelineStr = fmt.Sprintf(audioSrc+"avenc_g722"+pipelineStr, pipelineDevice) + } case webrtc.PCMU: // https://gstreamer.freedesktop.org/documentation/mulaw/mulawenc.html?gi-language=c // gstreamer1.0-plugins-good // audio/x-raw, rate=8000 ! mulawenc - - pipelineStr = pipelineSrc + " ! audio/x-raw, rate=8000 ! mulawenc ! " + pipelineStr - clockRate = pcmClockRate - if err := CheckPlugins([]string{"pulseaudio", "mulaw"}); err != nil { return nil, err } + clockRate = pcmClockRate + + if pipelineSrc != "" { + pipelineStr = fmt.Sprintf(pipelineSrc+pipelineStr, pipelineDevice) + } else { + pipelineStr = fmt.Sprintf(audioSrc+"audio/x-raw, rate=8000 ! mulawenc"+pipelineStr, pipelineDevice) + } case webrtc.PCMA: // https://gstreamer.freedesktop.org/documentation/alaw/alawenc.html?gi-language=c // gstreamer1.0-plugins-good // audio/x-raw, rate=8000 ! alawenc - pipelineStr = pipelineSrc + " ! audio/x-raw, rate=8000 ! alawenc ! " + pipelineStr - clockRate = pcmClockRate - if err := CheckPlugins([]string{"pulseaudio", "alaw"}); err != nil { return nil, err } + clockRate = pcmClockRate + + if pipelineSrc != "" { + pipelineStr = fmt.Sprintf(pipelineSrc+pipelineStr, pipelineDevice) + } else { + pipelineStr = fmt.Sprintf(audioSrc+"audio/x-raw, rate=8000 ! alawenc"+pipelineStr, pipelineDevice) + } default: return nil, errors.Errorf("unknown video codec %s", codecName) } @@ -169,16 +199,17 @@ func CreatePipeline(codecName string, pipelineSrc string) (*Pipeline, error) { pipelinesLock.Lock() defer pipelinesLock.Unlock() - pipeline := &Pipeline{ + p := &Pipeline{ Pipeline: C.gstreamer_send_create_pipeline(pipelineStrUnsafe), Sample: make(chan types.Sample), CodecName: codecName, ClockRate: clockRate, + Src: pipelineStr, id: len(pipelines), } - pipelines[pipeline.id] = pipeline - return pipeline, nil + pipelines[p.id] = p + return p, nil } // Start starts the GStreamer Pipeline diff --git a/server/internal/types/event/events.go b/server/internal/types/event/events.go index 723501b3..7b52cc88 100644 --- a/server/internal/types/event/events.go +++ b/server/internal/types/event/events.go @@ -1,33 +1,53 @@ package event -const SYSTEM_DISCONNECT = "system/disconnect" +const ( + SYSTEM_DISCONNECT = "system/disconnect" +) -const SIGNAL_ANSWER = "signal/answer" -const SIGNAL_PROVIDE = "signal/provide" +const ( + SIGNAL_ANSWER = "signal/answer" + SIGNAL_PROVIDE = "signal/provide" +) -const IDENTITY_PROVIDE = "identity/provide" -const IDENTITY_DETAILS = "identity/details" +const ( + IDENTITY_PROVIDE = "identity/provide" + IDENTITY_DETAILS = "identity/details" +) -const MEMBER_LIST = "member/list" -const MEMBER_CONNECTED = "member/connected" -const MEMBER_DISCONNECTED = "member/disconnected" +const ( + MEMBER_LIST = "member/list" + MEMBER_CONNECTED = "member/connected" + MEMBER_DISCONNECTED = "member/disconnected" +) -const CONTROL_LOCKED = "control/locked" -const CONTROL_RELEASE = "control/release" -const CONTROL_REQUEST = "control/request" -const CONTROL_REQUESTING = "control/requesting" -const CONTROL_GIVE = "control/give" -const CONTROL_CLIPBOARD = "control/clipboard" +const ( + CONTROL_LOCKED = "control/locked" + CONTROL_RELEASE = "control/release" + CONTROL_REQUEST = "control/request" + CONTROL_REQUESTING = "control/requesting" + CONTROL_GIVE = "control/give" + CONTROL_CLIPBOARD = "control/clipboard" +) -const CHAT_MESSAGE = "chat/message" -const CHAT_EMOTE = "chat/emote" +const ( + CHAT_MESSAGE = "chat/message" + CHAT_EMOTE = "chat/emote" +) -const ADMIN_BAN = "admin/ban" -const ADMIN_KICK = "admin/kick" -const ADMIN_LOCK = "admin/lock" -const ADMIN_MUTE = "admin/mute" -const ADMIN_UNLOCK = "admin/unlock" -const ADMIN_UNMUTE = "admin/unmute" -const ADMIN_CONTROL = "admin/control" -const ADMIN_RELEASE = "admin/release" -const ADMIN_GIVE = "admin/give" +const ( + SCREEN_CONFIGURATIONS = "screen/configurations" + SCREEN_RESOLUTION = "screen/resolution" + SCREEN_SET = "screen/set" +) + +const ( + ADMIN_BAN = "admin/ban" + ADMIN_KICK = "admin/kick" + ADMIN_LOCK = "admin/lock" + ADMIN_MUTE = "admin/mute" + ADMIN_UNLOCK = "admin/unlock" + ADMIN_UNMUTE = "admin/unmute" + ADMIN_CONTROL = "admin/control" + ADMIN_RELEASE = "admin/release" + ADMIN_GIVE = "admin/give" +) diff --git a/server/internal/types/message/messages.go b/server/internal/types/message/messages.go index 4542b5c6..3cb18e76 100644 --- a/server/internal/types/message/messages.go +++ b/server/internal/types/message/messages.go @@ -90,3 +90,16 @@ type AdminTarget struct { Target string `json:"target"` ID string `json:"id"` } + +type ScreenResolution struct { + Event string `json:"event"` + ID string `json:"id,omitempty"` + Width int `json:"width"` + Height int `json:"height"` + Rate int `json:"rate"` +} + +type ScreenConfigurations struct { + Event string `json:"event"` + Configurations map[int]types.ScreenConfiguration `json:"configurations"` +} diff --git a/server/internal/types/webrtc.go b/server/internal/types/webrtc.go index 76cc5713..c96572e1 100644 --- a/server/internal/types/webrtc.go +++ b/server/internal/types/webrtc.go @@ -9,6 +9,7 @@ type WebRTCManager interface { Start() Shutdown() error CreatePeer(id string, sdp string) (string, Peer, error) + ChangeScreenSize(width int, height int, rate int) error } type Peer interface { diff --git a/server/internal/types/xorg.go b/server/internal/types/xorg.go new file mode 100644 index 00000000..dec51611 --- /dev/null +++ b/server/internal/types/xorg.go @@ -0,0 +1,13 @@ +package types + +type ScreenSize struct { + Width int `json:"width"` + Height int `json:"height"` + Rate int16 `json:"rate"` +} + +type ScreenConfiguration struct { + Width int `json:"width"` + Height int `json:"height"` + Rates map[int]int16 `json:"rates"` +} diff --git a/server/internal/webrtc/handle.go b/server/internal/webrtc/handle.go index 95a3fe19..50490cd6 100644 --- a/server/internal/webrtc/handle.go +++ b/server/internal/webrtc/handle.go @@ -6,7 +6,7 @@ import ( "strconv" "github.com/pion/webrtc/v2" - "n.eko.moe/neko/internal/hid" + "n.eko.moe/neko/internal/xorg" ) const OP_MOVE = 0x01 @@ -63,7 +63,7 @@ func (m *WebRTCManager) handle(id string, msg webrtc.DataChannelMessage) error { return err } - hid.Move(int(payload.X), int(payload.Y)) + xorg.Move(int(payload.X), int(payload.Y)) break case OP_SCROLL: payload := &PayloadScroll{} @@ -77,7 +77,7 @@ func (m *WebRTCManager) handle(id string, msg webrtc.DataChannelMessage) error { Str("y", strconv.Itoa(int(payload.Y))). Msg("scroll") - hid.Scroll(int(payload.X), int(payload.Y)) + xorg.Scroll(int(payload.X), int(payload.Y)) break case OP_KEY_DOWN: payload := &PayloadKey{} @@ -86,7 +86,7 @@ func (m *WebRTCManager) handle(id string, msg webrtc.DataChannelMessage) error { } if payload.Key < 8 { - button, err := hid.ButtonDown(int(payload.Key)) + button, err := xorg.ButtonDown(int(payload.Key)) if err != nil { m.logger.Warn().Err(err).Msg("key down failed") return nil @@ -94,7 +94,7 @@ func (m *WebRTCManager) handle(id string, msg webrtc.DataChannelMessage) error { m.logger.Debug().Msgf("button down %s(%d)", button.Name, payload.Key) } else { - key, err := hid.KeyDown(int(payload.Key)) + key, err := xorg.KeyDown(int(payload.Key)) if err != nil { m.logger.Warn().Err(err).Msg("key down failed") return nil @@ -112,7 +112,7 @@ func (m *WebRTCManager) handle(id string, msg webrtc.DataChannelMessage) error { } if payload.Key < 8 { - button, err := hid.ButtonUp(int(payload.Key)) + button, err := xorg.ButtonUp(int(payload.Key)) if err != nil { m.logger.Warn().Err(err).Msg("button up failed") return nil @@ -120,7 +120,7 @@ func (m *WebRTCManager) handle(id string, msg webrtc.DataChannelMessage) error { m.logger.Debug().Msgf("button up %s(%d)", button.Name, payload.Key) } else { - key, err := hid.KeyUp(int(payload.Key)) + key, err := xorg.KeyUp(int(payload.Key)) if err != nil { m.logger.Warn().Err(err).Msg("keyup failed") return nil diff --git a/server/internal/webrtc/webrtc.go b/server/internal/webrtc/webrtc.go index a25f308b..e194a377 100644 --- a/server/internal/webrtc/webrtc.go +++ b/server/internal/webrtc/webrtc.go @@ -2,6 +2,7 @@ package webrtc import ( "fmt" + "strings" "time" "github.com/pion/webrtc/v2" @@ -9,9 +10,9 @@ import ( "github.com/rs/zerolog/log" "n.eko.moe/neko/internal/gst" - "n.eko.moe/neko/internal/hid" "n.eko.moe/neko/internal/types" "n.eko.moe/neko/internal/types/config" + "n.eko.moe/neko/internal/xorg" ) func New(sessions types.SessionManager, config *config.WebRTC) *WebRTCManager { @@ -53,11 +54,12 @@ type WebRTCManager struct { } func (m *WebRTCManager) Start() { - hid.Display(m.config.Display) + xorg.Display(m.config.Display) videoPipeline, err := gst.CreatePipeline( m.config.VideoCodec, - fmt.Sprintf("ximagesrc xid=%s show-pointer=true use-damage=false ! video/x-raw,framerate=30/1 ! videoconvert ! queue", m.config.Display), + m.config.Display, + m.config.VideoParams, ) if err != nil { @@ -66,7 +68,8 @@ func (m *WebRTCManager) Start() { audioPipeline, err := gst.CreatePipeline( m.config.AudioCodec, - fmt.Sprintf("pulsesrc device=%s ! audioconvert", m.config.Device), + m.config.Device, + m.config.AudioParams, ) if err != nil { @@ -88,22 +91,22 @@ func (m *WebRTCManager) Start() { select { case <-m.shutdown: return - case sample := <-videoPipeline.Sample: + case sample := <-m.videoPipeline.Sample: if err := m.sessions.WriteVideoSample(sample); err != nil { m.logger.Warn().Err(err).Msg("video pipeline failed to write") } - case sample := <-audioPipeline.Sample: + case sample := <-m.audioPipeline.Sample: if err := m.sessions.WriteAudioSample(sample); err != nil { m.logger.Warn().Err(err).Msg("audio pipeline failed to write") } case <-m.cleanup.C: - hid.Check(time.Second * 10) + xorg.CheckKeys(time.Second * 10) } } }() m.sessions.OnHostCleared(func(id string) { - hid.Reset() + xorg.ResetKeys() }) m.sessions.OnCreated(func(id string, session types.Session) { @@ -120,6 +123,10 @@ func (m *WebRTCManager) Start() { Str("video_codec", m.config.VideoCodec). Str("audio_device", m.config.Device). Str("audio_codec", m.config.AudioCodec). + Str("ephemeral_port_range", fmt.Sprintf("%d-%d", m.config.EphemeralMin, m.config.EphemeralMax)). + Str("nat_ips", strings.Join(m.config.NAT1To1IPs, ",")). + Str("audio_pipeline_src", audioPipeline.Src). + Str("video_pipeline_src", videoPipeline.Src). Msgf("webrtc streaming") } @@ -180,7 +187,7 @@ func (m *WebRTCManager) CreatePeer(id string, sdp string) (string, types.Peer, e return "", nil, err } - // set remote description + // Set remote description connection.SetRemoteDescription(description) answer, err := connection.CreateAnswer(nil) @@ -222,3 +229,26 @@ func (m *WebRTCManager) CreatePeer(id string, sdp string) (string, types.Peer, e connection: connection, }, nil } + +func (m *WebRTCManager) ChangeScreenSize(width int, height int, rate int) error { + m.videoPipeline.Stop() + + if err := xorg.ChangeScreenSize(width, height, rate); err != nil { + return err + } + + videoPipeline, err := gst.CreatePipeline( + m.config.VideoCodec, + m.config.Display, + m.config.VideoParams, + ) + + if err != nil { + m.logger.Panic().Err(err).Msg("unable to create new video pipeline") + } + + m.videoPipeline = videoPipeline + m.videoPipeline.Start() + + return nil +} diff --git a/server/internal/websocket/control.go b/server/internal/websocket/control.go index 0b567eaf..44f3dcc8 100644 --- a/server/internal/websocket/control.go +++ b/server/internal/websocket/control.go @@ -1,10 +1,10 @@ package websocket import ( - "n.eko.moe/neko/internal/hid" "n.eko.moe/neko/internal/types" "n.eko.moe/neko/internal/types/event" "n.eko.moe/neko/internal/types/message" + "n.eko.moe/neko/internal/xorg" ) func (h *MessageHandler) controlRelease(id string, session types.Session) error { @@ -113,6 +113,6 @@ func (h *MessageHandler) controlClipboard(id string, session types.Session, payl return nil } - hid.WriteClipboard(payload.Text) + xorg.WriteClipboard(payload.Text) return nil } diff --git a/server/internal/websocket/handler.go b/server/internal/websocket/handler.go index d850271e..a09190ef 100644 --- a/server/internal/websocket/handler.go +++ b/server/internal/websocket/handler.go @@ -103,6 +103,18 @@ func (h *MessageHandler) Message(id string, raw []byte) error { return h.chatEmote(id, session, payload) }), "%s failed", header.Event) + // Screen Events + case event.SCREEN_RESOLUTION: + return errors.Wrapf(h.screenResolution(id, session), "%s failed", header.Event) + case event.SCREEN_CONFIGURATIONS: + return errors.Wrapf(h.screenConfigurations(id, session), "%s failed", header.Event) + case event.SCREEN_SET: + payload := &message.ScreenResolution{} + return errors.Wrapf( + utils.Unmarshal(payload, raw, func() error { + return h.screenSet(id, session, payload) + }), "%s failed", header.Event) + // Admin Events case event.ADMIN_LOCK: return errors.Wrapf(h.adminLock(id, session), "%s failed", header.Event) diff --git a/server/internal/websocket/screen.go b/server/internal/websocket/screen.go new file mode 100644 index 00000000..b178dcfb --- /dev/null +++ b/server/internal/websocket/screen.go @@ -0,0 +1,67 @@ +package websocket + +import ( + "n.eko.moe/neko/internal/types" + "n.eko.moe/neko/internal/types/event" + "n.eko.moe/neko/internal/types/message" + "n.eko.moe/neko/internal/xorg" +) + +func (h *MessageHandler) screenSet(id string, session types.Session, payload *message.ScreenResolution) error { + if !session.Admin() { + h.logger.Debug().Msg("user not admin") + return nil + } + + if err := h.webrtc.ChangeScreenSize(payload.Width, payload.Height, payload.Rate); err != nil { + h.logger.Warn().Err(err).Msgf("unable to change screen size") + return err + } + + if err := h.sessions.Brodcast( + message.ScreenResolution{ + Event: event.SCREEN_RESOLUTION, + ID: id, + Width: payload.Width, + Height: payload.Height, + Rate: payload.Rate, + }, nil); err != nil { + h.logger.Warn().Err(err).Msgf("brodcasting event %s has failed", event.SCREEN_RESOLUTION) + return err + } + + return nil +} + +func (h *MessageHandler) screenResolution(id string, session types.Session) error { + if size := xorg.GetScreenSize(); size != nil { + if err := session.Send(message.ScreenResolution{ + Event: event.SCREEN_RESOLUTION, + Width: size.Width, + Height: size.Height, + Rate: int(size.Rate), + }); err != nil { + h.logger.Warn().Err(err).Msgf("sending event %s has failed", event.SCREEN_RESOLUTION) + return err + } + } + + return nil +} + +func (h *MessageHandler) screenConfigurations(id string, session types.Session) error { + if !session.Admin() { + h.logger.Debug().Msg("user not admin") + return nil + } + + if err := session.Send(message.ScreenConfigurations{ + Event: event.SCREEN_CONFIGURATIONS, + Configurations: xorg.ScreenConfigurations, + }); err != nil { + h.logger.Warn().Err(err).Msgf("sending event %s has failed", event.SCREEN_CONFIGURATIONS) + return err + } + + return nil +} diff --git a/server/internal/websocket/session.go b/server/internal/websocket/session.go index dc307924..58289d7a 100644 --- a/server/internal/websocket/session.go +++ b/server/internal/websocket/session.go @@ -14,6 +14,18 @@ func (h *MessageHandler) SessionCreated(id string, session types.Session) error return err } + // send screen current resolution + if err := h.screenResolution(id, session); err != nil { + return err + } + + if session.Admin() { + // send screen configurations + if err := h.screenConfigurations(id, session); err != nil { + return err + } + } + return nil } diff --git a/server/internal/websocket/websocket.go b/server/internal/websocket/websocket.go index b22bc9c4..f3da856b 100644 --- a/server/internal/websocket/websocket.go +++ b/server/internal/websocket/websocket.go @@ -9,12 +9,12 @@ import ( "github.com/rs/zerolog" "github.com/rs/zerolog/log" - "n.eko.moe/neko/internal/hid" "n.eko.moe/neko/internal/types" "n.eko.moe/neko/internal/types/config" "n.eko.moe/neko/internal/types/event" "n.eko.moe/neko/internal/types/message" "n.eko.moe/neko/internal/utils" + "n.eko.moe/neko/internal/xorg" ) func New(sessions types.SessionManager, webrtc types.WebRTCManager, conf *config.WebSocket) *WebSocketHandler { @@ -81,7 +81,7 @@ func (ws *WebSocketHandler) Start() error { ws.logger.Info().Msg("shutdown") }() - current := hid.ReadClipboard() + current := xorg.ReadClipboard() for { select { @@ -89,7 +89,7 @@ func (ws *WebSocketHandler) Start() error { return default: if ws.sessions.HasHost() { - text := hid.ReadClipboard() + text := xorg.ReadClipboard() if text != current { session, ok := ws.sessions.GetHost() if ok { diff --git a/server/internal/hid/keycode/button.go b/server/internal/xorg/keycode/button.go similarity index 100% rename from server/internal/hid/keycode/button.go rename to server/internal/xorg/keycode/button.go diff --git a/server/internal/hid/keycode/keys.go b/server/internal/xorg/keycode/keys.go similarity index 100% rename from server/internal/hid/keycode/keys.go rename to server/internal/xorg/keycode/keys.go diff --git a/server/internal/hid/hid.c b/server/internal/xorg/xorg.c similarity index 62% rename from server/internal/hid/hid.c rename to server/internal/xorg/xorg.c index 934b1132..a835f7a0 100644 --- a/server/internal/hid/hid.c +++ b/server/internal/xorg/xorg.c @@ -1,4 +1,4 @@ -#include "hid.h" +#include "xorg.h" static clipboard_c *CLIPBOARD = NULL; static Display *DISPLAY = NULL; @@ -9,7 +9,7 @@ static int DIRTY = 0; Display *getXDisplay(void) { /* Close the display if displayName has changed */ if (DIRTY) { - closeXDisplay(); + XDisplayClose(); DIRTY = 0; } @@ -25,7 +25,7 @@ Display *getXDisplay(void) { if (DISPLAY == NULL) { fputs("Could not open main display\n", stderr); } else if (!REGISTERED) { - atexit(&closeXDisplay); + atexit(&XDisplayClose); REGISTERED = 1; } } @@ -40,14 +40,14 @@ clipboard_c *getClipboard(void) { return CLIPBOARD; } -void closeXDisplay(void) { +void XDisplayClose(void) { if (DISPLAY != NULL) { XCloseDisplay(DISPLAY); DISPLAY = NULL; } } -void setXDisplay(char *input) { +void XDisplaySet(char *input) { NAME = strdup(input); DIRTY = 1; } @@ -110,3 +110,41 @@ char *XClipboardGet() { clipboard_c *cb = getClipboard(); return clipboard_text_ex(cb, NULL, 0); } + +void XGetScreenConfigurations() { + Display *display = getXDisplay(); + Window root = RootWindow(display, 0); + XRRScreenSize *xrrs; + int num_sizes; + + xrrs = XRRSizes(display, 0, &num_sizes); + for(int i = 0; i < num_sizes; i ++) { + short *rates; + int num_rates; + + goCreateScreenSize(i, xrrs[i].width, xrrs[i].height, xrrs[i].mwidth, xrrs[i].mheight); + rates = XRRRates(display, 0, i, &num_rates); + for (int j = 0; j < num_rates; j ++) { + goSetScreenRates(i, j, rates[j]); + } + } +} + +void XSetScreenConfiguration(int index, short rate) { + Display *display = getXDisplay(); + Window root = RootWindow(display, 0); + XRRSetScreenConfigAndRate(display, XRRGetScreenInfo(display, root), root, index, RR_Rotate_0, rate, CurrentTime); +} + +int XGetScreenSize() { + Display *display = getXDisplay(); + XRRScreenConfiguration *conf = XRRGetScreenInfo(display, RootWindow(display, 0)); + Rotation original_rotation; + return XRRConfigCurrentConfiguration(conf, &original_rotation); +} + +short XGetScreenRate() { + Display *display = getXDisplay(); + XRRScreenConfiguration *conf = XRRGetScreenInfo(display, RootWindow(display, 0)); + return XRRConfigCurrentRate(conf); +} diff --git a/server/internal/hid/hid.go b/server/internal/xorg/xorg.go similarity index 80% rename from server/internal/hid/hid.go rename to server/internal/xorg/xorg.go index e4b37551..3de6a8d8 100644 --- a/server/internal/hid/hid.go +++ b/server/internal/xorg/xorg.go @@ -4,13 +4,13 @@ // pretty sure this *isn't* thread safe either.... /shrug // if you know a better way to get this done *please* make a pr <3 -package hid +package xorg /* #cgo linux CFLAGS: -I/usr/src -#cgo linux LDFLAGS: -L/usr/src -lX11 -lXtst -lclipboard +#cgo linux LDFLAGS: -L/usr/src -lX11 -lXtst -lXrandr -lclipboard -#include "hid.h" +#include "xorg.h" */ import "C" @@ -18,10 +18,14 @@ import ( "fmt" "sync" "time" + "unsafe" - "n.eko.moe/neko/internal/hid/keycode" + "n.eko.moe/neko/internal/types" + "n.eko.moe/neko/internal/xorg/keycode" ) +var ScreenConfigurations = make(map[int]types.ScreenConfiguration) + var debounce = make(map[int]time.Time) var buttons = make(map[int]keycode.Button) var keys = make(map[int]keycode.Key) @@ -135,13 +139,18 @@ func init() { buttons[keycode.SCROLL_DOWN_BUTTON.Code] = keycode.SCROLL_DOWN_BUTTON buttons[keycode.SCROLL_LEFT_BUTTON.Code] = keycode.SCROLL_LEFT_BUTTON buttons[keycode.SCROLL_RIGHT_BUTTON.Code] = keycode.SCROLL_RIGHT_BUTTON + + C.XGetScreenConfigurations() } func Display(display string) { mu.Lock() defer mu.Unlock() - C.setXDisplay(C.CString(display)) + displayUnsafe := C.CString(display) + defer C.free(unsafe.Pointer(displayUnsafe)) + + C.XDisplaySet(displayUnsafe) } func Move(x, y int) { @@ -238,17 +247,23 @@ func ReadClipboard() string { mu.Lock() defer mu.Unlock() - return C.GoString(C.XClipboardGet()) + clipboardUnsafe := C.XClipboardGet() + defer C.free(unsafe.Pointer(clipboardUnsafe)) + + return C.GoString(clipboardUnsafe) } func WriteClipboard(data string) { mu.Lock() defer mu.Unlock() - C.XClipboardSet(C.CString(data)) + clipboardUnsafe := C.CString(data) + defer C.free(unsafe.Pointer(clipboardUnsafe)) + + C.XClipboardSet(clipboardUnsafe) } -func Reset() { +func ResetKeys() { for key := range debounce { if key < 8 { ButtonUp(key) @@ -260,7 +275,7 @@ func Reset() { } } -func Check(duration time.Duration) { +func CheckKeys(duration time.Duration) { t := time.Now() for key, start := range debounce { if t.Sub(start) < duration { @@ -276,3 +291,53 @@ func Check(duration time.Duration) { delete(debounce, key) } } + +func ChangeScreenSize(width int, height int, rate int) error { + mu.Lock() + defer mu.Unlock() + + for index, size := range ScreenConfigurations { + if size.Width == width && size.Height == height { + for _, srate := range size.Rates { + if int16(rate) == srate { + C.XSetScreenConfiguration(C.int(index), C.short(srate)) + return nil + } + } + } + } + + return fmt.Errorf("unknown configuration") +} + +func GetScreenSize() *types.ScreenSize { + mu.Lock() + defer mu.Unlock() + + index := int(C.XGetScreenSize()) + rate := int16(C.XGetScreenRate()) + + if conf, ok := ScreenConfigurations[index]; ok { + return &types.ScreenSize{ + Width: conf.Width, + Height: conf.Height, + Rate: rate, + } + } + + return nil +} + +//export goCreateScreenSize +func goCreateScreenSize(index C.int, width C.int, height C.int, mwidth C.int, mheight C.int) { + ScreenConfigurations[int(index)] = types.ScreenConfiguration{ + Width: int(width), + Height: int(height), + Rates: make(map[int]int16), + } +} + +//export goSetScreenRates +func goSetScreenRates(index C.int, rate_index C.int, rate C.short) { + ScreenConfigurations[int(index)].Rates[int(rate_index)] = int16(rate) +} diff --git a/server/internal/hid/hid.h b/server/internal/xorg/xorg.h similarity index 67% rename from server/internal/hid/hid.h rename to server/internal/xorg/xorg.h index 18a0f7ed..e29b95d8 100644 --- a/server/internal/hid/hid.h +++ b/server/internal/xorg/xorg.h @@ -4,6 +4,7 @@ #define XDISPLAY_H #include + #include #include #include #include @@ -11,6 +12,9 @@ #include /* For fputs() */ #include /* For strdup() */ + extern void goCreateScreenSize(int index, int width, int height, int mwidth, int mheight); + extern void goSetScreenRates(int index, int rate_index, short rate); + /* Returns the main display, closed either on exit or when closeMainDisplay() * is invoked. This removes a bit of the overhead of calling XOpenDisplay() & * XCloseDisplay() everytime the main display needs to be used. @@ -19,14 +23,20 @@ Display *getXDisplay(void); clipboard_c *getClipboard(void); - void XClipboardSet(char *src); - char *XClipboardGet(); void XMove(int x, int y); void XScroll(int x, int y); void XButton(unsigned int button, int down); void XKey(unsigned long key, int down); - void closeXDisplay(void); - void setXDisplay(char *input); + void XClipboardSet(char *src); + char *XClipboardGet(); + + void XGetScreenConfigurations(); + void XSetScreenConfiguration(int index, short rate); + int XGetScreenSize(); + short XGetScreenRate(); + + void XDisplayClose(void); + void XDisplaySet(char *input); #endif