move shared code to pkg.

This commit is contained in:
Miroslav Šedivý
2022-03-20 11:43:00 +01:00
parent 94c17e9a42
commit 8593d2d0fd
93 changed files with 132 additions and 133 deletions

67
pkg/auth/auth.go Normal file
View File

@ -0,0 +1,67 @@
package auth
import (
"context"
"net/http"
"gitlab.com/demodesk/neko/server/pkg/types"
"gitlab.com/demodesk/neko/server/pkg/utils"
)
type key int
const keySessionCtx key = iota
func SetSession(r *http.Request, session types.Session) context.Context {
return context.WithValue(r.Context(), keySessionCtx, session)
}
func GetSession(r *http.Request) (types.Session, bool) {
session, ok := r.Context().Value(keySessionCtx).(types.Session)
return session, ok
}
func AdminsOnly(w http.ResponseWriter, r *http.Request) (context.Context, error) {
session, ok := GetSession(r)
if !ok || !session.Profile().IsAdmin {
return nil, utils.HttpForbidden("session is not admin")
}
return nil, nil
}
func HostsOnly(w http.ResponseWriter, r *http.Request) (context.Context, error) {
session, ok := GetSession(r)
if !ok || !session.IsHost() {
return nil, utils.HttpForbidden("session is not host")
}
return nil, nil
}
func CanWatchOnly(w http.ResponseWriter, r *http.Request) (context.Context, error) {
session, ok := GetSession(r)
if !ok || !session.Profile().CanWatch {
return nil, utils.HttpForbidden("session cannot watch")
}
return nil, nil
}
func CanHostOnly(w http.ResponseWriter, r *http.Request) (context.Context, error) {
session, ok := GetSession(r)
if !ok || !session.Profile().CanHost {
return nil, utils.HttpForbidden("session cannot host")
}
return nil, nil
}
func CanAccessClipboardOnly(w http.ResponseWriter, r *http.Request) (context.Context, error) {
session, ok := GetSession(r)
if !ok || !session.Profile().CanAccessClipboard {
return nil, utils.HttpForbidden("session cannot access clipboard")
}
return nil, nil
}

93
pkg/drop/drop.c Normal file
View File

@ -0,0 +1,93 @@
#include "drop.h"
GtkWidget *drag_widget = NULL;
static void dragDataGet(
GtkWidget *widget,
GdkDragContext *context,
GtkSelectionData *data,
guint target_type,
guint time,
gpointer user_data
) {
gchar **uris = (gchar **) user_data;
if (target_type == DRAG_TARGET_TYPE_URI) {
gtk_selection_data_set_uris(data, uris);
return;
}
if (target_type == DRAG_TARGET_TYPE_TEXT) {
gtk_selection_data_set_text(data, uris[0], -1);
return;
}
}
static void dragEnd(
GtkWidget *widget,
GdkDragContext *context,
gpointer user_data
) {
gboolean succeeded = gdk_drag_drop_succeeded(context);
gtk_widget_destroy(widget);
goDragFinish(succeeded);
drag_widget = NULL;
}
void dragWindowOpen(char **uris) {
if (drag_widget != NULL) dragWindowClose();
gtk_init(NULL, NULL);
GtkWidget *widget = gtk_window_new(GTK_WINDOW_TOPLEVEL);;
GtkWindow *window = GTK_WINDOW(widget);
gtk_window_move(window, 0, 0);
gtk_window_set_title(window, "Neko Drag & Drop Window");
gtk_window_set_decorated(window, FALSE);
gtk_window_set_keep_above(window, TRUE);
gtk_window_set_default_size(window, 100, 100);
GtkTargetList* target_list = gtk_target_list_new(NULL, 0);
gtk_target_list_add_uri_targets(target_list, DRAG_TARGET_TYPE_URI);
gtk_target_list_add_text_targets(target_list, DRAG_TARGET_TYPE_TEXT);
gtk_drag_source_set(widget, GDK_BUTTON1_MASK, NULL, 0, GDK_ACTION_COPY | GDK_ACTION_LINK | GDK_ACTION_ASK);
gtk_drag_source_set_target_list(widget, target_list);
g_signal_connect(widget, "map-event", G_CALLBACK(goDragCreate), NULL);
g_signal_connect(widget, "enter-notify-event", G_CALLBACK(goDragCursorEnter), NULL);
g_signal_connect(widget, "button-press-event", G_CALLBACK(goDragButtonPress), NULL);
g_signal_connect(widget, "drag-begin", G_CALLBACK(goDragBegin), NULL);
g_signal_connect(widget, "drag-data-get", G_CALLBACK(dragDataGet), uris);
g_signal_connect(widget, "drag-end", G_CALLBACK(dragEnd), NULL);
g_signal_connect(window, "destroy", G_CALLBACK(gtk_main_quit), NULL);
gtk_widget_show_all(widget);
drag_widget = widget;
gtk_main();
}
void dragWindowClose() {
gtk_widget_destroy(drag_widget);
drag_widget = NULL;
}
char **dragUrisMake(int size) {
return calloc(size + 1, sizeof(char *));
}
void dragUrisSetFile(char **uris, char *file, int n) {
GFile *gfile = g_file_new_for_path(file);
uris[n] = g_file_get_uri(gfile);
}
void dragUrisFree(char **uris, int size) {
for (int i = 0; i < size; i++) {
free(uris[i]);
}
free(uris);
}

65
pkg/drop/drop.go Normal file
View File

@ -0,0 +1,65 @@
package drop
/*
#cgo pkg-config: gtk+-3.0
#include "drop.h"
*/
import "C"
import (
"sync"
"github.com/kataras/go-events"
)
var Emmiter events.EventEmmiter
var mu = sync.Mutex{}
func init() {
Emmiter = events.New()
}
func OpenWindow(files []string) {
mu.Lock()
defer mu.Unlock()
size := C.int(len(files))
urisUnsafe := C.dragUrisMake(size)
defer C.dragUrisFree(urisUnsafe, size)
for i, file := range files {
C.dragUrisSetFile(urisUnsafe, C.CString(file), C.int(i))
}
C.dragWindowOpen(urisUnsafe)
}
func CloseWindow() {
C.dragWindowClose()
}
//export goDragCreate
func goDragCreate(widget *C.GtkWidget, event *C.GdkEvent, user_data C.gpointer) {
go Emmiter.Emit("create")
}
//export goDragCursorEnter
func goDragCursorEnter(widget *C.GtkWidget, event *C.GdkEvent, user_data C.gpointer) {
go Emmiter.Emit("cursor-enter")
}
//export goDragButtonPress
func goDragButtonPress(widget *C.GtkWidget, event *C.GdkEvent, user_data C.gpointer) {
go Emmiter.Emit("button-press")
}
//export goDragBegin
func goDragBegin(widget *C.GtkWidget, context *C.GdkDragContext, user_data C.gpointer) {
go Emmiter.Emit("begin")
}
//export goDragFinish
func goDragFinish(succeeded C.gboolean) {
go Emmiter.Emit("finish", bool(succeeded == C.int(1)))
}

36
pkg/drop/drop.h Normal file
View File

@ -0,0 +1,36 @@
#pragma once
#include <gtk/gtk.h>
enum {
DRAG_TARGET_TYPE_TEXT,
DRAG_TARGET_TYPE_URI
};
extern void goDragCreate(GtkWidget *widget, GdkEvent *event, gpointer user_data);
extern void goDragCursorEnter(GtkWidget *widget, GdkEvent *event, gpointer user_data);
extern void goDragButtonPress(GtkWidget *widget, GdkEvent *event, gpointer user_data);
extern void goDragBegin(GtkWidget *widget, GdkDragContext *context, gpointer user_data);
extern void goDragFinish(gboolean succeeded);
static void dragDataGet(
GtkWidget *widget,
GdkDragContext *context,
GtkSelectionData *data,
guint target_type,
guint time,
gpointer user_data
);
static void dragEnd(
GtkWidget *widget,
GdkDragContext *context,
gpointer user_data
);
void dragWindowOpen(char **uris);
void dragWindowClose();
char **dragUrisMake(int size);
void dragUrisSetFile(char **uris, char *file, int n);
void dragUrisFree(char **uris, int size);

165
pkg/gst/gst.c Normal file
View File

