move server to server directory.

This commit is contained in:
Miroslav Šedivý
2024-06-23 17:48:14 +02:00
parent da45f62ca8
commit 5b98344205
211 changed files with 18 additions and 10 deletions

98
server/pkg/auth/auth.go Normal file
View File

@ -0,0 +1,98 @@
package auth
import (
"context"
"fmt"
"net/http"
"github.com/demodesk/neko/pkg/types"
"github.com/demodesk/neko/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")
}
if session.PrivateModeEnabled() {
return nil, utils.HttpUnprocessableEntity("private mode is enabled")
}
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
}
func PluginsGenericOnly[V comparable](key string, exp V) func(w http.ResponseWriter, r *http.Request) (context.Context, error) {
return func(w http.ResponseWriter, r *http.Request) (context.Context, error) {
session, ok := GetSession(r)
if !ok {
return nil, utils.HttpForbidden("session not found")
}
plugins := session.Profile().Plugins
if plugins[key] == nil {
return nil, utils.HttpForbidden(fmt.Sprintf("missing plugin permission: %s=%T", key, exp))
}
val, ok := plugins[key].(V)
if !ok {
return nil, utils.HttpForbidden(fmt.Sprintf("invalid plugin permission type: %s=%T expected %T", key, plugins[key], exp))
}
if val != exp {
return nil, utils.HttpForbidden(fmt.Sprintf("wrong plugin permission value for %s=%T", key, exp))
}
return nil, nil
}
}

View File

@ -0,0 +1,358 @@
package auth
import (
"fmt"
"net/http"
"reflect"
"testing"
"github.com/demodesk/neko/internal/config"
"github.com/demodesk/neko/internal/session"
"github.com/demodesk/neko/pkg/types"
)
var i = 0
var sessionManager = session.New(&config.Session{})
func rWithSession(profile types.MemberProfile) (*http.Request, types.Session, error) {
i++
r := &http.Request{}
session, _, err := sessionManager.Create(fmt.Sprintf("id-%d", i), profile)
ctx := SetSession(r, session)
r = r.WithContext(ctx)
return r, session, err
}
func TestSessionCtx(t *testing.T) {
r, session, err := rWithSession(types.MemberProfile{})
if err != nil {
t.Errorf("could not create session %s", err.Error())
return
}
sess, ok := GetSession(r)
if !ok {
t.Errorf("session not found")
return
}
if !reflect.DeepEqual(sess, session) {
t.Errorf("sessions not equal")
return
}
}
func TestAdminsOnly(t *testing.T) {
r1, _, err := rWithSession(types.MemberProfile{IsAdmin: false})
if err != nil {
t.Errorf("could not create session %s", err.Error())
return
}
r2, _, err := rWithSession(types.MemberProfile{IsAdmin: true})
if err != nil {
t.Errorf("could not create session %s", err.Error())
return
}
tests := []struct {
name string
r *http.Request
wantErr bool
}{
{
name: "is not admin",
r: r1,
wantErr: true,
},
{
name: "is admin",
r: r2,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := AdminsOnly(nil, tt.r)
if (err != nil) != tt.wantErr {
t.Errorf("AdminsOnly() error = %v, wantErr %v", err, tt.wantErr)
return
}
})
}
}
func TestHostsOnly(t *testing.T) {
r1, _, err := rWithSession(types.MemberProfile{CanHost: true})
if err != nil {
t.Errorf("could not create session %s", err.Error())
return
}
r2, session, err := rWithSession(types.MemberProfile{CanHost: true})
if err != nil {
t.Errorf("could not create session %s", err.Error())
return
}
// r2 is host
session.SetAsHost()
r3, _, err := rWithSession(types.MemberProfile{CanHost: false})
if err != nil {
t.Errorf("could not create session %s", err.Error())
return
}
tests := []struct {
name string
r *http.Request
wantErr bool
}{
{
name: "is not hosting",
r: r1,
wantErr: true,
},
{
name: "is hosting",
r: r2,
wantErr: false,
},
{
name: "cannot host",
r: r3,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := HostsOnly(nil, tt.r)
if (err != nil) != tt.wantErr {
t.Errorf("HostsOnly() error = %v, wantErr %v", err, tt.wantErr)
return
}
})
}
}
func TestCanWatchOnly(t *testing.T) {
r1, _, err := rWithSession(types.MemberProfile{CanWatch: false})
if err != nil {
t.Errorf("could not create session %s", err.Error())
return
}
r2, _, err := rWithSession(types.MemberProfile{CanWatch: true})
if err != nil {
t.Errorf("could not create session %s", err.Error())
return
}
tests := []struct {
name string
r *http.Request
wantErr bool
}{
{
name: "can not watch",
r: r1,
wantErr: true,
},
{
name: "can watch",
r: r2,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := CanWatchOnly(nil, tt.r)
if (err != nil) != tt.wantErr {
t.Errorf("CanWatchOnly() error = %v, wantErr %v", err, tt.wantErr)
return
}
})
}
}
func TestCanHostOnly(t *testing.T) {
r1, _, err := rWithSession(types.MemberProfile{CanHost: false})
if err != nil {
t.Errorf("could not create session %s", err.Error())
return
}
r2, _, err := rWithSession(types.MemberProfile{CanHost: true})
if err != nil {
t.Errorf("could not create session %s", err.Error())
return
}
tests := []struct {
name string
r *http.Request
wantErr bool
privateMode bool
}{
{
name: "can not host",
r: r1,
wantErr: true,
},
{
name: "can host",
r: r2,
wantErr: false,
},
{
name: "private mode enabled: can not host",
r: r1,
wantErr: true,
privateMode: true,
},
{
name: "private mode enabled: can host",
r: r2,
wantErr: true,
privateMode: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
session, _ := GetSession(tt.r)
sessionManager.UpdateSettingsFunc(session, func(s *types.Settings) bool {
s.PrivateMode = tt.privateMode
return true
})
_, err := CanHostOnly(nil, tt.r)
if (err != nil) != tt.wantErr {
t.Errorf("CanHostOnly() error = %v, wantErr %v", err, tt.wantErr)
return
}
})
}
}
func TestCanAccessClipboardOnly(t *testing.T) {
r1, _, err := rWithSession(types.MemberProfile{CanAccessClipboard: false})
if err != nil {
t.Errorf("could not create session %s", err.Error())
return
}
r2, _, err := rWithSession(types.MemberProfile{CanAccessClipboard: true})
if err != nil {
t.Errorf("could not create session %s", err.Error())
return
}
tests := []struct {
name string
r *http.Request
wantErr bool
}{
{
name: "can not access clipboard",
r: r1,
wantErr: true,
},
{
name: "can access clipboard",
r: r2,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := CanAccessClipboardOnly(nil, tt.r)
if (err != nil) != tt.wantErr {
t.Errorf("CanAccessClipboardOnly() error = %v, wantErr %v", err, tt.wantErr)
return
}
})
}
}
func TestPluginsGenericOnly(t *testing.T) {
r1, _, err := rWithSession(types.MemberProfile{
Plugins: map[string]any{
"foo.bar": 1,
},
})
if err != nil {
t.Errorf("could not create session %s", err.Error())
return
}
t.Run("test if exists", func(t *testing.T) {
key := "foo.bar"
val := 1
wantErr := false
handler := PluginsGenericOnly(key, val)
_, err := handler(nil, r1)
if (err != nil) != wantErr {
t.Errorf("PluginsGenericOnly(%q, %v) error = %v, wantErr %v", key, val, err, wantErr)
return
}
})
t.Run("test when gets different value", func(t *testing.T) {
key := "foo.bar"
val := 2
wantErr := true
handler := PluginsGenericOnly(key, val)
_, err := handler(nil, r1)
if (err != nil) != wantErr {
t.Errorf("PluginsGenericOnly(%q, %v) error = %v, wantErr %v", key, val, err, wantErr)
return
}
})
t.Run("test when gets different type", func(t *testing.T) {
key := "foo.bar"
val := "1"
wantErr := true
handler := PluginsGenericOnly(key, val)
_, err := handler(nil, r1)
if (err != nil) != wantErr {
t.Errorf("PluginsGenericOnly(%q, %v) error = %v, wantErr %v", key, val, err, wantErr)
return
}
})
t.Run("test if does not exists", func(t *testing.T) {
key := "foo.bar_not_extist"
val := 1
wantErr := true
handler := PluginsGenericOnly(key, val)
_, err := handler(nil, r1)
if (err != nil) != wantErr {
t.Errorf("PluginsGenericOnly(%q, %v) error = %v, wantErr %v", key, val, err, wantErr)
return
}
})
t.Run("test if session does not exists", func(t *testing.T) {
key := "foo.bar_not_extist"
val := 1
wantErr := true
handler := PluginsGenericOnly(key, val)
_, err := handler(nil, &http.Request{})
if (err != nil) != wantErr {
t.Errorf("PluginsGenericOnly(%q, %v) error = %v, wantErr %v", key, val, err, wantErr)
return
}
})
}

