Archived
2
0

first commit

This commit is contained in:
Craig
2020-01-13 08:05:38 +00:00
commit 0c8af21fab
95 changed files with 5312 additions and 0 deletions

View File

@ -0,0 +1,8 @@
package config
import "github.com/spf13/cobra"
type Config interface {
Init(cmd *cobra.Command) error
Set()
}

View File

@ -0,0 +1,37 @@
package config
import (
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
type Root struct {
Debug bool
Logs bool
CfgFile string
}
func (Root) Init(cmd *cobra.Command) error {
cmd.PersistentFlags().BoolP("debug", "d", false, "Enable debug mode")
if err := viper.BindPFlag("debug", cmd.PersistentFlags().Lookup("debug")); err != nil {
return err
}
cmd.PersistentFlags().BoolP("logs", "l", false, "Save logs to file")
if err := viper.BindPFlag("logs", cmd.PersistentFlags().Lookup("logs")); err != nil {
return err
}
cmd.PersistentFlags().String("config", "", "Configuration file path")
if err := viper.BindPFlag("config", cmd.PersistentFlags().Lookup("config")); err != nil {
return err
}
return nil
}
func (s *Root) Set() {
s.Logs = viper.GetBool("logs")
s.Debug = viper.GetBool("debug")
s.CfgFile = viper.GetString("config")
}

View File

@ -0,0 +1,52 @@
package config
import (
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
type Serve struct {
Cert string
Key string
Bind string
Password string
Static string
}
func (Serve) Init(cmd *cobra.Command) error {
cmd.PersistentFlags().String("bind", "127.0.0.1:8080", "Address/port/socket to serve neko")
if err := viper.BindPFlag("bind", cmd.PersistentFlags().Lookup("bind")); err != nil {
return err
}
cmd.PersistentFlags().String("cert", "", "Path to the SSL cert used to secure the neko server")
if err := viper.BindPFlag("cert", cmd.PersistentFlags().Lookup("cert")); err != nil {
return err
}
cmd.PersistentFlags().String("key", "", "Path to the SSL key used to secure the neko server")
if err := viper.BindPFlag("key", cmd.PersistentFlags().Lookup("key")); err != nil {
return err
}
cmd.PersistentFlags().String("password", "neko", "Password for connecting to stream")
if err := viper.BindPFlag("password", cmd.PersistentFlags().Lookup("password")); err != nil {
return err
}
cmd.PersistentFlags().String("static", "./www", "Static files to serve")
if err := viper.BindPFlag("static", cmd.PersistentFlags().Lookup("static")); err != nil {
return err
}
return nil
}
func (s *Serve) Set() {
s.Cert = viper.GetString("cert")
s.Key = viper.GetString("key")
s.Bind = viper.GetString("bind")
s.Password = viper.GetString("password")
s.Static = viper.GetString("static")
}

88
server/internal/gst/gst.c Normal file
View File

@ -0,0 +1,88 @@
#include "gst.h"
#include <gst/app/gstappsrc.h>
typedef struct SampleHandlerUserData {
int pipelineId;
} SampleHandlerUserData;
GMainLoop *gstreamer_send_main_loop = NULL;
void gstreamer_send_start_mainloop(void) {
gstreamer_send_main_loop = g_main_loop_new(NULL, FALSE);
g_main_loop_run(gstreamer_send_main_loop);
}
static gboolean gstreamer_send_bus_call(GstBus *bus, GstMessage *msg, gpointer data) {
switch (GST_MESSAGE_TYPE(msg)) {
case GST_MESSAGE_EOS:
g_print("End of stream\n");
exit(1);
break;
case GST_MESSAGE_ERROR: {
gchar *debug;
GError *error;
gst_message_parse_error(msg, &error, &debug);
g_free(debug);
g_printerr("Error: %s\n", error->message);
g_error_free(error);
exit(1);
}
default:
break;
}
return TRUE;
}
GstFlowReturn gstreamer_send_new_sample_handler(GstElement *object, gpointer user_data) {
GstSample *sample = NULL;
GstBuffer *buffer = NULL;
gpointer copy = NULL;
gsize copy_size = 0;
SampleHandlerUserData *s = (SampleHandlerUserData *)user_data;
g_signal_emit_by_name (object, "pull-sample", &sample);
if (sample) {
buffer = gst_sample_get_buffer(sample);
if (buffer) {
gst_buffer_extract_dup(buffer, 0, gst_buffer_get_size(buffer), &copy, &copy_size);
goHandlePipelineBuffer(copy, copy_size, GST_BUFFER_DURATION(buffer), s->pipelineId);
}
gst_sample_unref (sample);
}
return GST_FLOW_OK;
}
GstElement *gstreamer_send_create_pipeline(char *pipeline) {
gst_init(NULL, NULL);
GError *error = NULL;
return gst_parse_launch(pipeline, &error);
}
void gstreamer_send_start_pipeline(GstElement *pipeline, int pipelineId) {
SampleHandlerUserData *s = calloc(1, sizeof(SampleHandlerUserData));
s->pipelineId = pipelineId;
GstBus *bus = gst_pipeline_get_bus(GST_PIPELINE(pipeline));
gst_bus_add_watch(bus, gstreamer_send_bus_call, NULL);
gst_object_unref(bus);
GstElement *appsink = gst_bin_get_by_name(GST_BIN(pipeline), "appsink");
g_object_set(appsink, "emit-signals", TRUE, NULL);
g_signal_connect(appsink, "new-sample", G_CALLBACK(gstreamer_send_new_sample_handler), s);
gst_object_unref(appsink);
gst_element_set_state(pipeline, GST_STATE_PLAYING);
}
void gstreamer_send_stop_pipeline(GstElement *pipeline) {
gst_element_set_state(pipeline, GST_STATE_NULL);
}

125
server/internal/gst/gst.go Normal file
View File

@ -0,0 +1,125 @@
package gst
/*
#cgo pkg-config: gstreamer-1.0 gstreamer-app-1.0
#include "gst.h"
*/
import "C"
import (
"fmt"
"io"
"sync"
"unsafe"
"github.com/pion/webrtc/v2"
"github.com/pion/webrtc/v2/pkg/media"
)
func init() {
go C.gstreamer_send_start_mainloop()
}
// Pipeline is a wrapper for a GStreamer Pipeline
type Pipeline struct {
Pipeline *C.GstElement
tracks []*webrtc.Track
id int
codecName string
clockRate float32
}
var pipelines = make(map[int]*Pipeline)
var pipelinesLock sync.Mutex
const (
videoClockRate = 90000
audioClockRate = 48000
pcmClockRate = 8000
)
// CreatePipeline creates a GStreamer Pipeline
func CreatePipeline(codecName string, tracks []*webrtc.Track, pipelineSrc string) *Pipeline {
pipelineStr := "appsink name=appsink"
var clockRate float32
switch codecName {
case webrtc.VP8:
pipelineStr = pipelineSrc + " ! vp8enc error-resilient=partitions keyframe-max-dist=10 auto-alt-ref=true cpu-used=5 deadline=1 ! " + pipelineStr
clockRate = videoClockRate
case webrtc.VP9:
pipelineStr = pipelineSrc + " ! vp9enc ! " + pipelineStr
clockRate = videoClockRate
case webrtc.H264:
pipelineStr = pipelineSrc + " ! video/x-raw,format=I420 ! x264enc bframes=0 speed-preset=veryfast key-int-max=60 ! video/x-h264,stream-format=byte-stream ! " + pipelineStr
clockRate = videoClockRate
case webrtc.Opus:
pipelineStr = pipelineSrc + " ! opusenc ! " + pipelineStr
clockRate = audioClockRate
case webrtc.G722:
pipelineStr = pipelineSrc + " ! avenc_g722 ! " + pipelineStr
clockRate = audioClockRate
case webrtc.PCMU:
pipelineStr = pipelineSrc + " ! audio/x-raw, rate=8000 ! mulawenc ! " + pipelineStr
clockRate = pcmClockRate
case webrtc.PCMA:
pipelineStr = pipelineSrc + " ! audio/x-raw, rate=8000 ! alawenc ! " + pipelineStr
clockRate = pcmClockRate
default:
panic("Unhandled codec " + codecName)
}
pipelineStrUnsafe := C.CString(pipelineStr)
defer C.free(unsafe.Pointer(pipelineStrUnsafe))
pipelinesLock.Lock()
defer pipelinesLock.Unlock()
pipeline := &Pipeline{
Pipeline: C.gstreamer_send_create_pipeline(pipelineStrUnsafe),
tracks: tracks,
id: len(pipelines),
codecName: codecName,
clockRate: clockRate,
}
pipelines[pipeline.id] = pipeline
return pipeline
}
// Start starts the GStreamer Pipeline
func (p *Pipeline) Start() {
C.gstreamer_send_start_pipeline(p.Pipeline, C.int(p.id))
}
// Stop stops the GStreamer Pipeline
func (p *Pipeline) Stop() {
C.gstreamer_send_stop_pipeline(p.Pipeline)
}
//export goHandlePipelineBuffer
func goHandlePipelineBuffer(buffer unsafe.Pointer, bufferLen C.int, duration C.int, pipelineID C.int) {
pipelinesLock.Lock()
pipeline, ok := pipelines[int(pipelineID)]
pipelinesLock.Unlock()
if ok {
samples := uint32(pipeline.clockRate * (float32(duration) / 1000000000))
for _, t := range pipeline.tracks {
if err := t.WriteSample(media.Sample{Data: C.GoBytes(buffer, bufferLen), Samples: samples}); err != nil && err != io.ErrClosedPipe {
panic(err)
}
}
} else {
fmt.Printf("discarding buffer, no pipeline with id %d", int(pipelineID))
}
C.free(buffer)
}

16
server/internal/gst/gst.h Normal file
View File

@ -0,0 +1,16 @@
#ifndef GST_H
#define GST_H
#include <glib.h>
#include <gst/gst.h>
#include <stdint.h>
#include <stdlib.h>
extern void goHandlePipelineBuffer(void *buffer, int bufferLen, int samples, int pipelineId);
GstElement *gstreamer_send_create_pipeline(char *pipeline);
void gstreamer_send_start_pipeline(GstElement *pipeline, int pipelineId);
void gstreamer_send_stop_pipeline(GstElement *pipeline);
void gstreamer_send_start_mainloop(void);
#endif

View File

@ -0,0 +1,14 @@
package http
import (
"net/http"
"n.eko.moe/neko/internal/http/handler"
)
func New(bind, password, static string) *http.Server {
return &http.Server{
Addr: bind,
Handler: handler.New(password, static),
}
}

View File

@ -0,0 +1,102 @@
package endpoint
import (
"encoding/json"
"fmt"
"net/http"
"runtime/debug"
"github.com/go-chi/chi/middleware"
"github.com/rs/zerolog/log"
)
type (
Endpoint func(http.ResponseWriter, *http.Request) error
ErrResponse struct {
Status int `json:"status,omitempty"`
Err string `json:"error,omitempty"`
Message string `json:"message,omitempty"`
Details string `json:"details,omitempty"`
Code string `json:"code,omitempty"`
RequestID string `json:"request,omitempty"`
}
)
func Handle(handler Endpoint) http.HandlerFunc {
fn := func(w http.ResponseWriter, r *http.Request) {
if err := handler(w, r); err != nil {
WriteError(w, r, err)
}
}
return http.HandlerFunc(fn)
}
var nonErrorsCodes = map[int]bool{
404: true,
}
func errResponse(input interface{}) *ErrResponse {
var res *ErrResponse
var err interface{}
switch input.(type) {
case *HandlerError:
e := input.(*HandlerError)
res = &ErrResponse{
Status: e.Status,
Err: http.StatusText(e.Status),
Message: e.Message,
}
err = e.Err
default:
res = &ErrResponse{
Status: http.StatusInternalServerError,
Err: http.StatusText(http.StatusInternalServerError),
}
err = input
}
if err != nil {
switch err.(type) {
case *error:
e := err.(error)
res.Details = e.Error()
break
default:
res.Details = fmt.Sprintf("%+v", err)
break
}
}
return res
}
func WriteError(w http.ResponseWriter, r *http.Request, err interface{}) {
hlog := log.With().
Str("module", "http").
Logger()
res := errResponse(err)
if reqID := middleware.GetReqID(r.Context()); reqID != "" {
res.RequestID = reqID
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(res.Status)
if err := json.NewEncoder(w).Encode(res); err != nil {
hlog.Warn().Err(err).Msg("Failed writing json error response")
}
if !nonErrorsCodes[res.Status] {
logEntry := middleware.GetLogEntry(r)
if logEntry != nil {
logEntry.Panic(err, debug.Stack())
} else {
hlog.Error().Str("stack", string(debug.Stack())).Msgf("%+v", err)
}
}
}

View File

@ -0,0 +1,17 @@
package endpoint
import "fmt"
type HandlerError struct {
Status int
Message string
Err error
}
func (e *HandlerError) Error() string {
if e.Err != nil {
return fmt.Sprintf("%s: %s", e.Message, e.Err.Error())
}
return e.Message
}

View File

@ -0,0 +1,55 @@
package handler
import (
"fmt"
"net/http"
"os"
"n.eko.moe/neko/internal/http/middleware"
"n.eko.moe/neko/internal/http/endpoint"
"n.eko.moe/neko/internal/webrtc"
"github.com/go-chi/chi"
)
type Handler struct {
router *chi.Mux
manager *webrtc.WebRTCManager
}
func New(password, static string) *chi.Mux {
router := chi.NewRouter()
manager, err := webrtc.NewManager(password)
if err != nil {
panic(err)
}
handler := &Handler{
router: router,
manager: manager,
}
router.Use(middleware.Recoverer) // Recover from panics without crashing server
// router.Use(middleware.Logger) // Log API request calls
router.Get("/ping", endpoint.Handle(handler.Ping))
router.Get("/ws", endpoint.Handle(handler.WebSocket))
fs := http.FileServer(http.Dir(static))
router.Get("/*", func(w http.ResponseWriter, r *http.Request) {
if _, err := os.Stat(static + r.RequestURI); os.IsNotExist(err) {
http.StripPrefix(r.RequestURI, fs).ServeHTTP(w, r)
} else {
fs.ServeHTTP(w, r)
}
})
router.NotFound(endpoint.Handle(func(w http.ResponseWriter, r *http.Request) error {
return &endpoint.HandlerError{
Status: http.StatusNotFound,
Message: fmt.Sprintf("Endpoint '%s' is not avalible", r.RequestURI),
}
}))
return router
}

View File

@ -0,0 +1,10 @@
package handler
import "net/http"
func (h *Handler) Ping(w http.ResponseWriter, r *http.Request) error {
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(http.StatusOK)
w.Write([]byte("."))
return nil
}

View File

@ -0,0 +1,9 @@
package handler
import (
"net/http"
)
func (h *Handler) WebSocket(w http.ResponseWriter, r *http.Request) error {
return h.manager.Upgrade(w, r)
}

View File

@ -0,0 +1,80 @@
package middleware
import (
"fmt"
"net/http"
"time"
"github.com/go-chi/chi/middleware"
"github.com/rs/zerolog/log"
)
func Logger(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
req := map[string]interface{}{}
if reqID := middleware.GetReqID(r.Context()); reqID != "" {
req["id"] = reqID
}
scheme := "http"
if r.TLS != nil {
scheme = "https"
}
req["scheme"] = scheme
req["proto"] = r.Proto
req["method"] = r.Method
req["remote"] = r.RemoteAddr
req["agent"] = r.UserAgent()
req["uri"] = fmt.Sprintf("%s://%s%s", scheme, r.Host, r.RequestURI)
fields := map[string]interface{}{}
fields["req"] = req
entry := &entry{
fields: fields,
}
ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor)
t1 := time.Now()
defer func() {
entry.Write(ww.Status(), ww.BytesWritten(), time.Since(t1))
}()
next.ServeHTTP(ww, r)
}
return http.HandlerFunc(fn)
}
type entry struct {
fields map[string]interface{}
errors []map[string]interface{}
}
func (e *entry) Write(status, bytes int, elapsed time.Duration) {
res := map[string]interface{}{}
res["time"] = time.Now().UTC().Format(time.RFC1123)
res["status"] = status
res["bytes"] = bytes
res["elapsed"] = float64(elapsed.Nanoseconds()) / 1000000.0
e.fields["res"] = res
e.fields["module"] = "api"
if len(e.errors) > 0 {
e.fields["errors"] = e.errors
log.Error().Fields(e.fields).Msgf("Request failed (%d)", status)
} else {
log.Debug().Fields(e.fields).Msgf("Request complete (%d)", status)
}
}
func (e *entry) Panic(v interface{}, stack []byte) {
err := map[string]interface{}{}
err["message"] = fmt.Sprintf("%+v", v)
err["stack"] = string(stack)
e.errors = append(e.errors, err)
}

View File

@ -0,0 +1,12 @@
package middleware
// contextKey is a value for use with context.WithValue. It's used as
// a pointer so it fits in an interface{} without allocation. This technique
// for defining context keys was copied from Go 1.7's new use of context in net/http.
type ctxKey struct {
name string
}
func (k *ctxKey) String() string {
return "neko/ctx/" + k.name
}

View File

@ -0,0 +1,24 @@
package middleware
// The original work was derived from Goji's middleware, source:
// https://github.com/zenazn/goji/tree/master/web/middleware
import (
"net/http"
"n.eko.moe/neko/internal/http/endpoint"
)
func Recoverer(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
defer func() {
if rvr := recover(); rvr != nil {
endpoint.WriteError(w, r, rvr)
}
}()
next.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}

View File

@ -0,0 +1,89 @@
package middleware
import (
"context"
"crypto/rand"
"encoding/base64"
"fmt"
"net/http"
"os"
"strings"
"sync/atomic"
)
// Key to use when setting the request ID.
type ctxKeyRequestID int
// RequestIDKey is the key that holds the unique request ID in a request context.
const RequestIDKey ctxKeyRequestID = 0
var prefix string
var reqid uint64
// A quick note on the statistics here: we're trying to calculate the chance that
// two randomly generated base62 prefixes will collide. We use the formula from
// http://en.wikipedia.org/wiki/Birthday_problem
//
// P[m, n] \approx 1 - e^{-m^2/2n}
//
// We ballpark an upper bound for $m$ by imagining (for whatever reason) a server
// that restarts every second over 10 years, for $m = 86400 * 365 * 10 = 315360000$
//
// For a $k$ character base-62 identifier, we have $n(k) = 62^k$
//
// Plugging this in, we find $P[m, n(10)] \approx 5.75%$, which is good enough for
// our purposes, and is surely more than anyone would ever need in practice -- a
// process that is rebooted a handful of times a day for a hundred years has less
// than a millionth of a percent chance of generating two colliding IDs.
func init() {
hostname, err := os.Hostname()
if hostname == "" || err != nil {
hostname = "localhost"
}
var buf [12]byte
var b64 string
for len(b64) < 10 {
rand.Read(buf[:])
b64 = base64.StdEncoding.EncodeToString(buf[:])
b64 = strings.NewReplacer("+", "", "/", "").Replace(b64)
}
prefix = fmt.Sprintf("%s/%s", hostname, b64[0:10])
}
// RequestID is a middleware that injects a request ID into the context of each
// request. A request ID is a string of the form "host.example.com/random-0001",
// where "random" is a base62 random string that uniquely identifies this go
// process, and where the last number is an atomically incremented request
// counter.
func RequestID(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
requestID := r.Header.Get("X-Request-Id")
if requestID == "" {
myid := atomic.AddUint64(&reqid, 1)
requestID = fmt.Sprintf("%s-%06d", prefix, myid)
}
ctx = context.WithValue(ctx, RequestIDKey, requestID)
next.ServeHTTP(w, r.WithContext(ctx))
}
return http.HandlerFunc(fn)
}
// GetReqID returns a request ID from the given context if one is present.
// Returns the empty string if a request ID cannot be found.
func GetReqID(ctx context.Context) string {
if ctx == nil {
return ""
}
if reqID, ok := ctx.Value(RequestIDKey).(string); ok {
return reqID
}
return ""
}
// NextRequestID generates the next request ID in the sequence.
func NextRequestID() uint64 {
return atomic.AddUint64(&reqid, 1)
}