@ -0,0 +1,165 @@
#include "gst.h"
void gstreamer_init(void) {
gst_init(NULL, NULL);
}
GMainLoop *gstreamer_main_loop = NULL;
void gstreamer_loop(void) {
gstreamer_main_loop = g_main_loop_new (NULL, FALSE);
g_main_loop_run(gstreamer_main_loop);
}
static void gstreamer_pipeline_log(GstPipelineCtx *ctx, char* level, const char* format, ...) {
va_list argptr;
va_start(argptr, format);
char buffer[100];
vsprintf(buffer, format, argptr);
va_end(argptr);
goPipelineLog(level, buffer, ctx->pipelineId);
}
static gboolean gstreamer_bus_call(GstBus *bus, GstMessage *msg, gpointer user_data) {
GstPipelineCtx *ctx = (GstPipelineCtx *)user_data;
switch (GST_MESSAGE_TYPE(msg)) {
case GST_MESSAGE_EOS: {
gstreamer_pipeline_log(ctx, "fatal", "end of stream");
break;
}
case GST_MESSAGE_STATE_CHANGED: {
GstState old_state, new_state;
gst_message_parse_state_changed(msg, &old_state, &new_state, NULL);
gstreamer_pipeline_log(ctx, "debug",
"element %s changed state from %s to %s",
GST_OBJECT_NAME(msg->src),
gst_element_state_get_name(old_state),
gst_element_state_get_name(new_state));
break;
}
case GST_MESSAGE_TAG: {
GstTagList *tags = NULL;
gst_message_parse_tag(msg, &tags);
gstreamer_pipeline_log(ctx, "debug",
"got tags from element %s",
GST_OBJECT_NAME(msg->src));
gst_tag_list_unref(tags);
break;
}
case GST_MESSAGE_ERROR: {
GError *err = NULL;
gchar *dbg_info = NULL;
gst_message_parse_error(msg, &err, &dbg_info);
gstreamer_pipeline_log(ctx, "error",
"error from element %s: %s",
GST_OBJECT_NAME(msg->src), err->message);
gstreamer_pipeline_log(ctx, "warn",
"debugging info: %s",
(dbg_info) ? dbg_info : "none");
g_error_free(err);
g_free(dbg_info);
break;
}
default:
gstreamer_pipeline_log(ctx, "trace", "unknown message");
break;
}
return TRUE;
}
GstPipelineCtx *gstreamer_pipeline_create(char *pipelineStr, int pipelineId, GError **error) {
GstElement *pipeline = gst_parse_launch(pipelineStr, error);
if (pipeline == NULL) return NULL;
// create gstreamer pipeline context
GstPipelineCtx *ctx = calloc(1, sizeof(GstPipelineCtx));
ctx->pipelineId = pipelineId;
ctx->pipeline = pipeline;
GstBus *bus = gst_pipeline_get_bus(GST_PIPELINE(pipeline));
gst_bus_add_watch(bus, gstreamer_bus_call, ctx);
gst_object_unref(bus);
return ctx;
}
static GstFlowReturn gstreamer_send_new_sample_handler(GstElement *object, gpointer user_data) {
GstPipelineCtx *ctx = (GstPipelineCtx *)user_data;
GstSample *sample = NULL;
GstBuffer *buffer = NULL;
gpointer copy = NULL;
gsize copy_size = 0;
g_signal_emit_by_name(object, "pull-sample", &sample);
if (sample) {
buffer = gst_sample_get_buffer(sample);
if (buffer) {
gst_buffer_extract_dup(buffer, 0, gst_buffer_get_size(buffer), &copy, &copy_size);
goHandlePipelineBuffer(copy, copy_size, GST_BUFFER_DURATION(buffer), ctx->pipelineId);
}
gst_sample_unref(sample);
}
return GST_FLOW_OK;
}
void gstreamer_pipeline_attach_appsink(GstPipelineCtx *ctx, char *sinkName) {
ctx->appsink = gst_bin_get_by_name(GST_BIN(ctx->pipeline), sinkName);
g_object_set(ctx->appsink, "emit-signals", TRUE, NULL);
g_signal_connect(ctx->appsink, "new-sample", G_CALLBACK(gstreamer_send_new_sample_handler), ctx);
}
void gstreamer_pipeline_attach_appsrc(GstPipelineCtx *ctx, char *srcName) {
ctx->appsrc = gst_bin_get_by_name(GST_BIN(ctx->pipeline), srcName);
}
void gstreamer_pipeline_play(GstPipelineCtx *ctx) {
gst_element_set_state(GST_ELEMENT(ctx->pipeline), GST_STATE_PLAYING);
}
void gstreamer_pipeline_pause(GstPipelineCtx *ctx) {
gst_element_set_state(GST_ELEMENT(ctx->pipeline), GST_STATE_PAUSED);
}
void gstreamer_pipeline_destory(GstPipelineCtx *ctx) {
// end appsrc, if exists
if (ctx->appsrc) {
gst_app_src_end_of_stream(GST_APP_SRC(ctx->appsrc));
}
// send pipeline eos
gst_element_send_event(GST_ELEMENT(ctx->pipeline), gst_event_new_eos());
// set null state
gst_element_set_state(GST_ELEMENT(ctx->pipeline), GST_STATE_NULL);
if (ctx->appsink) {
gst_object_unref(ctx->appsink);
ctx->appsink = NULL;
}
if (ctx->appsrc) {
gst_object_unref(ctx->appsrc);
ctx->appsrc = NULL;
}
gst_object_unref(ctx->pipeline);
}
void gstreamer_pipeline_push(GstPipelineCtx *ctx, void *buffer, int bufferLen) {
if (ctx->appsrc != NULL) {
gpointer p = g_memdup(buffer, bufferLen);
GstBuffer *buffer = gst_buffer_new_wrapped(p, bufferLen);
gst_app_src_push_buffer(GST_APP_SRC(ctx->appsrc), buffer);
}
}

158
pkg/gst/gst.go Normal file
View File

@ -0,0 +1,158 @@
package gst
/*
#cgo pkg-config: gstreamer-1.0 gstreamer-app-1.0
#include "gst.h"
*/
import "C"
import (
"fmt"
"sync"
"sync/atomic"
"time"
"unsafe"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"gitlab.com/demodesk/neko/server/pkg/types"
)
type Pipeline struct {
id int
Src string
Ctx *C.GstPipelineCtx
Sample chan types.Sample
}
var pSerial int32
var pipelines = make(map[int]*Pipeline)
var pipelinesLock sync.Mutex
var registry *C.GstRegistry
func init() {
C.gstreamer_init()
go C.gstreamer_loop()
registry = C.gst_registry_get()
}
func CreatePipeline(pipelineStr string) (*Pipeline, error) {
id := atomic.AddInt32(&pSerial, 1)
pipelineStrUnsafe := C.CString(pipelineStr)
defer C.free(unsafe.Pointer(pipelineStrUnsafe))
pipelinesLock.Lock()
defer pipelinesLock.Unlock()
var gstError *C.GError
ctx := C.gstreamer_pipeline_create(pipelineStrUnsafe, C.int(id), &gstError)
if gstError != nil {
defer C.g_error_free(gstError)
return nil, fmt.Errorf("(pipeline error) %s", C.GoString(gstError.message))
}
p := &Pipeline{
id: int(id),
Src: pipelineStr,
Ctx: ctx,
Sample: make(chan types.Sample),
}
pipelines[p.id] = p
return p, nil
}
func (p *Pipeline) AttachAppsink(sinkName string) {
sinkNameUnsafe := C.CString(sinkName)
defer C.free(unsafe.Pointer(sinkNameUnsafe))
C.gstreamer_pipeline_attach_appsink(p.Ctx, sinkNameUnsafe)
}
func (p *Pipeline) AttachAppsrc(srcName string) {
srcNameUnsafe := C.CString(srcName)
defer C.free(unsafe.Pointer(srcNameUnsafe))
C.gstreamer_pipeline_attach_appsrc(p.Ctx, srcNameUnsafe)
}
func (p *Pipeline) Play() {
C.gstreamer_pipeline_play(p.Ctx)
}
func (p *Pipeline) Pause() {
C.gstreamer_pipeline_pause(p.Ctx)
}
func (p *Pipeline) Destroy() {
C.gstreamer_pipeline_destory(p.Ctx)
pipelinesLock.Lock()
delete(pipelines, p.id)
pipelinesLock.Unlock()
close(p.Sample)
C.free(unsafe.Pointer(p.Ctx))
p = nil
}
func (p *Pipeline) Push(buffer []byte) {
bytes := C.CBytes(buffer)
defer C.free(bytes)
C.gstreamer_pipeline_push(p.Ctx, bytes, C.int(len(buffer)))
}
// gst-inspect-1.0
func CheckPlugins(plugins []string) error {
var plugin *C.GstPlugin
for _, pluginstr := range plugins {
plugincstr := C.CString(pluginstr)
plugin = C.gst_registry_find_plugin(registry, plugincstr)
C.free(unsafe.Pointer(plugincstr))
if plugin == nil {
return fmt.Errorf("required gstreamer plugin %s not found", pluginstr)
}
}
return nil
}
//export goHandlePipelineBuffer
func goHandlePipelineBuffer(buffer unsafe.Pointer, bufferLen C.int, duration C.int, pipelineID C.int) {
defer C.free(buffer)
pipelinesLock.Lock()
pipeline, ok := pipelines[int(pipelineID)]
pipelinesLock.Unlock()
if ok {
pipeline.Sample <- types.Sample{
Data: C.GoBytes(buffer, bufferLen),
Duration: time.Duration(duration),
}
} else {
log.Warn().
Str("module", "capture").
Str("submodule", "gstreamer").
Int("pipeline_id", int(pipelineID)).
Msgf("discarding sample, pipeline not found")
}
}
//export goPipelineLog
func goPipelineLog(levelUnsafe *C.char, msgUnsafe *C.char, pipelineID C.int) {
levelStr := C.GoString(levelUnsafe)
msg := C.GoString(msgUnsafe)
level, _ := zerolog.ParseLevel(levelStr)
log.WithLevel(level).
Str("module", "capture").
Str("submodule", "gstreamer").
Int("pipeline_id", int(pipelineID)).
Msg(msg)
}

26
pkg/gst/gst.h Normal file
View File

@ -0,0 +1,26 @@
#pragma once
#include <stdio.h>
#include <gst/gst.h>
#include <gst/app/gstappsrc.h>
typedef struct GstPipelineCtx {
int pipelineId;
GstElement *pipeline;
GstElement *appsink;
GstElement *appsrc;
} GstPipelineCtx;
extern void goHandlePipelineBuffer(void *buffer, int bufferLen, int samples, int pipelineId);
extern void goPipelineLog(char *level, char *msg, int pipelineId);
GstPipelineCtx *gstreamer_pipeline_create(char *pipelineStr, int pipelineId, GError **error);
void gstreamer_pipeline_attach_appsink(GstPipelineCtx *ctx, char *sinkName);
void gstreamer_pipeline_attach_appsrc(GstPipelineCtx *ctx, char *srcName);
void gstreamer_pipeline_play(GstPipelineCtx *ctx);
void gstreamer_pipeline_pause(GstPipelineCtx *ctx);
void gstreamer_pipeline_destory(GstPipelineCtx *ctx);
void gstreamer_pipeline_push(GstPipelineCtx *ctx, void *buffer, int bufferLen);
void gstreamer_init(void);
void gstreamer_loop(void);