93
server/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_POPUP);
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
server/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
server/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);

217
server/pkg/gst/gst.c Normal file
View File

@ -0,0 +1,217 @@
#include "gst.h"
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(ctx->pipelineId, level, buffer);
}
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(ctx->pipelineId, copy, copy_size,
GST_BUFFER_DURATION(buffer),
GST_BUFFER_FLAG_IS_SET(buffer, GST_BUFFER_FLAG_DELTA_UNIT)
);
}
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_memdup2(buffer, bufferLen);
GstBuffer *buffer = gst_buffer_new_wrapped(p, bufferLen);
gst_app_src_push_buffer(GST_APP_SRC(ctx->appsrc), buffer);
}
}
gboolean gstreamer_pipeline_set_prop_int(GstPipelineCtx *ctx, char *binName, char *prop, gint value) {
GstElement *el = gst_bin_get_by_name(GST_BIN(ctx->pipeline), binName);
if (el == NULL) return FALSE;
g_object_set(G_OBJECT(el),
prop, value,
NULL);
gst_object_unref(el);
return TRUE;
}
gboolean gstreamer_pipeline_set_caps_framerate(GstPipelineCtx *ctx, const gchar* binName, gint numerator, gint denominator) {
GstElement *el = gst_bin_get_by_name(GST_BIN(ctx->pipeline), binName);
if (el == NULL) return FALSE;
GstCaps *caps = gst_caps_new_simple("video/x-raw",
"framerate", GST_TYPE_FRACTION, numerator, denominator,
NULL);
g_object_set(G_OBJECT(el),
"caps", caps,
NULL);
gst_caps_unref(caps);
gst_object_unref(el);
return TRUE;
}
gboolean gstreamer_pipeline_set_caps_resolution(GstPipelineCtx *ctx, const gchar* binName, gint width, gint height) {
GstElement *el = gst_bin_get_by_name(GST_BIN(ctx->pipeline), binName);
if (el == NULL) return FALSE;
GstCaps *caps = gst_caps_new_simple("video/x-raw",
"width", G_TYPE_INT, width,
"height", G_TYPE_INT, height,
NULL);
g_object_set(G_OBJECT(el),
"caps", caps,
NULL);
gst_caps_unref(caps);
gst_object_unref(el);
return TRUE;
}
gboolean gstreamer_pipeline_emit_video_keyframe(GstPipelineCtx *ctx) {
GstClock *clock = gst_pipeline_get_clock(GST_PIPELINE(ctx->pipeline));
gst_object_ref(clock);
GstClockTime time = gst_clock_get_time(clock);
GstClockTime now = time - gst_element_get_base_time(ctx->pipeline);
gst_object_unref(clock);
GstEvent *keyFrameEvent = gst_video_event_new_downstream_force_key_unit(now, time, now, TRUE, 0);
return gst_element_send_event(GST_ELEMENT(ctx->pipeline), keyFrameEvent);
}

238
server/pkg/gst/gst.go Normal file
View File

@ -0,0 +1,238 @@
package gst
/*
#cgo pkg-config: gstreamer-1.0 gstreamer-app-1.0 gstreamer-video-1.0
#include "gst.h"
*/
import "C"
import (
"fmt"
"sync"
"sync/atomic"
"time"
"unsafe"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/demodesk/neko/pkg/types"
)
var (
pSerial int32
pipelines = make(map[int]*pipeline)
pipelinesLock sync.Mutex
registry *C.GstRegistry
)
func init() {
C.gst_init(nil, nil)
registry = C.gst_registry_get()
}
type Pipeline interface {
Src() string
Sample() chan types.Sample
// attach sink or src to pipeline
AttachAppsink(sinkName string)
AttachAppsrc(srcName string)
// control pipeline lifecycle
Play()
Pause()
Destroy()
Push(buffer []byte)
// modify the property of a bin
SetPropInt(binName string, prop string, value int) bool
SetCapsFramerate(binName string, numerator, denominator int) bool
SetCapsResolution(binName string, width, height int) bool
// emit video keyframe
EmitVideoKeyframe() bool
}
type pipeline struct {
id int
logger zerolog.Logger
src string
ctx *C.GstPipelineCtx
sample chan types.Sample
}
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),
logger: log.With().
Str("module", "capture").
Str("submodule", "gstreamer").
Int("pipeline_id", int(id)).Logger(),
src: pipelineStr,
ctx: ctx,
sample: make(chan types.Sample),
}
pipelines[p.id] = p
return p, nil
}
func (p *pipeline) Src() string {
return p.src
}
func (p *pipeline) Sample() chan types.Sample {
return p.sample
}
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))
}
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)))
}
func (p *pipeline) SetPropInt(binName string, prop string, value int) bool {
cBinName := C.CString(binName)
defer C.free(unsafe.Pointer(cBinName))
cProp := C.CString(prop)
defer C.free(unsafe.Pointer(cProp))
cValue := C.int(value)
p.logger.Debug().Msgf("setting prop %s of %s to %d", prop, binName, value)
ok := C.gstreamer_pipeline_set_prop_int(p.ctx, cBinName, cProp, cValue)
return ok == C.TRUE
}
func (p *pipeline) SetCapsFramerate(binName string, numerator, denominator int) bool {
cBinName := C.CString(binName)
cNumerator := C.int(numerator)
cDenominator := C.int(denominator)
defer C.free(unsafe.Pointer(cBinName))
p.logger.Debug().Msgf("setting caps framerate of %s to %d/%d", binName, numerator, denominator)
ok := C.gstreamer_pipeline_set_caps_framerate(p.ctx, cBinName, cNumerator, cDenominator)
return ok == C.TRUE
}
func (p *pipeline) SetCapsResolution(binName string, width, height int) bool {
cBinName := C.CString(binName)
cWidth := C.int(width)
cHeight := C.int(height)
defer C.free(unsafe.Pointer(cBinName))
p.logger.Debug().Msgf("setting caps resolution of %s to %dx%d", binName, width, height)
ok := C.gstreamer_pipeline_set_caps_resolution(p.ctx, cBinName, cWidth, cHeight)
return ok == C.TRUE
}
func (p *pipeline) EmitVideoKeyframe() bool {
ok := C.gstreamer_pipeline_emit_video_keyframe(p.ctx)
return ok == C.TRUE
}
// 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(pipelineID C.int, buf C.gpointer, bufLen C.int, duration C.guint64, deltaUnit C.gboolean) {
defer C.g_free(buf)
pipelinesLock.Lock()
pipeline, ok := pipelines[int(pipelineID)]
pipelinesLock.Unlock()
if ok {
pipeline.sample <- types.Sample{
Data: C.GoBytes(unsafe.Pointer(buf), bufLen),
Length: int(bufLen),
Timestamp: time.Now(),
Duration: time.Duration(duration),
DeltaUnit: deltaUnit == C.TRUE,
}
} else {
log.Warn().
Str("module", "capture").
Str("submodule", "gstreamer").
Int("pipeline_id", int(pipelineID)).
Msgf("discarding sample, pipeline not found")
}
}
//export goPipelineLog
func goPipelineLog(pipelineID C.int, levelUnsafe *C.char, msgUnsafe *C.char) {
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)
}

40
server/pkg/gst/gst.h Normal file
View File