View File

@ -0,0 +1,32 @@
package response
import (
"encoding/json"
"net/http"
"n.eko.moe/neko/internal/http/endpoint"
)
// JSON encodes data to rw in JSON format. Returns a pointer to a
// HandlerError if encoding fails.
func JSON(w http.ResponseWriter, data interface{}, status int) error {
w.WriteHeader(status)
w.Header().Set("Content-Type", "application/json")
err := json.NewEncoder(w).Encode(data)
if err != nil {
return &endpoint.HandlerError{
Status: http.StatusInternalServerError,
Message: "Unable to write JSON response",
Err: err,
}
}
return nil
}
// Empty merely sets the response code to NoContent (204).
func Empty(w http.ResponseWriter) error {
w.WriteHeader(http.StatusNoContent)
return nil
}

View File

@ -0,0 +1,203 @@
package keys
const KEY_0 = 48
const KEY_1 = 49
const KEY_2 = 50
const KEY_3 = 51
const KEY_4 = 52
const KEY_5 = 53
const KEY_6 = 54
const KEY_7 = 55
const KEY_8 = 56
const KEY_9 = 57
const KEY_A = 65
const KEY_B = 66
const KEY_C = 67
const KEY_D = 68
const KEY_E = 69
const KEY_F = 70
const KEY_G = 71
const KEY_H = 72
const KEY_I = 73
const KEY_J = 74
const KEY_K = 75
const KEY_L = 76
const KEY_M = 77
const KEY_N = 78
const KEY_O = 79
const KEY_P = 80
const KEY_Q = 81
const KEY_R = 82
const KEY_S = 83
const KEY_T = 84
const KEY_U = 85
const KEY_V = 86
const KEY_W = 87
const KEY_X = 88
const KEY_Y = 89
const KEY_Z = 90
const KEY_NUMPAD0 = 96
const KEY_NUMPAD1 = 97
const KEY_NUMPAD2 = 98
const KEY_NUMPAD3 = 99
const KEY_NUMPAD4 = 100
const KEY_NUMPAD5 = 101
const KEY_NUMPAD6 = 102
const KEY_NUMPAD7 = 103
const KEY_NUMPAD8 = 104
const KEY_NUMPAD9 = 105
const KEY_F1 = 112
const KEY_F2 = 113
const KEY_F3 = 114
const KEY_F4 = 115
const KEY_F5 = 116
const KEY_F6 = 117
const KEY_F7 = 118
const KEY_F8 = 119
const KEY_F9 = 120
const KEY_F10 = 121
const KEY_F11 = 122
const KEY_F12 = 123
const KEY_BACK_SPACE = 8
const KEY_TAB = 9
const KEY_ENTER = 13
const KEY_ENTER_ALT = 14
const KEY_SHIFT = 16
const KEY_CONTROL = 17
const KEY_ALT = 18
const KEY_ESCAPE = 27
const KEY_SPACE = 32
const KEY_PAGE_UP = 33
const KEY_PAGE_DOWN = 34
const KEY_END = 35
const KEY_LEFT = 37
const KEY_UP = 38
const KEY_RIGHT = 39
const KEY_DOWN = 40
const KEY_DELETE = 46
const KEY_SEMICOLON = 59
const KEY_SEMICOLON_ALT = 186
const KEY_EQUALS = 61
const KEY_EQUALS_ALT = 187
const KEY_MULTIPLY = 106
const KEY_ADD = 107
const KEY_SEPARATOR = 108
const KEY_SUBTRACT = 109
const KEY_SUBTRACT_ALT = 189
const KEY_DECIMAL = 110
const KEY_DIVIDE = 111
const KEY_COMMA = 188
const KEY_PERIOD = 190
const KEY_SLASH = 191
const KEY_BACK_QUOTE = 192
const KEY_BACK_SLASH = 220
const KEY_OPEN_BRACKET = 219
const KEY_CLOSE_BRACKET = 221
const KEY_QUOTE = 222
var Keyboard = map[int]string{}
func init() {
Keyboard[KEY_A] = "a"
Keyboard[KEY_B] = "b"
Keyboard[KEY_C] = "c"
Keyboard[KEY_D] = "d"
Keyboard[KEY_E] = "e"
Keyboard[KEY_F] = "f"
Keyboard[KEY_G] = "g"
Keyboard[KEY_H] = "h"
Keyboard[KEY_I] = "i"
Keyboard[KEY_J] = "j"
Keyboard[KEY_K] = "k"
Keyboard[KEY_L] = "l"
Keyboard[KEY_M] = "m"
Keyboard[KEY_N] = "n"
Keyboard[KEY_O] = "o"
Keyboard[KEY_P] = "p"
Keyboard[KEY_Q] = "q"
Keyboard[KEY_R] = "r"
Keyboard[KEY_S] = "s"
Keyboard[KEY_T] = "r"
Keyboard[KEY_U] = "u"
Keyboard[KEY_V] = "v"
Keyboard[KEY_W] = "w"
Keyboard[KEY_X] = "x"
Keyboard[KEY_Y] = "y"
Keyboard[KEY_Z] = "z"
Keyboard[KEY_0] = "0"
Keyboard[KEY_1] = "1"
Keyboard[KEY_2] = "2"
Keyboard[KEY_3] = "3"
Keyboard[KEY_4] = "4"
Keyboard[KEY_5] = "5"
Keyboard[KEY_6] = "6"
Keyboard[KEY_7] = "7"
Keyboard[KEY_8] = "8"
Keyboard[KEY_9] = "9"
Keyboard[KEY_NUMPAD0] = "0"
Keyboard[KEY_NUMPAD1] = "1"
Keyboard[KEY_NUMPAD2] = "2"
Keyboard[KEY_NUMPAD3] = "3"
Keyboard[KEY_NUMPAD4] = "4"
Keyboard[KEY_NUMPAD5] = "5"
Keyboard[KEY_NUMPAD6] = "6"
Keyboard[KEY_NUMPAD7] = "7"
Keyboard[KEY_NUMPAD8] = "8"
Keyboard[KEY_NUMPAD9] = "9"
Keyboard[KEY_F1] = "f1"
Keyboard[KEY_F2] = "f2"
Keyboard[KEY_F3] = "f3"
Keyboard[KEY_F4] = "f4"
Keyboard[KEY_F5] = "f5"
Keyboard[KEY_F6] = "f6"
Keyboard[KEY_F7] = "f7"
Keyboard[KEY_F8] = "f8"
Keyboard[KEY_F9] = "f9"
Keyboard[KEY_F10] = "f10"
Keyboard[KEY_F11] = "f11"
Keyboard[KEY_F12] = "f12"
Keyboard[KEY_QUOTE] = "'"
Keyboard[KEY_COMMA] = ","
Keyboard[KEY_PERIOD] = "."
Keyboard[KEY_SEMICOLON] = ";"
Keyboard[KEY_SEMICOLON_ALT] = ";"
Keyboard[KEY_SLASH] = "/"
Keyboard[KEY_BACK_SLASH] = "\\"
Keyboard[KEY_BACK_QUOTE] = "`"
Keyboard[KEY_OPEN_BRACKET] = "["
Keyboard[KEY_CLOSE_BRACKET] = "]"
Keyboard[KEY_EQUALS] = "="
Keyboard[KEY_EQUALS_ALT] = "="
Keyboard[KEY_MULTIPLY] = "*"
Keyboard[KEY_ADD] = "+"
Keyboard[KEY_SEPARATOR] = "."
Keyboard[KEY_SUBTRACT] = "-"
Keyboard[KEY_SUBTRACT_ALT] = "-"
Keyboard[KEY_DECIMAL] = "."
Keyboard[KEY_DIVIDE] = "/"
Keyboard[KEY_BACK_SPACE] = "backspace"
Keyboard[KEY_DELETE] = "delete"
Keyboard[KEY_ENTER] = "enter"
Keyboard[KEY_ENTER_ALT] = "enter"
Keyboard[KEY_TAB] = "tab"
Keyboard[KEY_ESCAPE] = "escape"
Keyboard[KEY_UP] = "up"
Keyboard[KEY_DOWN] = "down"
Keyboard[KEY_RIGHT] = "right"
Keyboard[KEY_LEFT] = "left"
Keyboard[KEY_END] = "end"
Keyboard[KEY_PAGE_UP] = "pageup"
Keyboard[KEY_PAGE_DOWN] = "pagedown"
Keyboard[KEY_ALT] = "alt"
Keyboard[KEY_CONTROL] = "control"
Keyboard[KEY_SHIFT] = "shift"
Keyboard[KEY_SPACE] = "space"
}