6
pkg/types/api.go Normal file
View File

@ -0,0 +1,6 @@
package types
type ApiManager interface {
Route(r Router)
AddRouter(path string, router func(Router))
}

163
pkg/types/capture.go Normal file
View File

@ -0,0 +1,163 @@
package types
import (
"context"
"errors"
"fmt"
"math"
"strings"
"github.com/PaesslerAG/gval"
"github.com/pion/webrtc/v3/pkg/media"
"gitlab.com/demodesk/neko/server/pkg/types/codec"
)
var (
ErrCapturePipelineAlreadyExists = errors.New("capture pipeline already exists")
)
type Sample media.Sample
type BroadcastManager interface {
Start(url string) error
Stop()
Started() bool
Url() string
}
type ScreencastManager interface {
Enabled() bool
Started() bool
Image() ([]byte, error)
}
type StreamSinkManager interface {
Codec() codec.RTPCodec
AddListener(listener *func(sample Sample)) error
RemoveListener(listener *func(sample Sample)) error
MoveListenerTo(listener *func(sample Sample), targetStream StreamSinkManager) error
ListenersCount() int
Started() bool
}
type StreamSrcManager interface {
Codec() codec.RTPCodec
Start(codec codec.RTPCodec) error
Stop()
Push(bytes []byte)
Started() bool
}
type CaptureManager interface {
Start()
Shutdown() error
Broadcast() BroadcastManager
Screencast() ScreencastManager
Audio() StreamSinkManager
Video(videoID string) (StreamSinkManager, bool)
VideoIDs() []string
Webcam() StreamSrcManager
Microphone() StreamSrcManager
}
type VideoConfig struct {
Width string `mapstructure:"width"` // expression
Height string `mapstructure:"height"` // expression
Fps string `mapstructure:"fps"` // expression
GstPrefix string `mapstructure:"gst_prefix"` // pipeline prefix, starts with !
GstEncoder string `mapstructure:"gst_encoder"` // gst encoder name
GstParams map[string]string `mapstructure:"gst_params"` // map of expressions
GstSuffix string `mapstructure:"gst_suffix"` // pipeline suffix, starts with !
GstPipeline string `mapstructure:"gst_pipeline"` // whole pipeline as a string
}
func (config *VideoConfig) GetPipeline(screen ScreenSize) (string, error) {
values := map[string]interface{}{
"width": screen.Width,
"height": screen.Height,
"fps": screen.Rate,
}
language := []gval.Language{
gval.Function("round", func(args ...interface{}) (interface{}, error) {
return (int)(math.Round(args[0].(float64))), nil
}),
}
// get fps pipeline
fpsPipeline := "! video/x-raw ! videoconvert ! queue"
if config.Fps != "" {
eval, err := gval.Full(language...).NewEvaluable(config.Fps)
if err != nil {
return "", err
}
val, err := eval.EvalFloat64(context.Background(), values)
if err != nil {
return "", err
}
fpsPipeline = fmt.Sprintf("! video/x-raw,framerate=%d/100 ! videoconvert ! queue", int(val*100))
}
// get scale pipeline
scalePipeline := ""
if config.Width != "" && config.Height != "" {
eval, err := gval.Full(language...).NewEvaluable(config.Width)
if err != nil {
return "", err
}
w, err := eval.EvalInt(context.Background(), values)
if err != nil {
return "", err
}
eval, err = gval.Full(language...).NewEvaluable(config.Height)
if err != nil {
return "", err
}
h, err := eval.EvalInt(context.Background(), values)
if err != nil {
return "", err
}
scalePipeline = fmt.Sprintf("! videoscale ! video/x-raw,width=%d,height=%d ! queue", w, h)
}
// get encoder pipeline
encPipeline := fmt.Sprintf("! %s", config.GstEncoder)
for key, expr := range config.GstParams {
if expr == "" {
continue
}
val, err := gval.Evaluate(expr, values, language...)
if err != nil {
return "", err
}
if val != nil {
encPipeline += fmt.Sprintf(" %s=%v", key, val)
} else {
encPipeline += fmt.Sprintf(" %s=%s", key, expr)
}
}
// join strings with space
return strings.Join([]string{
fpsPipeline,
scalePipeline,
config.GstPrefix,
encPipeline,
config.GstSuffix,
}[:], " "), nil
}

183
pkg/types/codec/codecs.go Normal file
View File

@ -0,0 +1,183 @@
package codec
import (
"strings"
"github.com/pion/webrtc/v3"
)
func ParseRTC(codec webrtc.RTPCodecParameters) (RTPCodec, bool) {
codecName := strings.Split(codec.RTPCodecCapability.MimeType, "/")[1]
return ParseStr(codecName)
}
func ParseStr(codecName string) (codec RTPCodec, ok bool) {
ok = true
switch strings.ToLower(codecName) {
case VP8().Name:
codec = VP8()
case VP9().Name:
codec = VP9()
case H264().Name:
codec = H264()
case Opus().Name:
codec = Opus()
case G722().Name:
codec = G722()
case PCMU().Name:
codec = PCMU()
case PCMA().Name:
codec = PCMA()
default:
ok = false
}
return
}
type RTPCodec struct {
Name string
PayloadType webrtc.PayloadType
Type webrtc.RTPCodecType
Capability webrtc.RTPCodecCapability
Pipeline string
}
func (codec *RTPCodec) Register(engine *webrtc.MediaEngine) error {
return engine.RegisterCodec(webrtc.RTPCodecParameters{
RTPCodecCapability: codec.Capability,
PayloadType: codec.PayloadType,
}, codec.Type)
}
func VP8() RTPCodec {
return RTPCodec{
Name: "vp8",
PayloadType: 96,
Type: webrtc.RTPCodecTypeVideo,
Capability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeVP8,
ClockRate: 90000,
Channels: 0,
SDPFmtpLine: "",
RTCPFeedback: []webrtc.RTCPFeedback{},
},
// https://gstreamer.freedesktop.org/documentation/vpx/vp8enc.html
// gstreamer1.0-plugins-good
Pipeline: "vp8enc cpu-used=16 threads=4 deadline=1 error-resilient=partitions keyframe-max-dist=15 static-threshold=20",
}
}
// TODO: Profile ID.
func VP9() RTPCodec {
return RTPCodec{
Name: "vp9",
PayloadType: 98,
Type: webrtc.RTPCodecTypeVideo,
Capability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeVP9,
ClockRate: 90000,
Channels: 0,
SDPFmtpLine: "profile-id=0",
RTCPFeedback: []webrtc.RTCPFeedback{},
},
// https://gstreamer.freedesktop.org/documentation/vpx/vp9enc.html
// gstreamer1.0-plugins-good
Pipeline: "vp9enc cpu-used=16 threads=4 deadline=1 keyframe-max-dist=15 static-threshold=20",
}
}
// TODO: Profile ID.
func H264() RTPCodec {
return RTPCodec{
Name: "h264",
PayloadType: 102,
Type: webrtc.RTPCodecTypeVideo,
Capability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeH264,
ClockRate: 90000,
Channels: 0,
SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f",
RTCPFeedback: []webrtc.RTCPFeedback{},
},
// https://gstreamer.freedesktop.org/documentation/x264/index.html
// gstreamer1.0-plugins-ugly
Pipeline: "video/x-raw,format=I420 ! x264enc threads=4 bitrate=4096 key-int-max=15 byte-stream=true tune=zerolatency speed-preset=veryfast ! video/x-h264,stream-format=byte-stream",
// https://gstreamer.freedesktop.org/documentation/openh264/openh264enc.html
// gstreamer1.0-plugins-bad
//Pipeline: "openh264enc multi-thread=4 complexity=high bitrate=3072000 max-bitrate=4096000 ! video/x-h264,stream-format=byte-stream",
}
}
func Opus() RTPCodec {
return RTPCodec{
Name: "opus",
PayloadType: 111,
Type: webrtc.RTPCodecTypeAudio,
Capability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeOpus,
ClockRate: 48000,
Channels: 2,
SDPFmtpLine: "",
RTCPFeedback: []webrtc.RTCPFeedback{},
},
// https://gstreamer.freedesktop.org/documentation/opus/opusenc.html
// gstreamer1.0-plugins-base
Pipeline: "opusenc bitrate=128000",
}
}
func G722() RTPCodec {
return RTPCodec{
Name: "g722",
PayloadType: 9,
Type: webrtc.RTPCodecTypeAudio,
Capability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeG722,
ClockRate: 8000,
Channels: 0,
SDPFmtpLine: "",
RTCPFeedback: []webrtc.RTCPFeedback{},
},
// https://gstreamer.freedesktop.org/documentation/libav/avenc_g722.html
// gstreamer1.0-libav
Pipeline: "avenc_g722",
}
}
func PCMU() RTPCodec {
return RTPCodec{
Name: "pcmu",
PayloadType: 0,
Type: webrtc.RTPCodecTypeAudio,
Capability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypePCMU,
ClockRate: 8000,
Channels: 0,
SDPFmtpLine: "",
RTCPFeedback: []webrtc.RTCPFeedback{},
},
// https://gstreamer.freedesktop.org/documentation/mulaw/mulawenc.html
// gstreamer1.0-plugins-good
Pipeline: "audio/x-raw, rate=8000 ! mulawenc",
}
}
func PCMA() RTPCodec {
return RTPCodec{
Name: "pcma",
PayloadType: 8,
Type: webrtc.RTPCodecTypeAudio,
Capability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypePCMA,
ClockRate: 8000,
Channels: 0,
SDPFmtpLine: "",
RTCPFeedback: []webrtc.RTCPFeedback{},
},
// https://gstreamer.freedesktop.org/documentation/alaw/alawenc.html
// gstreamer1.0-plugins-good
Pipeline: "audio/x-raw, rate=8000 ! alawenc",
}
}