@ -0,0 +1,40 @@
#pragma once
#include <stdio.h>
#include <gst/gst.h>
#include <gst/app/gstappsrc.h>
#include <gst/video/video.h>
#define GLIB_CHECK_VERSION(major,minor,micro) \
(GLIB_MAJOR_VERSION > (major) || \
(GLIB_MAJOR_VERSION == (major) && GLIB_MINOR_VERSION > (minor)) || \
(GLIB_MAJOR_VERSION == (major) && GLIB_MINOR_VERSION == (minor) && \
GLIB_MICRO_VERSION >= (micro)))
// g_memdup2 was added in glib 2.67.4, maintain compatibility with older versions
#if !GLIB_CHECK_VERSION(2, 67, 4)
#define g_memdup2 g_memdup
#endif
typedef struct GstPipelineCtx {
int pipelineId;
GstElement *pipeline;
GstElement *appsink;
GstElement *appsrc;
} GstPipelineCtx;
extern void goHandlePipelineBuffer(int pipelineId, void *buffer, int bufferLen, guint64 duration, gboolean deltaUnit);
extern void goPipelineLog(int pipelineId, char *level, char *msg);
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);
gboolean gstreamer_pipeline_set_prop_int(GstPipelineCtx *ctx, char *binName, char *prop, gint value);
gboolean gstreamer_pipeline_set_caps_framerate(GstPipelineCtx *ctx, const gchar* binName, gint numerator, gint denominator);
gboolean gstreamer_pipeline_set_caps_resolution(GstPipelineCtx *ctx, const gchar* binName, gint width, gint height);
gboolean gstreamer_pipeline_emit_video_keyframe(GstPipelineCtx *ctx);

6
server/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))
}

245
server/pkg/types/capture.go Normal file
View File

@ -0,0 +1,245 @@
package types
import (
"context"
"errors"
"fmt"
"math"
"strings"
"time"
"github.com/PaesslerAG/gval"
"github.com/demodesk/neko/pkg/types/codec"
)
var (
ErrCapturePipelineAlreadyExists = errors.New("capture pipeline already exists")
)
type Sample struct {
// buffer with encoded media
Data []byte
Length int
// timing information
Timestamp time.Time
Duration time.Duration
// metadata
DeltaUnit bool // this unit cannot be decoded independently.
}
type SampleListener interface {
WriteSample(Sample)
}
type BroadcastManager interface {
Start(url string) error
Stop()
Started() bool
Url() string
}
type ScreencastManager interface {
Enabled() bool
Started() bool
Image() ([]byte, error)
}
type StreamSelectorType int
const (
// select exact stream
StreamSelectorTypeExact StreamSelectorType = iota
// select nearest stream (in either direction) if exact stream is not available
StreamSelectorTypeNearest
// if exact stream is found select the next lower stream, otherwise select the nearest lower stream
StreamSelectorTypeLower
// if exact stream is found select the next higher stream, otherwise select the nearest higher stream
StreamSelectorTypeHigher
)
func (s StreamSelectorType) String() string {
switch s {
case StreamSelectorTypeExact:
return "exact"
case StreamSelectorTypeNearest:
return "nearest"
case StreamSelectorTypeLower:
return "lower"
case StreamSelectorTypeHigher:
return "higher"
default:
return fmt.Sprintf("%d", int(s))
}
}
func (s *StreamSelectorType) UnmarshalText(text []byte) error {
switch strings.ToLower(string(text)) {
case "exact", "":
*s = StreamSelectorTypeExact
case "nearest":
*s = StreamSelectorTypeNearest
case "lower":
*s = StreamSelectorTypeLower
case "higher":
*s = StreamSelectorTypeHigher
default:
return fmt.Errorf("invalid stream selector type: %s", string(text))
}
return nil
}
func (s StreamSelectorType) MarshalText() ([]byte, error) {
return []byte(s.String()), nil
}
type StreamSelector struct {
// type of stream selector
Type StreamSelectorType `json:"type"`
// select stream by its ID
ID string `json:"id"`
// select stream by its bitrate
Bitrate uint64 `json:"bitrate"`
}
type StreamSelectorManager interface {
IDs() []string
Codec() codec.RTPCodec
GetStream(selector StreamSelector) (StreamSinkManager, bool)
}
type StreamSinkManager interface {
ID() string
Codec() codec.RTPCodec
Bitrate() uint64
AddListener(listener SampleListener) error
RemoveListener(listener SampleListener) error
MoveListenerTo(listener SampleListener, targetStream StreamSinkManager) error
ListenersCount() int
Started() bool
CreatePipeline() error
DestroyPipeline()
}
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() StreamSelectorManager
Webcam() StreamSrcManager
Microphone() StreamSrcManager
}
type VideoConfig struct {
Width string `mapstructure:"width"` // expression
Height string `mapstructure:"height"` // expression
Fps string `mapstructure:"fps"` // expression
Bitrate int `mapstructure:"bitrate"` // pipeline bitrate
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]any{
"width": screen.Width,
"height": screen.Height,
"fps": screen.Rate,
}
language := []gval.Language{
gval.Function("round", func(args ...any) (any, 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("! capsfilter caps=video/x-raw,framerate=%d/100 name=framerate ! 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
}
// element videoscale parameter method to 0 meaning nearest neighbor
scalePipeline = fmt.Sprintf("! videoscale method=0 ! capsfilter caps=video/x-raw,width=%d,height=%d name=resolution ! queue", w, h)
}
// get encoder pipeline
encPipeline := fmt.Sprintf("! %s name=encoder", 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
}

View File

@ -0,0 +1,207 @@
package codec
import (
"strings"
"github.com/pion/webrtc/v3"
)
var RTCPFeedback = []webrtc.RTCPFeedback{
{Type: webrtc.TypeRTCPFBTransportCC, Parameter: ""},
{Type: webrtc.TypeRTCPFBGoogREMB, Parameter: ""}, // TODO: Deprecated.
// https://www.iana.org/assignments/sdp-parameters/sdp-parameters.xhtml#sdp-parameters-19
{Type: webrtc.TypeRTCPFBCCM, Parameter: "fir"},
// https://www.iana.org/assignments/sdp-parameters/sdp-parameters.xhtml#sdp-parameters-15
{Type: webrtc.TypeRTCPFBNACK, Parameter: "pli"},
{Type: webrtc.TypeRTCPFBNACK, Parameter: ""},
}
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 (codec *RTPCodec) IsVideo() bool {
return codec.Type == webrtc.RTPCodecTypeVideo
}
func (codec *RTPCodec) IsAudio() bool {
return codec.Type == webrtc.RTPCodecTypeAudio
}
func (codec *RTPCodec) String() string {
return codec.Type.String() + "/" + codec.Name
}
func VP8() RTPCodec {
return RTPCodec{
Name: "vp8",
PayloadType: 96,
Type: webrtc.RTPCodecTypeVideo,
Capability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeVP8,
ClockRate: 90000,
Channels: 0,
SDPFmtpLine: "",
RTCPFeedback: 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: 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=42e01f",
RTCPFeedback: 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: "useinbandfec=1;stereo=1",
RTCPFeedback: []webrtc.RTCPFeedback{},
},
// https://gstreamer.freedesktop.org/documentation/opus/opusenc.html
// gstreamer1.0-plugins-base
Pipeline: "opusenc inband-fec=true 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",
}
}

104
server/pkg/types/desktop.go Normal file
View File

@ -0,0 +1,104 @@
package types
import (
"fmt"
"image"
)
type CursorImage struct {
Width uint16
Height uint16
Xhot uint16
Yhot uint16
Serial uint64
Image *image.RGBA
}
type ScreenSize struct {
Width int `json:"width"`
Height int `json:"height"`
Rate int16 `json:"rate"`
}
func (s ScreenSize) String() string {
return fmt.Sprintf("%dx%d@%d", s.Width, s.Height, s.Rate)
}
type KeyboardModifiers struct {
Shift *bool `json:"shift"`
CapsLock *bool `json:"capslock"`
Control *bool `json:"control"`
Alt *bool `json:"alt"`
NumLock *bool `json:"numlock"`
Meta *bool `json:"meta"`
Super *bool `json:"super"`
AltGr *bool `json:"altgr"`
}
type KeyboardMap struct {
Layout string `json:"layout"`
Variant string `json:"variant"`
}
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(deltaX, deltaY int, controlKey bool)
ButtonDown(code uint32) error
KeyDown(code uint32) error
ButtonUp(code uint32) error
KeyUp(code uint32) error
ButtonPress(code uint32) error
KeyPress(codes ...uint32) error
ResetKeys()
ScreenConfigurations() []ScreenSize
SetScreenSize(ScreenSize) (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))
// input driver
HasTouchSupport() bool
TouchBegin(touchId uint32, x, y int, pressure uint8) error
TouchUpdate(touchId uint32, x, y int, pressure uint8) error
TouchEnd(touchId uint32, x, y int, pressure uint8) error
// 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
IsUploadDropEnabled() bool
// filechooser
HandleFileChooserDialog(uri string) error
CloseFileChooserDialog()
IsFileChooserDialogEnabled() bool
IsFileChooserDialogOpened() bool
}