View File

@ -0,0 +1,21 @@
package keys
const MOUSE_LEFT = 0
const MOUSE_MIDDLE = 1
const MOUSE_RIGHT = 2
const MOUSE_WHEEL_UP = 4
const MOUSE_WHEEL_DOWN = 5
const MOUSE_WHEEL_RIGH = 6
const MOUSE_WHEEL_LEFT = 7
var Mouse = map[int]string{}
func init() {
Mouse[MOUSE_LEFT] = "left"
Mouse[MOUSE_MIDDLE] = "center"
Mouse[MOUSE_RIGHT] = "right"
Mouse[MOUSE_WHEEL_UP] = "wheelUp"
Mouse[MOUSE_WHEEL_DOWN] = "wheelDown"
Mouse[MOUSE_WHEEL_RIGH] = "wheelRight"
Mouse[MOUSE_WHEEL_LEFT] = "wheelLeft"
}

View File

@ -0,0 +1,71 @@
package nanoid
import (
"math/rand"
"time"
gonanoid "github.com/matoous/go-nanoid"
)
var nano *NanoID
func init() {
nano = &NanoID{
alphabet: "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
size: 16,
}
}
func New(alphabet string, size int) *NanoID {
return &NanoID{
alphabet: alphabet,
size: size,
}
}
type NanoID struct {
alphabet string
size int
}
func (n *NanoID) NewID() (string, error) {
return gonanoid.Generate(n.alphabet, n.size)
}
func (n *NanoID) NewIDSize(size int) (string, error) {
return gonanoid.Generate(n.alphabet, size)
}
func (n *NanoID) NewIDRang(max int, min int) (string, error) {
rand.Seed(time.Now().Unix())
return gonanoid.Generate(n.alphabet, rand.Intn(max-min)+min)
}
func (n *NanoID) GenerateID(alphabet string, size int) (string, error) {
return gonanoid.Generate(alphabet, size)
}
func (n *NanoID) GenerateIDRange(alphabet string, max int, min int) (string, error) {
rand.Seed(time.Now().Unix())
return gonanoid.Generate(alphabet, rand.Intn(max-min)+min)
}
func NewID() (string, error) {
return nano.NewID()
}
func NewIDSize(size int) (string, error) {
return nano.NewIDSize(size)
}
func NewIDRang(max int, min int) (string, error) {
return nano.NewIDRang(max, min)
}
func GenerateID(alphabet string, size int) (string, error) {
return nano.GenerateID(alphabet, size)
}
func GenerateIDRange(alphabet string, max int, min int) (string, error) {
return nano.GenerateIDRange(alphabet, max, min)
}