90
pkg/types/desktop.go Normal file
View File

@ -0,0 +1,90 @@
package types
import (
"image"
)
type CursorImage struct {
Width uint16
Height uint16
Xhot uint16
Yhot uint16
Serial uint64
Image *image.RGBA
}
type ScreenSize struct {
Width int
Height int
Rate int16
}
type ScreenConfiguration struct {
Width int
Height int
Rates map[int]int16
}
type KeyboardModifiers struct {
NumLock *bool
CapsLock *bool
}
type KeyboardMap struct {
Layout string
Variant string
}
type ClipboardText struct {
Text string
HTML string
}
type DesktopManager interface {
Start()
Shutdown() error
OnBeforeScreenSizeChange(listener func())
OnAfterScreenSizeChange(listener func())
// xorg
Move(x, y int)
GetCursorPosition() (int, int)
Scroll(x, y int)
ButtonDown(code uint32) error
KeyDown(code uint32) error
ButtonUp(code uint32) error
KeyUp(code uint32) error
KeyPress(codes ...uint32) error
ResetKeys()
ScreenConfigurations() map[int]ScreenConfiguration
SetScreenSize(ScreenSize) error
GetScreenSize() *ScreenSize
SetKeyboardMap(KeyboardMap) error
GetKeyboardMap() (*KeyboardMap, error)
SetKeyboardModifiers(mod KeyboardModifiers)
GetKeyboardModifiers() KeyboardModifiers
GetCursorImage() *CursorImage
GetScreenshotImage() *image.RGBA
// xevent
OnCursorChanged(listener func(serial uint64))
OnClipboardUpdated(listener func())
OnFileChooserDialogOpened(listener func())
OnFileChooserDialogClosed(listener func())
OnEventError(listener func(error_code uint8, message string, request_code uint8, minor_code uint8))
// clipboard
ClipboardGetText() (*ClipboardText, error)
ClipboardSetText(data ClipboardText) error
ClipboardGetBinary(mime string) ([]byte, error)
ClipboardSetBinary(mime string, data []byte) error
ClipboardGetTargets() ([]string, error)
// drop
DropFiles(x int, y int, files []string) bool
// filechooser
HandleFileChooserDialog(uri string) error
CloseFileChooserDialog()
IsFileChooserDialogOpened() bool
}

74
pkg/types/event/events.go Normal file
View File

@ -0,0 +1,74 @@
package event
const (
SYSTEM_INIT = "system/init"
SYSTEM_ADMIN = "system/admin"
SYSTEM_LOGS = "system/logs"
SYSTEM_DISCONNECT = "system/disconnect"
)
const (
SIGNAL_REQUEST = "signal/request"
SIGNAL_RESTART = "signal/restart"
SIGNAL_OFFER = "signal/offer"
SIGNAL_ANSWER = "signal/answer"
SIGNAL_PROVIDE = "signal/provide"
SIGNAL_CANDIDATE = "signal/candidate"
SIGNAL_VIDEO = "signal/video"
SIGNAL_CLOSE = "signal/close"
)
const (
SESSION_CREATED = "session/created"
SESSION_DELETED = "session/deleted"
SESSION_PROFILE = "session/profile"
SESSION_STATE = "session/state"
SESSION_CURSORS = "session/cursors"
)
const (
CONTROL_HOST = "control/host"
CONTROL_RELEASE = "control/release"
CONTROL_REQUEST = "control/request"
// mouse
CONTROL_MOVE = "control/move" // TODO: New. (fallback)
CONTROL_SCROLL = "control/scroll" // TODO: New. (fallback)
// keyboard
CONTROL_KEYPRESS = "control/keypress"
CONTROL_KEYDOWN = "control/keydown"
CONTROL_KEYUP = "control/keyup"
// actions
CONTROL_CUT = "control/cut"
CONTROL_COPY = "control/copy"
CONTROL_PASTE = "control/paste"
CONTROL_SELECT_ALL = "control/select_all"
)
const (
SCREEN_UPDATED = "screen/updated"
SCREEN_SET = "screen/set"
)
const (
CLIPBOARD_UPDATED = "clipboard/updated"
CLIPBOARD_SET = "clipboard/set"
)
const (
KEYBOARD_MODIFIERS = "keyboard/modifiers"
KEYBOARD_MAP = "keyboard/map"
)
const (
BORADCAST_STATUS = "broadcast/status"
)
const (
SEND_UNICAST = "send/unicast"
SEND_BROADCAST = "send/broadcast"
)
const (
FILE_CHOOSER_DIALOG_OPENED = "file_chooser_dialog/opened"
FILE_CHOOSER_DIALOG_CLOSED = "file_chooser_dialog/closed"
)

28
pkg/types/http.go Normal file
View File

@ -0,0 +1,28 @@
package types
import (
"context"
"net/http"
)
type RouterHandler func(w http.ResponseWriter, r *http.Request) error
type MiddlewareHandler func(w http.ResponseWriter, r *http.Request) (context.Context, error)
type Router interface {
Group(fn func(Router))
Route(pattern string, fn func(Router))
Get(pattern string, fn RouterHandler)
Post(pattern string, fn RouterHandler)
Put(pattern string, fn RouterHandler)
Delete(pattern string, fn RouterHandler)
With(fn MiddlewareHandler) Router
WithBypass(fn func(next http.Handler) http.Handler) Router
Use(fn MiddlewareHandler)
UseBypass(fn func(next http.Handler) http.Handler)
ServeHTTP(w http.ResponseWriter, req *http.Request)
}
type HttpManager interface {
Start()
Shutdown() error
}

43
pkg/types/member.go Normal file
View File

@ -0,0 +1,43 @@
package types
import "errors"
var (
ErrMemberAlreadyExists = errors.New("member already exists")
ErrMemberDoesNotExist = errors.New("member does not exist")
ErrMemberInvalidPassword = errors.New("invalid password")
)
type MemberProfile struct {
Name string `json:"name"`
IsAdmin bool `json:"is_admin"`
CanLogin bool `json:"can_login"`
CanConnect bool `json:"can_connect"`
CanWatch bool `json:"can_watch"`
CanHost bool `json:"can_host"`
CanShareMedia bool `json:"can_share_media"`
CanAccessClipboard bool `json:"can_access_clipboard"`
SendsInactiveCursor bool `json:"sends_inactive_cursor"`
CanSeeInactiveCursors bool `json:"can_see_inactive_cursors"`
}
type MemberProvider interface {
Connect() error
Disconnect() error
Authenticate(username string, password string) (string, MemberProfile, error)
Insert(username string, password string, profile MemberProfile) (string, error)
Select(id string) (MemberProfile, error)
SelectAll(limit int, offset int) (map[string]MemberProfile, error)
UpdateProfile(id string, profile MemberProfile) error
UpdatePassword(id string, password string) error
Delete(id string) error
}
type MemberManager interface {
MemberProvider
Login(username string, password string) (Session, string, error)
Logout(id string) error
}

View File

@ -0,0 +1,177 @@
package message
import (
"github.com/pion/webrtc/v3"
"gitlab.com/demodesk/neko/server/pkg/types"
)
/////////////////////////////
// System
/////////////////////////////
type SystemWebRTC struct {
Videos []string `json:"videos"`
}
type SystemInit struct {
SessionId string `json:"session_id"`
ControlHost ControlHost `json:"control_host"`
ScreenSize ScreenSize `json:"screen_size"`
Sessions map[string]SessionData `json:"sessions"`
ImplicitHosting bool `json:"implicit_hosting"`
InactiveCursors bool `json:"inactive_cursors"`
ScreencastEnabled bool `json:"screencast_enabled"`
WebRTC SystemWebRTC `json:"webrtc"`
}
type SystemAdmin struct {
ScreenSizesList []ScreenSize `json:"screen_sizes_list"`
BroadcastStatus BroadcastStatus `json:"broadcast_status"`
}
type SystemLogs = []SystemLog
type SystemLog struct {
Level string `json:"level"`
Fields map[string]interface{} `json:"fields"`
Message string `json:"message"`
}
type SystemDisconnect struct {
Message string `json:"message"`
}
/////////////////////////////
// Signal
/////////////////////////////
type SignalProvide struct {
SDP string `json:"sdp"`
ICEServers []types.ICEServer `json:"iceservers"`
Video string `json:"video"`
}
type SignalCandidate struct {
webrtc.ICECandidateInit
}
type SignalDescription struct {
SDP string `json:"sdp"`
}
type SignalVideo struct {
Video string `json:"video"`
}
/////////////////////////////
// Session
/////////////////////////////
type SessionID struct {
ID string `json:"id"`
}
type MemberProfile struct {
ID string `json:"id"`
types.MemberProfile
}
type SessionState struct {
ID string `json:"id"`
types.SessionState
}
type SessionData struct {
ID string `json:"id"`
Profile types.MemberProfile `json:"profile"`
State types.SessionState `json:"state"`
}
type SessionCursors struct {
ID string `json:"id"`
Cursors []types.Cursor `json:"cursors"`
}
/////////////////////////////
// Control
/////////////////////////////
type ControlHost struct {
HasHost bool `json:"has_host"`
HostID string `json:"host_id,omitempty"`
}
// TODO: New.
type ControlMove struct {
X uint16 `json:"x"`
Y uint16 `json:"y"`
}
// TODO: New.
type ControlScroll struct {
X int16 `json:"x"`
Y int16 `json:"y"`
}
type ControlKey struct {
Keysym uint32 `json:"keysym"`
}
/////////////////////////////
// Screen
/////////////////////////////
type ScreenSize struct {
Width int `json:"width"`
Height int `json:"height"`
Rate int16 `json:"rate"`
}
/////////////////////////////
// Clipboard
/////////////////////////////
type ClipboardData struct {
Text string `json:"text"`
}
/////////////////////////////
// Keyboard
/////////////////////////////
type KeyboardMap struct {
Layout string `json:"layout"`
Variant string `json:"variant"`
}
type KeyboardModifiers struct {
CapsLock *bool `json:"capslock"`
NumLock *bool `json:"numlock"`
}
/////////////////////////////
// Broadcast
/////////////////////////////
type BroadcastStatus struct {
IsActive bool `json:"is_active"`
URL string `json:"url,omitempty"`
}
/////////////////////////////
// Send (opaque comunication channel)
/////////////////////////////
type SendUnicast struct {
Sender string `json:"sender"`
Receiver string `json:"receiver"`
Subject string `json:"subject"`
Body interface{} `json:"body"`
}
type SendBroadcast struct {
Sender string `json:"sender"`
Subject string `json:"subject"`
Body interface{} `json:"body"`
}