View File

@ -0,0 +1,84 @@
package event
const (
SYSTEM_INIT = "system/init"
SYSTEM_ADMIN = "system/admin"
SYSTEM_SETTINGS = "system/settings"
SYSTEM_LOGS = "system/logs"
SYSTEM_DISCONNECT = "system/disconnect"
SYSTEM_HEARTBEAT = "system/heartbeat"
)
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_AUDIO = "signal/audio"
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"
CONTROL_SCROLL = "control/scroll"
CONTROL_BUTTONPRESS = "control/buttonpress"
CONTROL_BUTTONDOWN = "control/buttondown"
CONTROL_BUTTONUP = "control/buttonup"
// keyboard
CONTROL_KEYPRESS = "control/keypress"
CONTROL_KEYDOWN = "control/keydown"
CONTROL_KEYUP = "control/keyup"
// touch
CONTROL_TOUCHBEGIN = "control/touchbegin"
CONTROL_TOUCHUPDATE = "control/touchupdate"
CONTROL_TOUCHEND = "control/touchend"
// 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"
)

27
server/pkg/types/http.go Normal file
View File

@ -0,0 +1,27 @@
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)
Patch(pattern string, fn RouterHandler)
Delete(pattern string, fn RouterHandler)
With(fn MiddlewareHandler) Router
Use(fn MiddlewareHandler)
ServeHTTP(w http.ResponseWriter, req *http.Request)
}
type HttpManager interface {
Start()
Shutdown() error
}

View File

@ -0,0 +1,48 @@
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"`
// permissions
IsAdmin bool `json:"is_admin" mapstructure:"is_admin"`
CanLogin bool `json:"can_login" mapstructure:"can_login"`
CanConnect bool `json:"can_connect" mapstructure:"can_connect"`
CanWatch bool `json:"can_watch" mapstructure:"can_watch"`
CanHost bool `json:"can_host" mapstructure:"can_host"`
CanShareMedia bool `json:"can_share_media" mapstructure:"can_share_media"`
CanAccessClipboard bool `json:"can_access_clipboard" mapstructure:"can_access_clipboard"`
SendsInactiveCursor bool `json:"sends_inactive_cursor" mapstructure:"sends_inactive_cursor"`
CanSeeInactiveCursors bool `json:"can_see_inactive_cursors" mapstructure:"can_see_inactive_cursors"`
// plugin scope
Plugins PluginSettings `json:"plugins"`
}
type MemberProvider interface {
Connect() error
Disconnect() error
Authenticate(username string, password string) (id string, profile MemberProfile, err error)
Insert(username string, password string, profile MemberProfile) (id string, err error)
Select(id string) (profile MemberProfile, err error)
SelectAll(limit int, offset int) (profiles map[string]MemberProfile, err 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,212 @@
package message
import (
"github.com/pion/webrtc/v3"
"github.com/demodesk/neko/pkg/types"
)
/////////////////////////////
// System
/////////////////////////////
type SystemWebRTC struct {
Videos []string `json:"videos"`
}
type SystemInit struct {
SessionId string `json:"session_id"`
ControlHost ControlHost `json:"control_host"`
ScreenSize types.ScreenSize `json:"screen_size"`
Sessions map[string]SessionData `json:"sessions"`
Settings types.Settings `json:"settings"`
TouchEvents bool `json:"touch_events"`
ScreencastEnabled bool `json:"screencast_enabled"`
WebRTC SystemWebRTC `json:"webrtc"`
}
type SystemAdmin struct {
ScreenSizesList []types.ScreenSize `json:"screen_sizes_list"`
BroadcastStatus BroadcastStatus `json:"broadcast_status"`
}
type SystemLogs = []SystemLog
type SystemLog struct {
Level string `json:"level"`
Fields map[string]any `json:"fields"`
Message string `json:"message"`
}
type SystemDisconnect struct {
Message string `json:"message"`
}
type SystemSettingsUpdate struct {
ID string `json:"id"`
types.Settings
}
/////////////////////////////
// Signal
/////////////////////////////
type SignalRequest struct {
Video types.PeerVideoRequest `json:"video"`
Audio types.PeerAudioRequest `json:"audio"`
Auto bool `json:"auto"` // TODO: Remove this
}
type SignalProvide struct {
SDP string `json:"sdp"`
ICEServers []types.ICEServer `json:"iceservers"`
Video types.PeerVideo `json:"video"`
Audio types.PeerAudio `json:"audio"`
}
type SignalCandidate struct {
webrtc.ICECandidateInit
}
type SignalDescription struct {
SDP string `json:"sdp"`
}
type SignalVideo struct {
types.PeerVideoRequest
}
type SignalAudio struct {
types.PeerAudioRequest
}
/////////////////////////////
// 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 {
ID string `json:"id"`
HasHost bool `json:"has_host"`
HostID string `json:"host_id,omitempty"`
}
type ControlScroll struct {
// TOOD: remove this once the client is fixed
X int `json:"x"`
Y int `json:"y"`
DeltaX int `json:"delta_x"`
DeltaY int `json:"delta_y"`
ControlKey bool `json:"control_key"`
}
type ControlPos struct {
X int `json:"x"`
Y int `json:"y"`
}
type ControlButton struct {
*ControlPos
Code uint32 `json:"code"`
}
type ControlKey struct {
*ControlPos
Keysym uint32 `json:"keysym"`
}
type ControlTouch struct {
TouchId uint32 `json:"touch_id"`
*ControlPos
Pressure uint8 `json:"pressure"`
}
/////////////////////////////
// Screen
/////////////////////////////
type ScreenSize struct {
types.ScreenSize
}
type ScreenSizeUpdate struct {
ID string `json:"id"`
types.ScreenSize
}
/////////////////////////////
// Clipboard
/////////////////////////////
type ClipboardData struct {
Text string `json:"text"`
}
/////////////////////////////
// Keyboard
/////////////////////////////
type KeyboardMap struct {
types.KeyboardMap
}
type KeyboardModifiers struct {
types.KeyboardModifiers
}
/////////////////////////////
// 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 any `json:"body"`
}
type SendBroadcast struct {
Sender string `json:"sender"`
Subject string `json:"subject"`
Body any `json:"body"`
}

View File

@ -0,0 +1,91 @@
package types
import (
"errors"
"fmt"
"strings"
"github.com/demodesk/neko/pkg/utils"
"github.com/spf13/cobra"
)
var (
ErrPluginSettingsNotFound = errors.New("plugin settings not found")
)
type Plugin interface {
Name() string
Config() PluginConfig
Start(PluginManagers) error
Shutdown() error
}
type DependablePlugin interface {
Plugin
DependsOn() []string
}
type ExposablePlugin interface {
Plugin
ExposeService() any
}
type PluginConfig interface {
Init(cmd *cobra.Command) error
Set()
}
type PluginMetadata struct {
Name string
IsDependable bool
IsExposable bool
DependsOn []string `json:",omitempty"`
}
type PluginManagers struct {
SessionManager SessionManager
WebSocketManager WebSocketManager
ApiManager ApiManager
LoadServiceFromPlugin func(string) (any, error)
}
func (p *PluginManagers) Validate() error {
if p.SessionManager == nil {
return errors.New("SessionManager is nil")
}
if p.WebSocketManager == nil {
return errors.New("WebSocketManager is nil")
}
if p.ApiManager == nil {
return errors.New("ApiManager is nil")
}
if p.LoadServiceFromPlugin == nil {
return errors.New("LoadServiceFromPlugin is nil")
}
return nil
}
type PluginSettings map[string]any
func (p PluginSettings) Unmarshal(name string, def any) error {
if p == nil {
return fmt.Errorf("%w: %s", ErrPluginSettingsNotFound, name)
}
// loop through the plugin settings and take only the one that starts with the name
// because the settings are stored in a map["plugin_name.setting_name"] = value
newMap := make(map[string]any)
for k, v := range p {
if strings.HasPrefix(k, name+".") {
newMap[strings.TrimPrefix(k, name+".")] = v
}
}
fmt.Printf("newMap: %+v\n", newMap)
if len(newMap) == 0 {
return fmt.Errorf("%w: %s", ErrPluginSettingsNotFound, name)
}
return utils.Decode(newMap, def)
}