View File

@ -0,0 +1,48 @@
package preflight
import (
"runtime"
"github.com/rs/zerolog/log"
"github.com/spf13/viper"
)
func Config(name string) {
config := viper.GetString("neko.config")
if config != "" {
viper.SetConfigFile(config) // Use config file from the flag.
} else {
if runtime.GOOS == "linux" {
viper.AddConfigPath("/etc/neko/")
}
viper.AddConfigPath(".")
viper.SetConfigName(name)
}
viper.SetEnvPrefix("NEKO")
viper.AutomaticEnv() // read in environment variables that match
if err := viper.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
log.Error().Err(err)
}
if config != "" {
log.Error().Err(err)
}
}
file := viper.ConfigFileUsed()
logger := log.With().
Bool("debug", viper.GetBool("neko.debug")).
Str("logging", viper.GetString("neko.logs")).
Str("config", file).
Logger()
if file == "" {
logger.Warn().Msg("Preflight complete without config file")
} else {
logger.Info().Msg("Preflight complete")
}
}

View File

@ -0,0 +1,60 @@
package preflight
import (
"fmt"
"io"
"os"
"path/filepath"
"runtime"
"time"
"github.com/rs/zerolog"
"github.com/rs/zerolog/diode"
"github.com/rs/zerolog/log"
"github.com/spf13/viper"
)
func Logs(name string) {
zerolog.TimeFieldFormat = ""
zerolog.SetGlobalLevel(zerolog.InfoLevel)
if viper.GetBool("neko.debug") {
zerolog.SetGlobalLevel(zerolog.DebugLevel)
}
console := zerolog.ConsoleWriter{Out: os.Stdout}
if !viper.GetBool("neko.logs") {
log.Logger = log.Output(console)
} else {
logs := filepath.Join(".", "logs")
if runtime.GOOS == "linux" {
logs = "/var/log/neko"
}
if _, err := os.Stat(logs); os.IsNotExist(err) {
os.Mkdir(logs, os.ModePerm)
}
latest := filepath.Join(logs, name+"-latest.log")
_, err := os.Stat(latest)
if err == nil {
err = os.Rename(latest, filepath.Join(logs, "neko."+time.Now().Format("2006-01-02T15-04-05Z07-00")+".log"))
if err != nil {
log.Panic().Err(err).Msg("Failed to rotate log file")
}
}
logf, err := os.OpenFile(latest, os.O_RDWR|os.O_CREATE, 0666)
if err != nil {
log.Panic().Err(err).Msg("Failed to create log file")
}
logger := diode.NewWriter(logf, 1000, 10*time.Millisecond, func(missed int) {
fmt.Printf("Logger Dropped %d messages", missed)
})
log.Logger = log.Output(io.MultiWriter(console, logger))
}
}