81
pkg/types/session.go Normal file
View File

@ -0,0 +1,81 @@
package types
import (
"errors"
"net/http"
)
var (
ErrSessionNotFound = errors.New("session not found")
ErrSessionAlreadyExists = errors.New("session already exists")
ErrSessionAlreadyConnected = errors.New("session is already connected")
ErrSessionLoginDisabled = errors.New("session login disabled")
)
type Cursor struct {
X int `json:"x"`
Y int `json:"y"`
}
type SessionState struct {
IsConnected bool `json:"is_connected"`
IsWatching bool `json:"is_watching"`
}
type Session interface {
ID() string
Profile() MemberProfile
State() SessionState
IsHost() bool
// cursor
SetCursor(cursor Cursor)
// websocket
SetWebSocketPeer(websocketPeer WebSocketPeer)
SetWebSocketConnected(websocketPeer WebSocketPeer, connected bool)
GetWebSocketPeer() WebSocketPeer
Send(event string, payload interface{})
// webrtc
SetWebRTCPeer(webrtcPeer WebRTCPeer)
SetWebRTCConnected(webrtcPeer WebRTCPeer, connected bool)
GetWebRTCPeer() WebRTCPeer
}
type SessionManager interface {
Create(id string, profile MemberProfile) (Session, string, error)
Update(id string, profile MemberProfile) error
Delete(id string) error
Get(id string) (Session, bool)
GetByToken(token string) (Session, bool)
List() []Session
SetHost(host Session)
GetHost() Session
ClearHost()
SetCursor(cursor Cursor, session Session)
PopCursors() map[Session][]Cursor
Broadcast(event string, payload interface{}, exclude interface{})
AdminBroadcast(event string, payload interface{}, exclude interface{})
InactiveCursorsBroadcast(event string, payload interface{}, exclude interface{})
OnCreated(listener func(session Session))
OnDeleted(listener func(session Session))
OnConnected(listener func(session Session))
OnDisconnected(listener func(session Session))
OnProfileChanged(listener func(session Session))
OnStateChanged(listener func(session Session))
OnHostChanged(listener func(session Session))
ImplicitHosting() bool
InactiveCursors() bool
CookieEnabled() bool
MercifulReconnect() bool
CookieSetToken(w http.ResponseWriter, token string)
CookieClearToken(w http.ResponseWriter, r *http.Request)
Authenticate(r *http.Request) (Session, error)
}

42
pkg/types/webrtc.go Normal file
View File

@ -0,0 +1,42 @@
package types
import (
"errors"
"github.com/pion/webrtc/v3"
)
var (
ErrWebRTCVideoNotFound = errors.New("webrtc video not found")
ErrWebRTCDataChannelNotFound = errors.New("webrtc data channel not found")
ErrWebRTCConnectionNotFound = errors.New("webrtc connection not found")
)
type ICEServer struct {
URLs []string `mapstructure:"urls" json:"urls"`
Username string `mapstructure:"username" json:"username,omitempty"`
Credential string `mapstructure:"credential" json:"credential,omitempty"`
}
type WebRTCPeer interface {
CreateOffer(ICERestart bool) (*webrtc.SessionDescription, error)
CreateAnswer() (*webrtc.SessionDescription, error)
SetOffer(sdp string) error
SetAnswer(sdp string) error
SetCandidate(candidate webrtc.ICECandidateInit) error
SetVideoID(videoID string) error
SendCursorPosition(x, y int) error
SendCursorImage(cur *CursorImage, img []byte) error
Destroy()
}
type WebRTCManager interface {
Start()
Shutdown() error
ICEServers() []ICEServer
CreatePeer(session Session, videoID string) (*webrtc.SessionDescription, error)
}

28
pkg/types/websocket.go Normal file
View File

@ -0,0 +1,28 @@
package types
import (
"encoding/json"
"net/http"
)
type WebSocketMessage struct {
Event string `json:"event"`
Payload json.RawMessage `json:"payload"`
}
type WebSocketHandler func(Session, WebSocketMessage) bool
type CheckOrigin func(r *http.Request) bool
type WebSocketPeer interface {
Send(event string, payload interface{})
Ping() error
Destroy(reason string)
}
type WebSocketManager interface {
Start()
Shutdown() error
AddHandler(handler WebSocketHandler)
Upgrade(checkOrigin CheckOrigin) RouterHandler
}

24
pkg/utils/array.go Normal file
View File

@ -0,0 +1,24 @@
package utils
import (
"reflect"
)
func ArrayIn(val interface{}, array interface{}) (exists bool, index int) {
exists = false
index = -1
switch reflect.TypeOf(array).Kind() {
case reflect.Slice:
s := reflect.ValueOf(array)
for i := 0; i < s.Len(); i++ {
if reflect.DeepEqual(val, s.Index(i).Interface()) {
index = i
exists = true
return
}
}
}
return
}

34
pkg/utils/color.go Normal file
View File

@ -0,0 +1,34 @@
package utils
import (
"fmt"
"regexp"
)
const (
char = "&"
)
// Colors: http://www.lihaoyi.com/post/BuildyourownCommandLinewithANSIescapecodes.html
var re = regexp.MustCompile(char + `(?m)([0-9]{1,2};[0-9]{1,2}|[0-9]{1,2})`)
func Color(str string) string {
result := ""
lastIndex := 0
for _, v := range re.FindAllSubmatchIndex([]byte(str), -1) {
groups := []string{}
for i := 0; i < len(v); i += 2 {
groups = append(groups, str[v[i]:v[i+1]])
}
result += str[lastIndex:v[0]] + "\033[" + groups[1] + "m"
lastIndex = v[1]
}
return result + str[lastIndex:]
}
func Colorf(format string, a ...interface{}) string {
return fmt.Sprintf(Color(format), a...)
}

133
pkg/utils/http.go Normal file
View File

@ -0,0 +1,133 @@
package utils
import (
"encoding/json"
"fmt"
"io"
"net/http"
"github.com/rs/zerolog/log"
)
func HttpJsonRequest(w http.ResponseWriter, r *http.Request, res interface{}) error {
err := json.NewDecoder(r.Body).Decode(res)
if err == nil {
return nil
}
if err == io.EOF {
return HttpBadRequest("no data provided").WithInternalErr(err)
}
return HttpBadRequest("unable to parse provided data").WithInternalErr(err)
}
func HttpJsonResponse(w http.ResponseWriter, code int, res interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
if err := json.NewEncoder(w).Encode(res); err != nil {
log.Err(err).Str("module", "http").Msg("sending http json response failed")
}
}
func HttpSuccess(w http.ResponseWriter, res ...interface{}) error {
if len(res) == 0 {
w.WriteHeader(http.StatusNoContent)
} else {
HttpJsonResponse(w, http.StatusOK, res[0])
}
return nil
}
// HTTPError is an error with a message and an HTTP status code.
type HTTPError struct {
Code int `json:"code"`
Message string `json:"message"`
InternalErr error `json:"-"`
InternalMsg string `json:"-"`
}
func (e *HTTPError) Error() string {
if e.InternalMsg != "" {
return e.InternalMsg
}
return fmt.Sprintf("%d: %s", e.Code, e.Message)
}
func (e *HTTPError) Cause() error {
if e.InternalErr != nil {
return e.InternalErr
}
return e
}
// WithInternalErr adds internal error information to the error
func (e *HTTPError) WithInternalErr(err error) *HTTPError {
e.InternalErr = err
return e
}
// WithInternalMsg adds internal message information to the error
func (e *HTTPError) WithInternalMsg(msg string) *HTTPError {
e.InternalMsg = msg
return e
}
// WithInternalMsg adds internal formated message information to the error
func (e *HTTPError) WithInternalMsgf(fmtStr string, args ...interface{}) *HTTPError {
e.InternalMsg = fmt.Sprintf(fmtStr, args...)
return e
}
// Sends error with custom formated message
func (e *HTTPError) Msgf(fmtSt string, args ...interface{}) *HTTPError {
e.Message = fmt.Sprintf(fmtSt, args...)
return e
}
// Sends error with custom message
func (e *HTTPError) Msg(str string) *HTTPError {
e.Message = str
return e
}
func HttpError(code int, res ...string) *HTTPError {
err := &HTTPError{
Code: code,
Message: http.StatusText(code),
}
if len(res) == 1 {
err.Message = res[0]
}
return err
}
func HttpBadRequest(res ...string) *HTTPError {
return HttpError(http.StatusBadRequest, res...)
}
func HttpUnauthorized(res ...string) *HTTPError {
return HttpError(http.StatusUnauthorized, res...)
}
func HttpForbidden(res ...string) *HTTPError {
return HttpError(http.StatusForbidden, res...)
}
func HttpNotFound(res ...string) *HTTPError {
return HttpError(http.StatusNotFound, res...)
}
func HttpUnprocessableEntity(res ...string) *HTTPError {
return HttpError(http.StatusUnprocessableEntity, res...)
}
func HttpInternalServerError(res ...string) *HTTPError {
return HttpError(http.StatusInternalServerError, res...)
}