115
server/pkg/types/session.go Normal file
View File

@ -0,0 +1,115 @@
package types
import (
"errors"
"net/http"
"time"
)
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")
ErrSessionLoginsLocked = errors.New("session logins locked")
)
type Cursor struct {
X int `json:"x"`
Y int `json:"y"`
}
type SessionProfile struct {
Id string
Token string
Profile MemberProfile
}
type SessionState struct {
IsConnected bool `json:"is_connected"`
// when the session was last connected
ConnectedSince *time.Time `json:"connected_since,omitempty"`
// when the session was last not connected
NotConnectedSince *time.Time `json:"not_connected_since,omitempty"`
IsWatching bool `json:"is_watching"`
// when the session was last watching
WatchingSince *time.Time `json:"watching_since,omitempty"`
// when the session was last not watching
NotWatchingSince *time.Time `json:"not_watching_since,omitempty"`
}
type Settings struct {
PrivateMode bool `json:"private_mode"`
LockedLogins bool `json:"locked_logins"`
LockedControls bool `json:"locked_controls"`
ControlProtection bool `json:"control_protection"`
ImplicitHosting bool `json:"implicit_hosting"`
InactiveCursors bool `json:"inactive_cursors"`
MercifulReconnect bool `json:"merciful_reconnect"`
// plugin scope
Plugins PluginSettings `json:"plugins"`
}
type Session interface {
ID() string
Profile() MemberProfile
State() SessionState
IsHost() bool
SetAsHost()
SetAsHostBy(session Session)
ClearHost()
PrivateModeEnabled() bool
// cursor
SetCursor(cursor Cursor)
// websocket
ConnectWebSocketPeer(websocketPeer WebSocketPeer)
DisconnectWebSocketPeer(websocketPeer WebSocketPeer, delayed bool)
DestroyWebSocketPeer(reason string)
Send(event string, payload any)
// 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
Disconnect(id string) error
Get(id string) (Session, bool)
GetByToken(token string) (Session, bool)
List() []Session
Range(func(Session) bool)
GetHost() (Session, bool)
SetCursor(cursor Cursor, session Session)
PopCursors() map[Session][]Cursor
Broadcast(event string, payload any, exclude ...string)
AdminBroadcast(event string, payload any, exclude ...string)
InactiveCursorsBroadcast(event string, payload any, exclude ...string)
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, new, old MemberProfile))
OnStateChanged(listener func(session Session))
OnHostChanged(listener func(session, host Session))
OnSettingsChanged(listener func(session Session, new, old Settings))
UpdateSettingsFunc(session Session, f func(settings *Settings) bool)
Settings() Settings
CookieEnabled() bool
CookieSetToken(w http.ResponseWriter, token string)
CookieClearToken(w http.ResponseWriter, r *http.Request)
Authenticate(r *http.Request) (Session, error)
}

View File

@ -0,0 +1,70 @@
package types
import (
"errors"
"github.com/pion/webrtc/v3"
)
var (
ErrWebRTCDataChannelNotFound = errors.New("webrtc data channel not found")
ErrWebRTCConnectionNotFound = errors.New("webrtc connection not found")
ErrWebRTCStreamNotFound = errors.New("webrtc stream 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 PeerVideo struct {
Disabled bool `json:"disabled"`
ID string `json:"id"`
Video string `json:"video"` // TODO: Remove this, used for compatibility with old clients.
Auto bool `json:"auto"`
}
type PeerVideoRequest struct {
Disabled *bool `json:"disabled,omitempty"`
Selector *StreamSelector `json:"selector,omitempty"`
Auto *bool `json:"auto,omitempty"`
}
type PeerAudio struct {
Disabled bool `json:"disabled"`
}
type PeerAudioRequest struct {
Disabled *bool `json:"disabled,omitempty"`
}
type WebRTCPeer interface {
CreateOffer(ICERestart bool) (*webrtc.SessionDescription, error)
CreateAnswer() (*webrtc.SessionDescription, error)
SetRemoteDescription(webrtc.SessionDescription) error
SetCandidate(webrtc.ICECandidateInit) error
SetPaused(isPaused bool) error
Paused() bool
SetVideo(PeerVideoRequest) error
Video() PeerVideo
SetAudio(PeerAudioRequest) error
Audio() PeerAudio
SendCursorPosition(x, y int) error
SendCursorImage(cur *CursorImage, img []byte) error
Destroy()
}
type WebRTCManager interface {
Start()
Shutdown() error
ICEServers() []ICEServer
CreatePeer(session Session) (*webrtc.SessionDescription, WebRTCPeer, error)
SetCursorPosition(x, y int)
}

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,omitempty"`
}
type WebSocketHandler func(Session, WebSocketMessage) bool
type CheckOrigin func(r *http.Request) bool
type WebSocketPeer interface {
Send(event string, payload any)
Ping() error
Destroy(reason string)
}
type WebSocketManager interface {
Start()
Shutdown() error
AddHandler(handler WebSocketHandler)
Upgrade(checkOrigin CheckOrigin) RouterHandler
}

14
server/pkg/utils/array.go Normal file
View File

@ -0,0 +1,14 @@
package utils
func ArrayIn[T comparable](val T, array []T) (exists bool, index int) {
exists, index = false, -1
for i, a := range array {
if a == val {
exists, index = true, i
return
}
}
return
}

34
server/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 ...any) string {
return fmt.Sprintf(Color(format), a...)
}

View File

@ -0,0 +1,35 @@
package utils
import (
"encoding/json"
"reflect"
"github.com/mitchellh/mapstructure"
)
func Decode(input interface{}, output interface{}) error {
return mapstructure.Decode(input, output)
}
func Unmarshal(in any, raw []byte, callback func() error) error {
if err := json.Unmarshal(raw, &in); err != nil {
return err
}
return callback()
}
func JsonStringAutoDecode(m any) func(rf reflect.Kind, rt reflect.Kind, data any) (any, error) {
return func(rf reflect.Kind, rt reflect.Kind, data any) (any, 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
}
}

133
server/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 any) 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 any) {
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 ...any) 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 ...any) *HTTPError {
e.InternalMsg = fmt.Sprintf(fmtStr, args...)
return e
}
// Sends error with custom formated message
func (e *HTTPError) Msgf(fmtSt string, args ...any) *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
server/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
}

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
}

View File