View File

@ -0,0 +1,21 @@
package structs
import "fmt"
type Version struct {
Major string
Minor string
Patch string
Version string
GitVersion string
GitCommit string
GitTreeState string
BuildDate string
GoVersion string
Compiler string
Platform string
}
func (i *Version) String() string {
return fmt.Sprintf("%s.%s.%s", i.Major, i.Minor, i.Patch)
}

View File

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

View File

@ -0,0 +1,10 @@
package utils
const Header = `&34
_ __ __
/ | / /__ / /______ \ /\
/ |/ / _ \/ //_/ __ \ ) ( ')
/ /| / __/ ,< / /_/ / ( / )
/_/ |_/\___/_/|_|\____/ \(__)|
&1&37 nurdism/neko &33%s v%s&0
`

View File

@ -0,0 +1,25 @@
package utils
import (
"sync"
"sync/atomic"
)
type CountedSyncMap struct {
sync.Map
len uint64
}
func (m *CountedSyncMap) CountedDelete(key interface{}) {
m.Delete(key)
atomic.AddUint64(&m.len, ^uint64(0))
}
func (m *CountedSyncMap) CountedStore(key, value interface{}) {
m.Store(key, value)
atomic.AddUint64(&m.len, uint64(1))
}
func (m *CountedSyncMap) CountedLen() uint64 {
return atomic.LoadUint64(&m.len)
}