39
pkg/utils/image.go Normal file
View File

@ -0,0 +1,39 @@
package utils
import (
"bytes"
"encoding/base64"
"image"
"image/jpeg"
"image/png"
)
func CreatePNGImage(img *image.RGBA) ([]byte, error) {
out := new(bytes.Buffer)
err := png.Encode(out, img)
if err != nil {
return nil, err
}
return out.Bytes(), nil
}
func CreateJPGImage(img *image.RGBA, quality int) ([]byte, error) {
out := new(bytes.Buffer)
err := jpeg.Encode(out, img, &jpeg.Options{Quality: quality})
if err != nil {
return nil, err
}
return out.Bytes(), nil
}
func CreatePNGImageURI(img *image.RGBA) (string, error) {
data, err := CreatePNGImage(img)
if err != nil {
return "", err
}
uri := "data:image/png;base64," + base64.StdEncoding.EncodeToString(data)
return uri, nil
}

29
pkg/utils/json.go Normal file
View File

@ -0,0 +1,29 @@
package utils
import (
"encoding/json"
"reflect"
)
func Unmarshal(in interface{}, raw []byte, callback func() error) error {
if err := json.Unmarshal(raw, &in); err != nil {
return err
}
return callback()
}
func JsonStringAutoDecode(m interface{}) func(rf reflect.Kind, rt reflect.Kind, data interface{}) (interface{}, error) {
return func(rf reflect.Kind, rt reflect.Kind, data interface{}) (interface{}, error) {
if rf != reflect.String || rt == reflect.String {
return data, nil
}
raw := data.(string)
if raw != "" && (raw[0:1] == "{" || raw[0:1] == "[") {
err := json.Unmarshal([]byte(raw), &m)
return m, err
}
return data, nil
}
}

22
pkg/utils/request.go Normal file
View File

@ -0,0 +1,22 @@
package utils
import (
"bytes"
"io"
"net/http"
)
func HttpRequestGET(url string) (string, error) {
rsp, err := http.Get(url)
if err != nil {
return "", err
}
defer rsp.Body.Close()
buf, err := io.ReadAll(rsp.Body)
if err != nil {
return "", err
}
return string(bytes.TrimSpace(buf)), nil
}

98
pkg/utils/uid.go Normal file
View File

@ -0,0 +1,98 @@
package utils
import (
"crypto/rand"
"fmt"
"math"
)
const (
defaultAlphabet = "_-0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" // len=64
defaultSize = 21
defaultMaskSize = 5
)
// Generator function
type Generator func([]byte) (int, error)
// BytesGenerator is the default bytes generator
var BytesGenerator Generator = rand.Read
func initMasks(params ...int) []uint {
var size int
if len(params) == 0 {
size = defaultMaskSize
} else {
size = params[0]
}
masks := make([]uint, size)
for i := 0; i < size; i++ {
shift := 3 + i
masks[i] = (2 << uint(shift)) - 1
}
return masks
}
func getMask(alphabet string, masks []uint) int {
for i := 0; i < len(masks); i++ {
curr := int(masks[i])
if curr >= len(alphabet)-1 {
return curr
}
}
return 0
}
// GenerateUID is a low-level function to change alphabet and ID size.
func GenerateUID(alphabet string, size int) (string, error) {
if len(alphabet) == 0 || len(alphabet) > 255 {
return "", fmt.Errorf("alphabet must not empty and contain no more than 255 chars. Current len is %d", len(alphabet))
}
if size <= 0 {
return "", fmt.Errorf("size must be positive integer")
}
masks := initMasks(size)
mask := getMask(alphabet, masks)
ceilArg := 1.6 * float64(mask*size) / float64(len(alphabet))
step := int(math.Ceil(ceilArg))
id := make([]byte, size)
bytes := make([]byte, step)
for j := 0; ; {
_, err := BytesGenerator(bytes)
if err != nil {
return "", err
}
for i := 0; i < step; i++ {
currByte := bytes[i] & byte(mask)
if currByte < byte(len(alphabet)) {
id[j] = alphabet[currByte]
j++
if j == size {
return string(id[:size]), nil
}
}
}
}
}
// NewUID generates secure URL-friendly unique ID.
func NewUID(param ...int) (string, error) {
var size int
if len(param) == 0 {
size = defaultSize
} else {
size = param[0]
}
bytes := make([]byte, size)
_, err := BytesGenerator(bytes)
if err != nil {
return "", err
}
id := make([]byte, size)
for i := 0; i < size; i++ {
id[i] = defaultAlphabet[bytes[i]&63]
}
return string(id[:size]), nil
}

114
pkg/utils/zip.go Normal file
View File

@ -0,0 +1,114 @@
package utils
import (
"archive/zip"
"io"
"os"
"path/filepath"
"strings"
)
func Zip(source, zipPath string) error {
archiveFile, err := os.Create(zipPath)
if err != nil {
return err
}
defer archiveFile.Close()
archive := zip.NewWriter(archiveFile)
defer archive.Close()
return filepath.Walk(source, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() && !info.Mode().IsRegular() {
return nil
}
header, err := zip.FileInfoHeader(info)
if err != nil {
return err
}
header.Name = strings.TrimPrefix(path, source)
if info.IsDir() {
header.Name += "/"
} else {
header.Method = zip.Deflate
}
writer, err := archive.CreateHeader(header)
if err != nil {
return err
}
if info.IsDir() {
return nil
}
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()
_, err = io.Copy(writer, file)
return err
})
}
func Unzip(zipPath, target string) error {
reader, err := zip.OpenReader(zipPath)
if err != nil {
return err
}
if err := os.MkdirAll(target, 0755); err != nil {
return err
}
for _, file := range reader.File {
path := filepath.Join(target, file.Name)
if file.FileInfo().IsDir() {
if err := os.MkdirAll(path, file.Mode()); err != nil {
return err
}
continue
}
fileReader, err := file.Open()
if err != nil {
if fileReader != nil {
fileReader.Close()
}
return err
}
targetFile, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.Mode())
if err != nil {
fileReader.Close()
if targetFile != nil {
targetFile.Close()
}
return err
}
if _, err := io.Copy(targetFile, fileReader); err != nil {
fileReader.Close()
targetFile.Close()
return err
}
fileReader.Close()
targetFile.Close()
}
return nil
}

123
pkg/xevent/xevent.c Normal file
View File

@ -0,0 +1,123 @@
#include "xevent.h"
static int XEventError(Display *display, XErrorEvent *event) {
char message[100];
int error;
error = XGetErrorText(display, event->error_code, message, sizeof(message));
if (error) {
goXEventError(event, "Could not get error message.");
} else {
goXEventError(event, message);
}
return 1;
}
void XEventLoop(char *name) {
Display *display = XOpenDisplay(name);
Window root = RootWindow(display, 0);
int xfixes_event_base, xfixes_error_base;
if (!XFixesQueryExtension(display, &xfixes_event_base, &xfixes_error_base)) {
return;
}
Atom WM_WINDOW_ROLE = XInternAtom(display, "WM_WINDOW_ROLE", 1);
Atom XA_CLIPBOARD = XInternAtom(display, "CLIPBOARD", 0);
XFixesSelectSelectionInput(display, root, XA_CLIPBOARD, XFixesSetSelectionOwnerNotifyMask);
XFixesSelectCursorInput(display, root, XFixesDisplayCursorNotifyMask);
XSelectInput(display, root, SubstructureNotifyMask);
XSync(display, 0);
XSetErrorHandler(XEventError);
while (goXEventActive()) {
XEvent event;
XNextEvent(display, &event);
// XFixesDisplayCursorNotify
if (event.type == xfixes_event_base + 1) {
XFixesCursorNotifyEvent notifyEvent = *((XFixesCursorNotifyEvent *) &event);
if (notifyEvent.subtype == XFixesDisplayCursorNotify) {
goXEventCursorChanged(notifyEvent);
continue;
}
}
// XFixesSelectionNotifyEvent
if (event.type == xfixes_event_base + XFixesSelectionNotify) {
XFixesSelectionNotifyEvent notifyEvent = *((XFixesSelectionNotifyEvent *) &event);
if (notifyEvent.subtype == XFixesSetSelectionOwnerNotify && notifyEvent.selection == XA_CLIPBOARD) {
goXEventClipboardUpdated();
continue;
}
}
// ConfigureNotify
if (event.type == ConfigureNotify) {
Window window = event.xconfigure.window;
char *name;
XFetchName(display, window, &name);
XTextProperty role;
XGetTextProperty(display, window, &role, WM_WINDOW_ROLE);
goXEventConfigureNotify(display, window, name, role.value);
XFree(name);
continue;
}
// UnmapNotify
if (event.type == UnmapNotify) {
Window window = event.xunmap.window;
goXEventUnmapNotify(window);
continue;
}
}
XCloseDisplay(display);
}
void XFileChooserHide(Display *display, Window window) {
Window root = RootWindow(display, 0);
// The WM_TRANSIENT_FOR property is defined by the [ICCCM] for managed windows.
// This specification extends the use of the property to override-redirect windows.
// If an override-redirect is a pop-up on behalf of another window, then the Client
// SHOULD set WM_TRANSIENT_FOR on the override-redirect to this other window.
//
// As an example, a Client should set WM_TRANSIENT_FOR on dropdown menus to the
// toplevel application window that contains the menubar.
// Remove WM_TRANSIENT_FOR
Atom WM_TRANSIENT_FOR = XInternAtom(display, "WM_TRANSIENT_FOR", 0);
XDeleteProperty(display, window, WM_TRANSIENT_FOR);
// Add _NET_WM_STATE_BELOW
XClientMessageEvent clientMessageEvent;
memset(&clientMessageEvent, 0, sizeof(clientMessageEvent));
// window = the respective client window
// message_type = _NET_WM_STATE
// format = 32
// data.l[0] = the action, as listed below
// _NET_WM_STATE_REMOVE 0 // remove/unset property
// _NET_WM_STATE_ADD 1 // add/set property
// _NET_WM_STATE_TOGGLE 2 // toggle property
// data.l[1] = first property to alter
// data.l[2] = second property to alter
// data.l[3] = source indication
// other data.l[] elements = 0
clientMessageEvent.type = ClientMessage;
clientMessageEvent.window = window;
clientMessageEvent.message_type = XInternAtom(display, "_NET_WM_STATE", 0);
clientMessageEvent.format = 32;
clientMessageEvent.data.l[0] = 1;
clientMessageEvent.data.l[1] = XInternAtom(display, "_NET_WM_STATE_BELOW", 0);
clientMessageEvent.data.l[3] = 1;
XSendEvent(display, root, 0, SubstructureRedirectMask | SubstructureNotifyMask, (XEvent *)&clientMessageEvent);
}

