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 @@
+
+
+
+
+
+ {{ conf.width }}x{{ conf.height }}
+ {{ rate }}
+
+
+
+
+
+
+
+
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