View File

@ -0,0 +1,22 @@
package webrtc
type dataHeader struct {
Event uint8
Length uint16
}
type dataMouseMove struct {
dataHeader
X int16
Y int16
}
type dataMouseKey struct {
dataHeader
Key uint8
}
type dataKeyboardKey struct {
dataHeader
Key uint16
}

View File

@ -0,0 +1,73 @@
package webrtc
import (
"math/rand"
"net/http"
"github.com/gorilla/websocket"
"github.com/pion/webrtc/v2"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"n.eko.moe/neko/internal/gst"
)
func NewManager(password string) (*WebRTCManager, error) {
engine := webrtc.MediaEngine{}
videoCodec := webrtc.NewRTPVP8Codec(webrtc.DefaultPayloadTypeVP8, 90000)
video, err := webrtc.NewTrack(webrtc.DefaultPayloadTypeVP8, rand.Uint32(), "stream", "stream", videoCodec)
if err != nil {
return nil, err
}
gst.CreatePipeline(webrtc.VP8, []*webrtc.Track{video}, "ximagesrc show-pointer=true use-damage=false ! video/x-raw,framerate=30/1 ! videoconvert").Start()
engine.RegisterCodec(videoCodec)
// ximagesrc xid=0 show-pointer=true ! videoconvert ! queue | videotestsrc
audioCodec := webrtc.NewRTPOpusCodec(webrtc.DefaultPayloadTypeOpus, 48000)
audio, err := webrtc.NewTrack(webrtc.DefaultPayloadTypeOpus, rand.Uint32(), "stream", "stream", audioCodec)
if err != nil {
return nil, err
}
gst.CreatePipeline(webrtc.Opus, []*webrtc.Track{audio}, "pulsesrc device=auto_null.monitor ! audioconvert").Start()
engine.RegisterCodec(audioCodec)
// pulsesrc device=auto_null.monitor ! audioconvert | audiotestsrc
// gst-launch-1.0 -v pulsesrc device=auto_null.monitor ! audioconvert ! vorbisenc ! oggmux ! filesink location=alsasrc.ogg
return &WebRTCManager{
logger: log.With().Str("service", "webrtc").Logger(),
engine: engine,
api: webrtc.NewAPI(webrtc.WithMediaEngine(engine)),
video: video,
audio: audio,
controller: "",
password: password,
sessions: make(map[string]*session),
upgrader: websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true
},
},
config: webrtc.Configuration{
ICEServers: []webrtc.ICEServer{
{
URLs: []string{"stun:stun.l.google.com:19302"},
},
},
SDPSemantics: webrtc.SDPSemanticsUnifiedPlanWithFallback,
},
}, nil
}
type WebRTCManager struct {
logger zerolog.Logger
upgrader websocket.Upgrader
engine webrtc.MediaEngine
api *webrtc.API
config webrtc.Configuration
password string
controller string
sessions map[string]*session
video *webrtc.Track
audio *webrtc.Track
}