@ -0,0 +1,153 @@
// From https://github.com/livekit/livekit/blob/master/pkg/sfu/streamallocator/trenddetector.go
package utils
import (
"fmt"
"time"
)
// ------------------------------------------------
type TrendDirection int
const (
TrendDirectionNeutral TrendDirection = iota
TrendDirectionUpward
TrendDirectionDownward
)
func (t TrendDirection) String() string {
switch t {
case TrendDirectionNeutral:
return "NEUTRAL"
case TrendDirectionUpward:
return "UPWARD"
case TrendDirectionDownward:
return "DOWNWARD"
default:
return fmt.Sprintf("%d", int(t))
}
}
// ------------------------------------------------
type TrendDetectorParams struct {
RequiredSamples int
DownwardTrendThreshold float64
CollapseValues bool
}
type TrendDetector struct {
params TrendDetectorParams
startTime time.Time
numSamples int
values []int64
lowestValue int64
highestValue int64
direction TrendDirection
}
func NewTrendDetector(params TrendDetectorParams) *TrendDetector {
return &TrendDetector{
params: params,
startTime: time.Now(),
direction: TrendDirectionNeutral,
}
}
func (t *TrendDetector) Seed(value int64) {
if len(t.values) != 0 {
return
}
t.values = append(t.values, value)
}
func (t *TrendDetector) AddValue(value int64) {
t.numSamples++
if t.lowestValue == 0 || value < t.lowestValue {
t.lowestValue = value
}
if value > t.highestValue {
t.highestValue = value
}
// ignore duplicate values
if t.params.CollapseValues && len(t.values) != 0 && t.values[len(t.values)-1] == value {
return
}
if len(t.values) == t.params.RequiredSamples {
t.values = t.values[1:]
}
t.values = append(t.values, value)
t.updateDirection()
}
func (t *TrendDetector) GetLowest() int64 {
return t.lowestValue
}
func (t *TrendDetector) GetHighest() int64 {
return t.highestValue
}
func (t *TrendDetector) GetValues() []int64 {
return t.values
}
func (t *TrendDetector) GetDirection() TrendDirection {
return t.direction
}
func (t *TrendDetector) ToString() string {
now := time.Now()
elapsed := now.Sub(t.startTime).Seconds()
str := fmt.Sprintf("t: %+v|%+v|%.2fs", t.startTime.Format(time.UnixDate), now.Format(time.UnixDate), elapsed)
str += fmt.Sprintf(", v: %d|%d|%d|%+v|%.2f", t.numSamples, t.lowestValue, t.highestValue, t.values, kendallsTau(t.values))
return str
}
func (t *TrendDetector) updateDirection() {
if len(t.values) < t.params.RequiredSamples {
t.direction = TrendDirectionNeutral
return
}
// using Kendall's Tau to find trend
kt := kendallsTau(t.values)
t.direction = TrendDirectionNeutral
switch {
case kt > 0:
t.direction = TrendDirectionUpward
case kt < t.params.DownwardTrendThreshold:
t.direction = TrendDirectionDownward
}
}
// ------------------------------------------------
func kendallsTau(values []int64) float64 {
concordantPairs := 0
discordantPairs := 0
for i := 0; i < len(values)-1; i++ {
for j := i + 1; j < len(values); j++ {
if values[i] < values[j] {
concordantPairs++
} else if values[i] > values[j] {
discordantPairs++
}
}
}
if (concordantPairs + discordantPairs) == 0 {
return 0.0
}
return (float64(concordantPairs) - float64(discordantPairs)) / (float64(concordantPairs) + float64(discordantPairs))
}

98
server/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
server/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
}

192
server/pkg/xevent/xevent.c Normal file
View File

@ -0,0 +1,192 @@
#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 XSetupErrorHandler() {
XSetErrorHandler(XEventError);
}
void XEventLoop(char *name) {
Display *display = XOpenDisplay(name);
Window root = DefaultRootWindow(display);
int xfixes_event_base, xfixes_error_base;
if (!XFixesQueryExtension(display, &xfixes_event_base, &xfixes_error_base)) {
return;
}
// save last size id for fullscreen bug fix
SizeID last_size_id;
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);
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;
}
// ClientMessage
if (event.type == ClientMessage) {
Window window = event.xclient.window;
// check for net window manager state (fullscreen, maximized, etc)
if (event.xclient.message_type == XInternAtom(display, "_NET_WM_STATE", 0)) {
// see documentation in XWindowManagerStateEvent
Atom action = event.xclient.data.l[0];
Atom first = event.xclient.data.l[1];
Atom second = event.xclient.data.l[2];
// check if user is entering or exiting fullscreen
if (first == XInternAtom(display, "_NET_WM_STATE_FULLSCREEN", 0)) {
// get current size id
Rotation current_rotation;
XRRScreenConfiguration *conf = XRRGetScreenInfo(display, root);
SizeID current_size_id = XRRConfigCurrentConfiguration(conf, &current_rotation);
// window just went fullscreen
if (action == 1) {
// save current size id
last_size_id = current_size_id;
continue;
}
// window just exited fullscreen
if (action == 0) {
// if size id changed, that means user changed resolution while in fullscreen
// there is a bug in some window managers that causes the window to not resize
// if it was previously maximized, so we need to unmaximize it and maximize it again
if (current_size_id != last_size_id) {
// toggle maximized state twice
XWindowManagerStateEvent(display, window, 2,
XInternAtom(display, "_NET_WM_STATE_MAXIMIZED_VERT", 0),
XInternAtom(display, "_NET_WM_STATE_MAXIMIZED_HORZ", 0));
XWindowManagerStateEvent(display, window, 2,
XInternAtom(display, "_NET_WM_STATE_MAXIMIZED_VERT", 0),
XInternAtom(display, "_NET_WM_STATE_MAXIMIZED_HORZ", 0));
}
}
}
}
// check for window manager change state (minimize, maximize, etc)
if (event.xclient.message_type == XInternAtom(display, "WM_CHANGE_STATE", 0)) {
int window_state = event.xclient.data.l[0];
// NormalState - The client's top-level window is viewable.
// IconicState - The client's top-level window is iconic (whatever that means for this window manager).
// WithdrawnState - Neither the client's top-level window nor its icon is visible.
goXEventWMChangeState(display, window, window_state);
}
continue;
}
}
XCloseDisplay(display);
}
static void XWindowManagerStateEvent(Display *display, Window window, ulong action, ulong first, ulong second) {
Window root = DefaultRootWindow(display);
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] = action;
clientMessageEvent.data.l[1] = first;
clientMessageEvent.data.l[2] = second;
clientMessageEvent.data.l[3] = 1;
XSendEvent(display, root, 0, SubstructureRedirectMask | SubstructureNotifyMask, (XEvent *)&clientMessageEvent);
XFlush(display);
}
void XFileChooserHide(Display *display, Window window) {
// 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
XWindowManagerStateEvent(display, window, 1, // set property
XInternAtom(display, "_NET_WM_STATE_BELOW", 0), 0);
}

View File

@ -0,0 +1,96 @@
package xevent
/*
#cgo LDFLAGS: -lX11 -lXfixes
#include "xevent.h"
*/
import "C"
import (
"strings"
"unsafe"
"github.com/kataras/go-events"
)
var Emmiter events.EventEmmiter
var Unminimize bool = false
var FileChooserDialog bool = false
var fileChooserDialogWindow uint32 = 0
func init() {
Emmiter = events.New()
}
func SetupErrorHandler() {
C.XSetupErrorHandler()
}
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" || !FileChooserDialog {
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)
if fileChooserDialogWindow == 0 {
fileChooserDialogWindow = uint32(window)
Emmiter.Emit("file-chooser-dialog-opened")
}
}
//export goXEventUnmapNotify
func goXEventUnmapNotify(window C.Window) {
if uint32(window) != fileChooserDialogWindow || !FileChooserDialog {
return
}
fileChooserDialogWindow = 0
Emmiter.Emit("file-chooser-dialog-closed")
}
//export goXEventWMChangeState
func goXEventWMChangeState(display *C.Display, window C.Window, window_state C.ulong) {
// if we just realized that window is minimized and we want it to be unminimized
if window_state != C.NormalState && Unminimize {
// we want to unmap and map the window to force it to redraw
C.XUnmapWindow(display, window)
C.XMapWindow(display, window)
C.XFlush(display)
}
}
//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)
}

View File

@ -0,0 +1,24 @@
#pragma once
#include <X11/Xutil.h>
#include <X11/Xatom.h>
#include <X11/Xlib.h>
#include <X11/extensions/Xrandr.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 goXEventWMChangeState(Display *display, Window window, ulong state);
extern void goXEventError(XErrorEvent *event, char *message);
extern int goXEventActive();
static int XEventError(Display *display, XErrorEvent *event);
void XSetupErrorHandler();
void XEventLoop(char *display);
static void XWindowManagerStateEvent(Display *display, Window window, ulong action, ulong first, ulong second);
void XFileChooserHide(Display *display, Window window);

View File

@ -0,0 +1,31 @@
package xinput
import "time"
type dummy struct{}
func NewDummy() Driver {
return &dummy{}
}
func (d *dummy) Connect() error {
return nil
}
func (d *dummy) Close() error {
return nil
}
func (d *dummy) Debounce(duration time.Duration) {}
func (d *dummy) TouchBegin(touchId uint32, x, y int, pressure uint8) error {
return nil
}
func (d *dummy) TouchUpdate(touchId uint32, x, y int, pressure uint8) error {
return nil
}
func (d *dummy) TouchEnd(touchId uint32, x, y int, pressure uint8) error {
return nil
}

View File