84
pkg/xevent/xevent.go Normal file
View File

@ -0,0 +1,84 @@
package xevent
/*
#cgo LDFLAGS: -lX11 -lXfixes
#include "xevent.h"
*/
import "C"
import (
"strings"
"time"
"unsafe"
"github.com/kataras/go-events"
)
var Emmiter events.EventEmmiter
var file_chooser_dialog_window uint32 = 0
func init() {
Emmiter = events.New()
}
func EventLoop(display string) {
displayUnsafe := C.CString(display)
defer C.free(unsafe.Pointer(displayUnsafe))
C.XEventLoop(displayUnsafe)
}
//export goXEventCursorChanged
func goXEventCursorChanged(event C.XFixesCursorNotifyEvent) {
Emmiter.Emit("cursor-changed", uint64(event.cursor_serial))
}
//export goXEventClipboardUpdated
func goXEventClipboardUpdated() {
Emmiter.Emit("clipboard-updated")
}
//export goXEventConfigureNotify
func goXEventConfigureNotify(display *C.Display, window C.Window, name *C.char, role *C.char) {
if C.GoString(role) != "GtkFileChooserDialog" {
return
}
// TODO: Refactor. Right now processing of this dialog relies on identifying
// via its name. When that changes to role, this condition should be removed.
if !strings.HasPrefix(C.GoString(name), "Open File") {
return
}
C.XFileChooserHide(display, window)
// Because first dialog is not put properly to background
time.Sleep(10 * time.Millisecond)
C.XFileChooserHide(display, window)
if file_chooser_dialog_window == 0 {
file_chooser_dialog_window = uint32(window)
Emmiter.Emit("file-chooser-dialog-opened")
}
}
//export goXEventUnmapNotify
func goXEventUnmapNotify(window C.Window) {
if uint32(window) != file_chooser_dialog_window {
return
}
file_chooser_dialog_window = 0
Emmiter.Emit("file-chooser-dialog-closed")
}
//export goXEventError
func goXEventError(event *C.XErrorEvent, message *C.char) {
Emmiter.Emit("event-error", uint8(event.error_code), C.GoString(message), uint8(event.request_code), uint8(event.minor_code))
}
//export goXEventActive
func goXEventActive() C.int {
return C.int(1)
}

20
pkg/xevent/xevent.h Normal file
View File

@ -0,0 +1,20 @@
#pragma once
#include <X11/Xutil.h>
#include <X11/Xatom.h>
#include <X11/Xlib.h>
#include <X11/extensions/Xfixes.h>
#include <stdlib.h>
#include <string.h>
extern void goXEventCursorChanged(XFixesCursorNotifyEvent event);
extern void goXEventClipboardUpdated();
extern void goXEventConfigureNotify(Display *display, Window window, char *name, char *role);
extern void goXEventUnmapNotify(Window window);
extern void goXEventError(XErrorEvent *event, char *message);
extern int goXEventActive();
static int XEventError(Display *display, XErrorEvent *event);
void XEventLoop(char *display);
void XFileChooserHide(Display *display, Window window);

2498
pkg/xorg/keysymdef.go Normal file

File diff suppressed because it is too large Load Diff

6
pkg/xorg/keysymdef.sh Executable file
View File

@ -0,0 +1,6 @@
#!/bin/sh
wget https://cgit.freedesktop.org/xorg/proto/x11proto/plain/keysymdef.h
sed -i -E 's/\#define (XK_[a-zA-Z_0-9]+\s+)(0x[0-9a-f]+)/const \1 = \2/g' keysymdef.h
sed -i -E 's/^\#/\/\//g' keysymdef.h
echo "package xorg" | cat - keysymdef.h > keysymdef.go && rm keysymdef.h

266
pkg/xorg/xorg.c Normal file
View File

@ -0,0 +1,266 @@
#include "xorg.h"
static Display *DISPLAY = NULL;
Display *getXDisplay(void) {
return DISPLAY;
}
int XDisplayOpen(char *name) {
DISPLAY = XOpenDisplay(name);
return DISPLAY == NULL;
}
void XDisplayClose(void) {
XCloseDisplay(DISPLAY);
}
void XMove(int x, int y) {
Display *display = getXDisplay();
XWarpPointer(display, None, DefaultRootWindow(display), 0, 0, 0, 0, x, y);
XSync(display, 0);
}
void XCursorPosition(int *x, int *y) {
Display *display = getXDisplay();
Window root = DefaultRootWindow(display);
Window window;
int i;
unsigned mask;
XQueryPointer(display, root, &root, &window, x, y, &i, &i, &mask);
}
void XScroll(int x, int y) {
int ydir = 4; /* Button 4 is up, 5 is down. */
int xdir = 6;
Display *display = getXDisplay();
if (y < 0) {
ydir = 5;
}
if (x < 0) {
xdir = 7;
}
int xi;
int yi;
for (xi = 0; xi < abs(x); xi++) {
XTestFakeButtonEvent(display, xdir, 1, CurrentTime);
XTestFakeButtonEvent(display, xdir, 0, CurrentTime);
}
for (yi = 0; yi < abs(y); yi++) {
XTestFakeButtonEvent(display, ydir, 1, CurrentTime);
XTestFakeButtonEvent(display, ydir, 0, CurrentTime);
}
XSync(display, 0);
}
void XButton(unsigned int button, int down) {
if (button == 0)
return;
Display *display = getXDisplay();
XTestFakeButtonEvent(display, button, down, CurrentTime);
XSync(display, 0);
}
static xkeyentry_t *xKeysHead = NULL;
void XKeyEntryAdd(KeySym keysym, KeyCode keycode) {
xkeyentry_t *entry = (xkeyentry_t *) malloc(sizeof(xkeyentry_t));
if (entry == NULL)
return;
entry->keysym = keysym;
entry->keycode = keycode;
entry->next = xKeysHead;
xKeysHead = entry;
}
KeyCode XKeyEntryGet(KeySym keysym) {
xkeyentry_t *prev = NULL;
xkeyentry_t *curr = xKeysHead;
KeyCode keycode = 0;
while (curr != NULL) {
if (curr->keysym == keysym) {
keycode = curr->keycode;
if (prev == NULL) {
xKeysHead = curr->next;
} else {
prev->next = curr->next;
}
free(curr);
return keycode;
}
prev = curr;
curr = curr->next;
}
return 0;
}
// From https://github.com/TigerVNC/tigervnc/blob/0946e298075f8f7b6d63e552297a787c5f84d27c/unix/x0vncserver/XDesktop.cxx#L343-L379
KeyCode XkbKeysymToKeycode(Display* dpy, KeySym keysym) {
XkbDescPtr xkb;
XkbStateRec state;
unsigned int mods;
unsigned keycode;
xkb = XkbGetMap(dpy, XkbAllComponentsMask, XkbUseCoreKbd);
if (!xkb)
return 0;
XkbGetState(dpy, XkbUseCoreKbd, &state);
// XkbStateFieldFromRec() doesn't work properly because
// state.lookup_mods isn't properly updated, so we do this manually
mods = XkbBuildCoreState(XkbStateMods(&state), state.group);
for (keycode = xkb->min_key_code;
keycode <= xkb->max_key_code;
keycode++) {
KeySym cursym;
unsigned int out_mods;
XkbTranslateKeyCode(xkb, keycode, mods, &out_mods, &cursym);
if (cursym == keysym)
break;
}
if (keycode > xkb->max_key_code)
keycode = 0;
XkbFreeKeyboard(xkb, XkbAllComponentsMask, True);
// Shift+Tab is usually ISO_Left_Tab, but RFB hides this fact. Do
// another attempt if we failed the initial lookup
if ((keycode == 0) && (keysym == XK_Tab) && (mods & ShiftMask))
return XkbKeysymToKeycode(dpy, XK_ISO_Left_Tab);
return keycode;
}
void XKey(KeySym keysym, int down) {
if (keysym == 0)
return;
Display *display = getXDisplay();
KeyCode keycode = 0;
if (!down)
keycode = XKeyEntryGet(keysym);
if (keycode == 0)
keycode = XkbKeysymToKeycode(display, keysym);
// Map non-existing keysyms to new keycodes
if (keycode == 0) {
int min, max, numcodes;
XDisplayKeycodes(display, &min, &max);
XGetKeyboardMapping(display, min, max-min, &numcodes);
keycode = (max-min+1)*numcodes;
KeySym keysym_list[numcodes];
for(int i=0;i<numcodes;i++) keysym_list[i] = keysym;
XChangeKeyboardMapping(display, keycode, numcodes, keysym_list, 1);
}
if (down)
XKeyEntryAdd(keysym, keycode);
XTestFakeKeyEvent(display, keycode, down, CurrentTime);
XSync(display, 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);
}
void XSetKeyboardModifier(int mod, int on) {
Display *display = getXDisplay();
XkbLockModifiers(display, XkbUseCoreKbd, mod, on ? mod : 0);
XFlush(display);
}
char XGetKeyboardModifiers() {
XkbStateRec xkbState;
Display *display = getXDisplay();
XkbGetState(display, XkbUseCoreKbd, &xkbState);
return xkbState.locked_mods;
}
XFixesCursorImage *XGetCursorImage(void) {
Display *display = getXDisplay();
return XFixesGetCursorImage(display);
}
char *XGetScreenshot(int *w, int *h) {
Display *display = getXDisplay();
Window root = DefaultRootWindow(display);
XWindowAttributes attr;
XGetWindowAttributes(display, root, &attr);
int width = attr.width;
int height = attr.height;
XImage *ximage = XGetImage(display, root, 0, 0, width, height, AllPlanes, ZPixmap);
*w = width;
*h = height;
char *pixels = (char *)malloc(width * height * 3);
for (int row = 0; row < height; row++) {
for (int col = 0; col < width; col++) {
int pos = ((row * width) + col) * 3;
unsigned long pixel = XGetPixel(ximage, col, row);
pixels[pos] = (pixel & ximage->red_mask) >> 16;
pixels[pos+1] = (pixel & ximage->green_mask) >> 8;
pixels[pos+2] = pixel & ximage->blue_mask;
}
}
XDestroyImage(ximage);
return pixels;
}