View File

@ -0,0 +1,15 @@
package webrtc
type message struct {
Event string `json:"event"`
}
type messageIdentityProvide struct {
message
ID string `json:"id"`
}
type messageSDP struct {
message
SDP string `json:"sdp"`
}

View File

@ -0,0 +1,220 @@
package webrtc
import (
"bytes"
"encoding/binary"
"encoding/json"
"github.com/go-vgo/robotgo"
"github.com/pion/webrtc/v2"
"n.eko.moe/neko/internal/keys"
)
func (manager *WebRTCManager) createPeer(session *session, raw []byte) error {
payload := messageSDP{}
if err := json.Unmarshal(raw, &payload); err != nil {
return err
}
peer, err := manager.api.NewPeerConnection(manager.config)
if err != nil {
return err
}
_, err = peer.AddTrack(manager.video)
if err != nil {
return err
}
_, err = peer.AddTrack(manager.audio)
if err != nil {
return err
}
peer.SetRemoteDescription(webrtc.SessionDescription{
SDP: payload.SDP,
Type: webrtc.SDPTypeOffer,
})
answer, err := peer.CreateAnswer(nil)
if err != nil {
return err
}
if err = peer.SetLocalDescription(answer); err != nil {
return err
}
session.send(messageSDP{
message{Event: "sdp/reply"},
answer.SDP,
})
session.peer = peer
peer.OnDataChannel(func(d *webrtc.DataChannel) {
d.OnMessage(func(msg webrtc.DataChannelMessage) {
if err = manager.onData(session, msg); err != nil {
manager.logger.Warn().Err(err).Msg("onData failed")
}
})
})
peer.OnConnectionStateChange(func(connectionState webrtc.PeerConnectionState) {
switch connectionState {
case webrtc.PeerConnectionStateDisconnected:
case webrtc.PeerConnectionStateFailed:
manager.destroy(session)
break
case webrtc.PeerConnectionStateConnected:
manager.logger.Info().Str("ID", session.id).Msg("Peer connected")
break
}
})
return nil
}
var debounce = map[int]bool{}
func (manager *WebRTCManager) onData(session *session, msg webrtc.DataChannelMessage) error {
if manager.controller != session.id {
return nil
}
header := &dataHeader{}
buffer := bytes.NewBuffer(msg.Data)
byt := make([]byte, 3)
_, err := buffer.Read(byt)
if err != nil {
return err
}
err = binary.Read(bytes.NewBuffer(byt), binary.LittleEndian, header)
if err != nil {
return err
}
buffer = bytes.NewBuffer(msg.Data)
switch header.Event {
case 0x01: // MOUSE_MOVE
payload := &dataMouseMove{}
err := binary.Read(buffer, binary.LittleEndian, payload)
if err != nil {
return err
}
robotgo.Move(int(payload.X), int(payload.Y))
break
case 0x02: // MOUSE_UP
payload := &dataMouseKey{}
err := binary.Read(buffer, binary.LittleEndian, payload)
if err != nil {
return err
}
if key, ok := keys.Mouse[int(payload.Key)]; ok {
if !debounce[int(payload.Key)] {
return nil
}
debounce[int(payload.Key)] = false
robotgo.MouseToggle("up", key)
} else {
manager.logger.Warn().Msgf("Unknown MOUSE_DOWN key: %v", payload.Key)
}
break
case 0x03: // MOUSE_DOWN
payload := &dataMouseKey{}
err := binary.Read(buffer, binary.LittleEndian, payload)
if err != nil {
return err
}
if key, ok := keys.Mouse[int(payload.Key)]; ok {
if debounce[int(payload.Key)] {
return nil
}
debounce[int(payload.Key)] = true
robotgo.MouseToggle("down", key)
} else {
manager.logger.Warn().Msgf("Unknown MOUSE_DOWN key: %v", payload.Key)
}
break
case 0x04: // MOUSE_CLK
payload := &dataMouseKey{}
err := binary.Read(buffer, binary.LittleEndian, payload)
if err != nil {
return err
}
if key, ok := keys.Mouse[int(payload.Key)]; ok {
switch int(payload.Key) {
case keys.MOUSE_WHEEL_DOWN:
robotgo.Scroll(0, -10)
break
case keys.MOUSE_WHEEL_UP:
robotgo.Scroll(0, 10)
break
case keys.MOUSE_WHEEL_LEFT:
robotgo.Scroll(-10, 0)
break
case keys.MOUSE_WHEEL_RIGH:
robotgo.Scroll(10, 0)
break
default:
robotgo.Click(key, false)
}
} else {
manager.logger.Warn().Msgf("Unknown MOUSE_CLK key: %v", payload.Key)
}
break
case 0x05: // KEY_DOWN
payload := &dataKeyboardKey{}
err := binary.Read(buffer, binary.LittleEndian, payload)
if err != nil {
return err
}
if key, ok := keys.Keyboard[int(payload.Key)]; ok {
if debounce[int(payload.Key)] {
return nil
}
debounce[int(payload.Key)] = true
robotgo.KeyToggle(key, "down")
} else {
manager.logger.Warn().Msgf("Unknown KEY_DOWN key: %v", payload.Key)
}
break
case 0x06: // KEY_UP
payload := &dataKeyboardKey{}
err := binary.Read(buffer, binary.LittleEndian, payload)
if err != nil {
return err
}
if key, ok := keys.Keyboard[int(payload.Key)]; ok {
if !debounce[int(payload.Key)] {
return nil
}
debounce[int(payload.Key)] = false
robotgo.KeyToggle(key, "up")
} else {
manager.logger.Warn().Msgf("Unknown KEY_UP key: %v", payload.Key)
}
break
case 0x07: // KEY_CLK
payload := &dataKeyboardKey{}
err := binary.Read(buffer, binary.LittleEndian, payload)
if err != nil {
return err
}
if key, ok := keys.Keyboard[int(payload.Key)]; ok {
robotgo.KeyTap(key)
} else {
manager.logger.Warn().Msgf("Unknown KEY_CLK key: %v", payload.Key)
}
break
}
return nil
}