@ -0,0 +1,61 @@
package xinput
import "time"
const (
// absolute coordinates used in driver
AbsX = 0xffff
AbsY = 0xffff
)
const (
XI_TouchBegin = 18
XI_TouchUpdate = 19
XI_TouchEnd = 20
)
type Message struct {
_type uint16
touchId uint32
x int32 // can be negative?
y int32 // can be negative?
pressure uint8
}
func (msg *Message) Unpack(buffer []byte) {
msg._type = uint16(buffer[0])
msg.touchId = uint32(buffer[1]) | (uint32(buffer[2]) << 8)
msg.x = int32(buffer[3]) | (int32(buffer[4]) << 8) | (int32(buffer[5]) << 16) | (int32(buffer[6]) << 24)
msg.y = int32(buffer[7]) | (int32(buffer[8]) << 8) | (int32(buffer[9]) << 16) | (int32(buffer[10]) << 24)
msg.pressure = uint8(buffer[11])
}
func (msg *Message) Pack() []byte {
var buffer [12]byte
buffer[0] = byte(msg._type)
buffer[1] = byte(msg.touchId)
buffer[2] = byte(msg.touchId >> 8)
buffer[3] = byte(msg.x)
buffer[4] = byte(msg.x >> 8)
buffer[5] = byte(msg.x >> 16)
buffer[6] = byte(msg.x >> 24)
buffer[7] = byte(msg.y)
buffer[8] = byte(msg.y >> 8)
buffer[9] = byte(msg.y >> 16)
buffer[10] = byte(msg.y >> 24)
buffer[11] = byte(msg.pressure)
return buffer[:]
}
type Driver interface {
Connect() error
Close() error
// release touches, that were not updated for duration
Debounce(duration time.Duration)
// touch events
TouchBegin(touchId uint32, x, y int, pressure uint8) error
TouchUpdate(touchId uint32, x, y int, pressure uint8) error
TouchEnd(touchId uint32, x, y int, pressure uint8) error
}

122
server/pkg/xinput/xinput.go Normal file
View File

@ -0,0 +1,122 @@
/* custom xf86 input driver communication protocol */
package xinput
import (
"fmt"
"net"
"sync"
"time"
)
type driver struct {
mu sync.Mutex
socket string
conn net.Conn
debounceTouchIds map[uint32]time.Time
}
func NewDriver(socket string) Driver {
return &driver{
socket: socket,
debounceTouchIds: make(map[uint32]time.Time),
}
}
func (d *driver) Connect() error {
c, err := net.Dial("unix", d.socket)
if err != nil {
return err
}
d.conn = c
return nil
}
func (d *driver) Close() error {
return d.conn.Close()
}
func (d *driver) Debounce(duration time.Duration) {
d.mu.Lock()
defer d.mu.Unlock()
t := time.Now()
for touchId, start := range d.debounceTouchIds {
if t.Sub(start) < duration {
continue
}
msg := Message{
_type: XI_TouchEnd,
touchId: touchId,
x: -1,
y: -1,
}
_, _ = d.conn.Write(msg.Pack())
delete(d.debounceTouchIds, touchId)
}
}
func (d *driver) TouchBegin(touchId uint32, x, y int, pressure uint8) error {
d.mu.Lock()
defer d.mu.Unlock()
if _, ok := d.debounceTouchIds[touchId]; ok {
return fmt.Errorf("debounced touch id %v", touchId)
}
d.debounceTouchIds[touchId] = time.Now()
msg := Message{
_type: XI_TouchBegin,
touchId: touchId,
x: int32(x),
y: int32(y),
pressure: pressure,
}
_, err := d.conn.Write(msg.Pack())
return err
}
func (d *driver) TouchUpdate(touchId uint32, x, y int, pressure uint8) error {
d.mu.Lock()
defer d.mu.Unlock()
if _, ok := d.debounceTouchIds[touchId]; !ok {
return fmt.Errorf("unknown touch id %v", touchId)
}
d.debounceTouchIds[touchId] = time.Now()
msg := Message{
_type: XI_TouchUpdate,
touchId: touchId,
x: int32(x),
y: int32(y),
pressure: pressure,
}
_, err := d.conn.Write(msg.Pack())
return err
}
func (d *driver) TouchEnd(touchId uint32, x, y int, pressure uint8) error {
d.mu.Lock()
defer d.mu.Unlock()
if _, ok := d.debounceTouchIds[touchId]; !ok {
return fmt.Errorf("unknown touch id %v", touchId)
}
delete(d.debounceTouchIds, touchId)
msg := Message{
_type: XI_TouchEnd,
touchId: touchId,
x: int32(x),
y: int32(y),
pressure: pressure,
}
_, err := d.conn.Write(msg.Pack())
return err
}

2498
server/pkg/xorg/keysymdef.go Normal file

File diff suppressed because it is too large Load Diff

6
server/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

407
server/pkg/xorg/xorg.c Normal file
View File