320
pkg/xorg/xorg.go Normal file
View File

@ -0,0 +1,320 @@
package xorg
/*
#cgo LDFLAGS: -lX11 -lXrandr -lXtst -lXfixes
#include "xorg.h"
*/
import "C"
import (
"fmt"
"image"
"image/color"
"sync"
"time"
"unsafe"
"gitlab.com/demodesk/neko/server/pkg/types"
)
//go:generate ./keysymdef.sh
type KbdMod uint8
const (
KbdModCapsLock KbdMod = 2
KbdModNumLock KbdMod = 16
)
var ScreenConfigurations = make(map[int]types.ScreenConfiguration)
var debounce_button = make(map[uint32]time.Time)
var debounce_key = make(map[uint32]time.Time)
var mu = sync.Mutex{}
func GetScreenConfigurations() {
mu.Lock()
defer mu.Unlock()
C.XGetScreenConfigurations()
}
func DisplayOpen(display string) bool {
mu.Lock()
defer mu.Unlock()
displayUnsafe := C.CString(display)
defer C.free(unsafe.Pointer(displayUnsafe))
ok := C.XDisplayOpen(displayUnsafe)
return int(ok) == 1
}
func DisplayClose() {
mu.Lock()
defer mu.Unlock()
C.XDisplayClose()
}
func Move(x, y int) {
mu.Lock()
defer mu.Unlock()
C.XMove(C.int(x), C.int(y))
}
func GetCursorPosition() (int, int) {
mu.Lock()
defer mu.Unlock()
var x C.int
var y C.int
C.XCursorPosition(&x, &y)
return int(x), int(y)
}
func Scroll(x, y int) {
mu.Lock()
defer mu.Unlock()
C.XScroll(C.int(x), C.int(y))
}
func ButtonDown(code uint32) error {
mu.Lock()
defer mu.Unlock()
if _, ok := debounce_button[code]; ok {
return fmt.Errorf("debounced button %v", code)
}
debounce_button[code] = time.Now()
C.XButton(C.uint(code), C.int(1))
return nil
}
func KeyDown(code uint32) error {
mu.Lock()
defer mu.Unlock()
if _, ok := debounce_key[code]; ok {
return fmt.Errorf("debounced key %v", code)
}
debounce_key[code] = time.Now()
C.XKey(C.KeySym(code), C.int(1))
return nil
}
func ButtonUp(code uint32) error {
mu.Lock()
defer mu.Unlock()
if _, ok := debounce_button[code]; !ok {
return fmt.Errorf("debounced button %v", code)
}
delete(debounce_button, code)
C.XButton(C.uint(code), C.int(0))
return nil
}
func KeyUp(code uint32) error {
mu.Lock()
defer mu.Unlock()
if _, ok := debounce_key[code]; !ok {
return fmt.Errorf("debounced key %v", code)
}
delete(debounce_key, code)
C.XKey(C.KeySym(code), C.int(0))
return nil
}
func ResetKeys() {
mu.Lock()
defer mu.Unlock()
for code := range debounce_button {
C.XButton(C.uint(code), C.int(0))
delete(debounce_button, code)
}
for code := range debounce_key {
C.XKey(C.KeySym(code), C.int(0))
delete(debounce_key, code)
}
}
func CheckKeys(duration time.Duration) {
mu.Lock()
defer mu.Unlock()
t := time.Now()
for code, start := range debounce_button {
if t.Sub(start) < duration {
continue
}
C.XButton(C.uint(code), C.int(0))
delete(debounce_button, code)
}
for code, start := range debounce_key {
if t.Sub(start) < duration {
continue
}
C.XKey(C.KeySym(code), C.int(0))
delete(debounce_key, code)
}
}
func ChangeScreenSize(width int, height int, rate int16) error {
mu.Lock()
defer mu.Unlock()
for index, size := range ScreenConfigurations {
if size.Width == width && size.Height == height {
for _, fps := range size.Rates {
if rate == fps {
C.XSetScreenConfiguration(C.int(index), C.short(fps))
return nil
}
}
}
}
return fmt.Errorf("unknown screen configuration %dx%d@%d", width, height, rate)
}
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
}
func SetKeyboardModifier(mod KbdMod, active bool) {
mu.Lock()
defer mu.Unlock()
num := C.int(0)
if active {
num = C.int(1)
}
C.XSetKeyboardModifier(C.int(mod), num)
}
func GetKeyboardModifiers() KbdMod {
mu.Lock()
defer mu.Unlock()
return KbdMod(C.XGetKeyboardModifiers())
}
func GetCursorImage() *types.CursorImage {
mu.Lock()
defer mu.Unlock()
cur := C.XGetCursorImage()
defer C.XFree(unsafe.Pointer(cur))
width := int(cur.width)
height := int(cur.height)
// Xlib stores 32-bit data in longs, even if longs are 64-bits long.
pixels := C.GoBytes(unsafe.Pointer(cur.pixels), C.int(width*height*8))
img := image.NewRGBA(image.Rect(0, 0, width, height))
for y := 0; y < height; y++ {
for x := 0; x < width; x++ {
pos := ((y * width) + x) * 8
img.SetRGBA(x, y, color.RGBA{
A: pixels[pos+3],
R: pixels[pos+2],
G: pixels[pos+1],
B: pixels[pos+0],
})
}
}
return &types.CursorImage{
Width: uint16(width),
Height: uint16(height),
Xhot: uint16(cur.xhot),
Yhot: uint16(cur.yhot),
Serial: uint64(cur.cursor_serial),
Image: img,
}
}
func GetScreenshotImage() *image.RGBA {
mu.Lock()
defer mu.Unlock()
var w, h C.int
pixelsUnsafe := C.XGetScreenshot(&w, &h)
pixels := C.GoBytes(unsafe.Pointer(pixelsUnsafe), w*h*3)
defer C.free(unsafe.Pointer(pixelsUnsafe))
width := int(w)
height := int(h)
img := image.NewRGBA(image.Rect(0, 0, width, height))
for row := 0; row < height; row++ {
for col := 0; col < width; col++ {
pos := ((row * width) + col) * 3
img.SetRGBA(col, row, color.RGBA{
R: uint8(pixels[pos]),
G: uint8(pixels[pos+1]),
B: uint8(pixels[pos+2]),
A: 0xFF,
})
}
}
return img
}
//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, rateC C.short) {
rate := int16(rateC)
// filter out all irrelevant rates
if rate > 60 || (rate > 30 && rate%10 != 0) {
return
}
ScreenConfigurations[int(index)].Rates[int(rate_index)] = rate
}

43
pkg/xorg/xorg.h Normal file
View File

@ -0,0 +1,43 @@
#pragma once
#include <X11/Xlib.h>
#include <X11/XKBlib.h>
#include <X11/Xutil.h>
#include <X11/extensions/Xrandr.h>
#include <X11/extensions/XTest.h>
#include <X11/extensions/Xfixes.h>
#include <stdlib.h>
extern void goCreateScreenSize(int index, int width, int height, int mwidth, int mheight);
extern void goSetScreenRates(int index, int rate_index, short rate);
Display *getXDisplay(void);
int XDisplayOpen(char *input);
void XDisplayClose(void);
void XMove(int x, int y);
void XCursorPosition(int *x, int *y);
void XScroll(int x, int y);
void XButton(unsigned int button, int down);
typedef struct xkeyentry_t {
KeySym keysym;
KeyCode keycode;
struct xkeyentry_t *next;
} xkeyentry_t;
static void XKeyEntryAdd(KeySym keysym, KeyCode keycode);
static KeyCode XKeyEntryGet(KeySym keysym);
static KeyCode XkbKeysymToKeycode(Display *dpy, KeySym keysym);
void XKey(KeySym keysym, int down);
void XGetScreenConfigurations();
void XSetScreenConfiguration(int index, short rate);
int XGetScreenSize();
short XGetScreenRate();
void XSetKeyboardModifier(int mod, int on);
char XGetKeyboardModifiers();
XFixesCursorImage *XGetCursorImage(void);
char *XGetScreenshot(int *w, int *h);