View File

@ -0,0 +1,41 @@
package webrtc
import (
"sync"
"github.com/gorilla/websocket"
"github.com/pion/webrtc/v2"
)
type session struct {
id string
socket *websocket.Conn
peer *webrtc.PeerConnection
mu sync.Mutex
}
func (session *session) send(v interface{}) error {
session.mu.Lock()
defer session.mu.Unlock()
if session.socket != nil {
return session.socket.WriteJSON(v)
}
return nil
}
func (session *session) destroy() error {
if session.peer != nil && session.peer.ConnectionState() == webrtc.PeerConnectionStateConnected {
if err := session.peer.Close(); err != nil {
return err
}
}
if session.socket != nil {
if err := session.socket.Close(); err != nil {
return err
}
}
return nil
}

View File

@ -0,0 +1,222 @@
package webrtc
import (
"encoding/json"
"net/http"
"sync"
"time"
"github.com/gorilla/websocket"
"github.com/pkg/errors"
"n.eko.moe/neko/internal/nanoid"
)
const (
// Send pings to peer with this period. Must be less than pongWait.
pingPeriod = 60 * time.Second
)
func (manager *WebRTCManager) Upgrade(w http.ResponseWriter, r *http.Request) error {
socket, err := manager.upgrader.Upgrade(w, r, nil)
if err != nil {
manager.logger.Error().Err(err).Msg("Failed to upgrade websocket!")
return nil
}
sessionID, ok := manager.authenticate(r)
if ok != true {
manager.logger.Warn().Msg("Authenticatetion failed")
if err = socket.Close(); err != nil {
return err
}
return nil
}
session := &session{
id: sessionID,
socket: socket,
mu: sync.Mutex{},
}
manager.logger.
Info().
Str("ID", sessionID).
Str("RemoteAddr", socket.RemoteAddr().String()).
Msg("Created Session")
manager.sessions[sessionID] = session
defer func() {
manager.destroy(session)
}()
if err = manager.onConnected(session); err != nil {
manager.logger.Error().Err(err).Msg("onConnected failed!")
return nil
}
manager.handleWS(session)
return nil
}
func (manager *WebRTCManager) authenticate(r *http.Request) (sessionID string, ok bool) {
passwords, ok := r.URL.Query()["password"]
if !ok || len(passwords[0]) < 1 {
return "", false
}
if passwords[0] != manager.password {
manager.logger.Warn().Str("Password", passwords[0]).Msg("Wrong password: ")
return "", false
}
id, err := nanoid.NewIDSize(32)
if err != nil {
return "", false
}
return id, true
}
func (manager *WebRTCManager) onConnected(session *session) error {
if err := session.send(messageIdentityProvide{
message: message{Event: "identity/provide"},
ID: session.id,
}); err != nil {
return err
}
return nil
}
func (manager *WebRTCManager) onMessage(session *session, raw []byte) error {
message := message{}
if err := json.Unmarshal(raw, &message); err != nil {
return err
}
switch message.Event {
case "sdp/provide":
return errors.Wrap(manager.createPeer(session, raw), "sdp/provide failed")
case "control/release":
return errors.Wrap(manager.controlRelease(session), "control/release failed")
case "control/request":
return errors.Wrap(manager.controlRequest(session), "control/request failed")
default:
manager.logger.Warn().Msgf("Unknown client method %s", message.Event)
}
return nil
}
func (manager *WebRTCManager) handleWS(session *session) {
bytes := make(chan []byte)
cancel := make(chan struct{})
ticker := time.NewTicker(pingPeriod)
go func() {
defer func() {
ticker.Stop()
manager.logger.Info().Str("RemoteAddr", session.socket.RemoteAddr().String()).Msg("Handle WS ending")
manager.destroy(session)
}()
for {
_, raw, err := session.socket.ReadMessage()
if err != nil {
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
manager.logger.Warn().Err(err).Msg("ReadMessage error")
}
break
}
bytes <- raw
}
}()
for {
select {
case raw := <-bytes:
manager.logger.Info().
Str("ID", session.id).
Str("Message", string(raw)).
Msg("Reading from Websocket")
if err := manager.onMessage(session, raw); err != nil {
manager.logger.Error().Err(err).Msg("onClientMessage has failed")
return
}
case <-cancel:
return
case _ = <-ticker.C:
if err := session.socket.WriteMessage(websocket.PingMessage, nil); err != nil {
return
}
}
}
}
func (manager *WebRTCManager) destroy(session *session) {
if manager.controller == session.id {
manager.controller = ""
for id, sess := range manager.sessions {
if id != session.id {
if err := sess.send(message{Event: "control/released"}); err != nil {
manager.logger.Error().Err(err).Msg("session.send has failed")
}
}
}
}
if err := session.destroy(); err != nil {
manager.logger.Error().Err(err).Msg("session.destroy has failed")
}
delete(manager.sessions, session.id)
}
func (manager *WebRTCManager) controlRelease(session *session) error {
if manager.controller == session.id {
manager.controller = ""
if err := session.send(message{Event: "control/release"}); err != nil {
return err
}
for id, sess := range manager.sessions {
if id != session.id {
if err := sess.send(message{Event: "control/released"}); err != nil {
return err
}
}
}
}
return nil
}
func (manager *WebRTCManager) controlRequest(session *session) error {
if manager.controller == "" {
manager.controller = session.id
if err := session.send(message{Event: "control/give"}); err != nil {
return err
}
for id, sess := range manager.sessions {
if id != session.id {
if err := sess.send(message{Event: "control/given"}); err != nil {
return err
}
}
}
} else {
if err := session.send(message{Event: "control/locked"}); err != nil {
return err
}
controller, ok := manager.sessions[manager.controller]
if ok {
controller.send(message{Event: "control/requesting"})
}
}
return nil
}