@ -0,0 +1,407 @@
#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 deltaX, int deltaY) {
Display *display = getXDisplay();
int ydir;
if (deltaY > 0) {
ydir = 4; // button 4 is up
} else {
ydir = 5; // button 5 is down
}
int xdir;
if (deltaX > 0) {
xdir = 6; // button 6 is right
} else {
xdir = 7; // button 7 is left
}
for (int i = 0; i < abs(deltaY); i++) {
XTestFakeButtonEvent(display, ydir, 1, CurrentTime);
XTestFakeButtonEvent(display, ydir, 0, CurrentTime);
}
for (int i = 0; i < abs(deltaX); i++) {
XTestFakeButtonEvent(display, xdir, 1, CurrentTime);
XTestFakeButtonEvent(display, xdir, 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;
// add keycode->keysym mapping to list
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;
}
// get keycode for keysym from list
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;
}
// From https://github.com/TigerVNC/tigervnc/blob/a434ef3377943e89165ac13c537cd0f28be97f84/unix/x0vncserver/XDesktop.cxx#L401-L453
KeyCode XkbAddKeyKeysym(Display* dpy, KeySym keysym) {
int types[1];
unsigned int key;
XkbDescPtr xkb;
XkbMapChangesRec changes;
KeySym *syms;
KeySym upper, lower;
xkb = XkbGetMap(dpy, XkbAllComponentsMask, XkbUseCoreKbd);
if (!xkb)
return 0;
for (key = xkb->max_key_code; key >= xkb->min_key_code; key--) {
if (XkbKeyNumGroups(xkb, key) == 0)
break;
}
// no free keycodes
if (key < xkb->min_key_code)
return 0;
// assign empty structure
changes = *(XkbMapChangesRec *) malloc(sizeof(XkbMapChangesRec));
for (int i = 0; i < sizeof(changes); i++) ((char *) &changes)[i] = 0;
XConvertCase(keysym, &lower, &upper);
if (upper == lower)
types[XkbGroup1Index] = XkbOneLevelIndex;
else
types[XkbGroup1Index] = XkbAlphabeticIndex;
XkbChangeTypesOfKey(xkb, key, 1, XkbGroup1Mask, types, &changes);
syms = XkbKeySymsPtr(xkb,key);
if (upper == lower)
syms[0] = keysym;
else {
syms[0] = lower;
syms[1] = upper;
}
changes.changed |= XkbKeySymsMask;
changes.first_key_sym = key;
changes.num_key_syms = 1;
if (XkbChangeMap(dpy, xkb, &changes)) {
return key;
}
return 0;
}
void XKey(KeySym keysym, int down) {
if (keysym == 0)
return;
Display *display = getXDisplay();
KeyCode keycode = 0;
if (!down)
keycode = XKeyEntryGet(keysym);
// Try to get keysyms from existing keycodes
if (keycode == 0)
keycode = XkbKeysymToKeycode(display, keysym);
// Map non-existing keysyms to new keycodes
if (keycode == 0)
keycode = XkbAddKeyKeysym(display, keysym);
if (down)
XKeyEntryAdd(keysym, keycode);
XTestFakeKeyEvent(display, keycode, down, CurrentTime);
XSync(display, 0);
}
Status XSetScreenConfiguration(int width, int height, short rate) {
Display *display = getXDisplay();
Window root = DefaultRootWindow(display);
XRRScreenConfiguration *conf = XRRGetScreenInfo(display, root);
XRRScreenSize *xrrs;
int num_sizes;
xrrs = XRRConfigSizes(conf, &num_sizes);
int size_index = -1;
for (int i = 0; i < num_sizes; i++) {
if (xrrs[i].width == width && xrrs[i].height == height) {
size_index = i;
break;
}
}
// if we cannot find the size
if (size_index == -1) {
return RRSetConfigFailed;
}
Status status;
status = XRRSetScreenConfigAndRate(display, conf, root, size_index, RR_Rotate_0, rate, CurrentTime);
XRRFreeScreenConfigInfo(conf);
return status;
}
void XGetScreenConfiguration(int *width, int *height, short *rate) {
Display *display = getXDisplay();
Window root = DefaultRootWindow(display);
XRRScreenConfiguration *conf = XRRGetScreenInfo(display, root);
Rotation current_rotation;
SizeID current_size_id = XRRConfigCurrentConfiguration(conf, &current_rotation);
XRRScreenSize *xrrs;
int num_sizes;
xrrs = XRRConfigSizes(conf, &num_sizes);
// if we cannot find the size
if (current_size_id >= num_sizes) {
return;
}
*width = xrrs[current_size_id].width;
*height = xrrs[current_size_id].height;
*rate = XRRConfigCurrentRate(conf);
XRRFreeScreenConfigInfo(conf);
}
void XGetScreenConfigurations() {
Display *display = getXDisplay();
Window root = DefaultRootWindow(display);
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]);
}
}
}
// Inspired by https://github.com/raboof/xrandr/blob/master/xrandr.c
void XCreateScreenMode(int width, int height, short rate) {
Display *display = getXDisplay();
Window root = DefaultRootWindow(display);
// create new mode info
XRRModeInfo *mode_info = XCreateScreenModeInfo(width, height, rate);
// create new mode
RRMode mode = XRRCreateMode(display, root, mode_info);
XSync(display, 0);
// add new mode to all outputs
XRRScreenResources *resources = XRRGetScreenResources(display, root);
for (int i = 0; i < resources->noutput; ++i) {
XRRAddOutputMode(display, resources->outputs[i], mode);
}
XRRFreeScreenResources(resources);
XRRFreeModeInfo(mode_info);
}
// Inspired by https://fossies.org/linux/xwayland/hw/xwayland/xwayland-cvt.c
XRRModeInfo *XCreateScreenModeInfo(int hdisplay, int vdisplay, short vrefresh) {
char name[128];
snprintf(name, sizeof name, "%dx%d_%d", hdisplay, vdisplay, vrefresh);
XRRModeInfo *modeinfo = XRRAllocModeInfo(name, strlen(name));
#ifdef _LIBCVT_H_
struct libxcvt_mode_info *mode_info;
// get screen mode from libxcvt, if available
mode_info = libxcvt_gen_mode_info(hdisplay, vdisplay, vrefresh, false, false);
modeinfo->width = mode_info->hdisplay;
modeinfo->height = mode_info->vdisplay;
modeinfo->dotClock = mode_info->dot_clock * 1000;
modeinfo->hSyncStart = mode_info->hsync_start;
modeinfo->hSyncEnd = mode_info->hsync_end;
modeinfo->hTotal = mode_info->htotal;
modeinfo->vSyncStart = mode_info->vsync_start;
modeinfo->vSyncEnd = mode_info->vsync_end;
modeinfo->vTotal = mode_info->vtotal;
modeinfo->modeFlags = mode_info->mode_flags;
free(mode_info);
#else
// fallback to a simple mode without refresh rate
modeinfo->width = hdisplay;
modeinfo->height = vdisplay;
#endif
return modeinfo;
}
void XSetKeyboardModifier(unsigned char mod, int on) {
Display *display = getXDisplay();
XkbLockModifiers(display, XkbUseCoreKbd, mod, on ? mod : 0);
XFlush(display);
}
unsigned char XGetKeyboardModifiers() {
XkbStateRec xkbState;
Display *display = getXDisplay();
XkbGetState(display, XkbUseCoreKbd, &xkbState);
// XkbStateFieldFromRec() doesn't work properly because
// state.lookup_mods isn't properly updated, so we do this manually
return XkbBuildCoreState(XkbStateMods(&xkbState), xkbState.group);
}
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;
}

349
server/pkg/xorg/xorg.go Normal file
View File

@ -0,0 +1,349 @@
package xorg
/*
#cgo LDFLAGS: -lX11 -lXrandr -lXtst -lXfixes -lxcvt
#include "xorg.h"
*/
import "C"
import (
"fmt"
"image"
"image/color"
"sync"
"time"
"unsafe"
"github.com/demodesk/neko/pkg/types"
)
//go:generate ./keysymdef.sh
type KbdMod uint8
const (
KbdModShift KbdMod = C.ShiftMask
KbdModCapsLock KbdMod = C.LockMask
KbdModControl KbdMod = C.ControlMask
KbdModAlt KbdMod = C.Mod1Mask
KbdModNumLock KbdMod = C.Mod2Mask
KbdModMeta KbdMod = C.Mod3Mask
KbdModSuper KbdMod = C.Mod4Mask
KbdModAltGr KbdMod = C.Mod5Mask
)
type ScreenConfiguration struct {
Width int
Height int
Rates map[int]int16
}
var ScreenConfigurations = make(map[int]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(deltaX, deltaY int, controlKey bool) {
mu.Lock()
defer mu.Unlock()
if controlKey {
C.XSetKeyboardModifier(C.uchar(C.ControlMask), 1)
defer C.XSetKeyboardModifier(C.uchar(C.ControlMask), 0)
}
C.XScroll(C.int(deltaX), C.int(deltaY))
}
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)
}
}
// set screen configuration, create new one if not exists
func ChangeScreenSize(s types.ScreenSize) (types.ScreenSize, error) {
mu.Lock()
defer mu.Unlock()
// round width to 8, because of Xorg
s.Width = s.Width - (s.Width % 8)
// if rate is 0, set it to 60
if s.Rate == 0 {
s.Rate = 60
}
// convert variables to C types
c_width, c_height, c_rate := C.int(s.Width), C.int(s.Height), C.short(s.Rate)
// if screen configuration already exists, just set it
status := C.XSetScreenConfiguration(c_width, c_height, c_rate)
if status != C.RRSetConfigSuccess {
// create new screen configuration
C.XCreateScreenMode(c_width, c_height, c_rate)
// screen configuration should exist now, set it
status = C.XSetScreenConfiguration(c_width, c_height, c_rate)
}
var err error
// if screen configuration was not set successfully, return error
if status != C.RRSetConfigSuccess {
err = fmt.Errorf("unknown screen configuration %s", s.String())
}
// if specified rate is not supported a BadValue error is returned
if status == C.BadValue {
err = fmt.Errorf("unsupported screen rate %d", s.Rate)
}
return s, err
}
func GetScreenSize() types.ScreenSize {
mu.Lock()
defer mu.Unlock()
c_width, c_height, c_rate := C.int(0), C.int(0), C.short(0)
C.XGetScreenConfiguration(&c_width, &c_height, &c_rate)
return types.ScreenSize{
Width: int(c_width),
Height: int(c_height),
Rate: int16(c_rate),
}
}
func SetKeyboardModifier(mod KbdMod, active bool) {
mu.Lock()
defer mu.Unlock()
num := C.int(0)
if active {
num = C.int(1)
}
C.XSetKeyboardModifier(C.uchar(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)] = 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) {
ScreenConfigurations[int(index)].Rates[int(rate_index)] = int16(rateC)
}

49
server/pkg/xorg/xorg.h Normal file
View File

@ -0,0 +1,49 @@
#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>
#include <stdio.h>
#include <string.h>
// for computing xrandr modelines at runtime
#include <libxcvt/libxcvt.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 deltaX, int deltaY);
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);
Status XSetScreenConfiguration(int width, int height, short rate);
void XGetScreenConfiguration(int *width, int *height, short *rate);
void XGetScreenConfigurations();
void XCreateScreenMode(int width, int height, short rate);
XRRModeInfo *XCreateScreenModeInfo(int hdisplay, int vdisplay, short vrefresh);
void XSetKeyboardModifier(unsigned char mod, int on);
unsigned char XGetKeyboardModifiers();
XFixesCursorImage *XGetCursorImage(void);
char *XGetScreenshot(int *w, int *h);