mirror of
https://github.com/m1k1o/neko.git
synced 2024-07-24 14:40:50 +12:00
initial commit - from neko open source repository..
This commit is contained in:
commit
56de805f54
9
.editorconfig
Normal file
9
.editorconfig
Normal file
@ -0,0 +1,9 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
2
.env.development
Normal file
2
.env.development
Normal file
@ -0,0 +1,2 @@
|
||||
DISPLAY=:99.0
|
||||
PION_LOG_TRACE=all
|
16
.vscode/launch.json
vendored
Normal file
16
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "launch",
|
||||
"type": "go",
|
||||
"request": "launch",
|
||||
"mode": "debug",
|
||||
"program": "${workspaceFolder}/cmd/neko",
|
||||
"envFile": "${workspaceFolder}/.env.development",
|
||||
"output": "${workspaceFolder}/bin/debug/neko",
|
||||
"cwd": "${workspaceFolder}/",
|
||||
"args": ["serve", "-d", "--bind", ":3000", "--static", "../client/dist", "--password", "neko", "--password_admin", "admin"]
|
||||
}
|
||||
]
|
||||
}
|
22
.vscode/settings.json
vendored
Normal file
22
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
|
||||
{
|
||||
"go.formatTool": "goformat",
|
||||
"go.inferGopath": false,
|
||||
"go.autocompleteUnimportedPackages": true,
|
||||
"go.delveConfig": {
|
||||
"useApiV1": false,
|
||||
"dlvLoadConfig": {
|
||||
"followPointers": true,
|
||||
"maxVariableRecurse": 3,
|
||||
"maxStringLen": 400,
|
||||
"maxArrayValues": 400,
|
||||
"maxStructFields": -1
|
||||
}
|
||||
},
|
||||
"[go]": {
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": true
|
||||
}
|
||||
}
|
||||
}
|
10
build
Normal file
10
build
Normal file
@ -0,0 +1,10 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -ex
|
||||
|
||||
BUILD_TIME=`date -u +'%Y-%m-%dT%H:%M:%SZ'`
|
||||
GIT_COMMIT=`git rev-parse --short HEAD`
|
||||
GIT_BRANCH=`git rev-parse --symbolic-full-name --abbrev-ref HEAD`
|
||||
GIT_DIRTY=`git diff-index --quiet HEAD -- || echo "✗-"`
|
||||
|
||||
go build -o bin/neko -ldflags "-s -X 'n.eko.moe/neko.buildDate=${BUILD_TIME}' -X 'n.eko.moe/neko.gitCommit=${GIT_DIRTY}${GIT_COMMIT}' -X 'n.eko.moe/neko.gitBranch=${GIT_BRANCH}'" -i cmd/neko/main.go
|
18
cmd/neko/main.go
Normal file
18
cmd/neko/main.go
Normal file
@ -0,0 +1,18 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"n.eko.moe/neko"
|
||||
"n.eko.moe/neko/cmd"
|
||||
"n.eko.moe/neko/internal/utils"
|
||||
)
|
||||
|
||||
func main() {
|
||||
fmt.Print(utils.Colorf(neko.Header, "server", neko.Service.Version))
|
||||
if err := cmd.Execute(); err != nil {
|
||||
log.Panic().Err(err).Msg("failed to execute command")
|
||||
}
|
||||
}
|
127
cmd/root.go
Normal file
127
cmd/root.go
Normal file
@ -0,0 +1,127 @@
|
||||
package cmd
|
||||
|
||||
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/cobra"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"n.eko.moe/neko"
|
||||
)
|
||||
|
||||
func Execute() error {
|
||||
return root.Execute()
|
||||
}
|
||||
|
||||
var root = &cobra.Command{
|
||||
Use: "neko",
|
||||
Short: "neko streaming server",
|
||||
Long: `neko streaming server`,
|
||||
Version: neko.Service.Version.String(),
|
||||
}
|
||||
|
||||
func init() {
|
||||
cobra.OnInitialize(func() {
|
||||
//////
|
||||
// logs
|
||||
//////
|
||||
zerolog.TimeFieldFormat = ""
|
||||
zerolog.SetGlobalLevel(zerolog.InfoLevel)
|
||||
|
||||
if viper.GetBool("debug") {
|
||||
zerolog.SetGlobalLevel(zerolog.DebugLevel)
|
||||
}
|
||||
|
||||
console := zerolog.ConsoleWriter{Out: os.Stdout}
|
||||
|
||||
if !viper.GetBool("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, "neko-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))
|
||||
}
|
||||
|
||||
//////
|
||||
// configs
|
||||
//////
|
||||
config := viper.GetString("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("neko")
|
||||
}
|
||||
|
||||
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("debug")).
|
||||
Str("logging", viper.GetString("logs")).
|
||||
Str("config", file).
|
||||
Logger()
|
||||
|
||||
if file == "" {
|
||||
logger.Warn().Msg("preflight complete without config file")
|
||||
} else {
|
||||
logger.Info().Msg("preflight complete")
|
||||
}
|
||||
|
||||
neko.Service.Root.Set()
|
||||
})
|
||||
|
||||
if err := neko.Service.Root.Init(root); err != nil {
|
||||
log.Panic().Err(err).Msg("unable to run root command")
|
||||
}
|
||||
|
||||
root.SetVersionTemplate(neko.Service.Version.Details())
|
||||
}
|
41
cmd/serve.go
Normal file
41
cmd/serve.go
Normal file
@ -0,0 +1,41 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"n.eko.moe/neko"
|
||||
"n.eko.moe/neko/internal/types/config"
|
||||
)
|
||||
|
||||
func init() {
|
||||
command := &cobra.Command{
|
||||
Use: "serve",
|
||||
Short: "serve neko streaming server",
|
||||
Long: `serve neko streaming server`,
|
||||
Run: neko.Service.ServeCommand,
|
||||
}
|
||||
|
||||
configs := []config.Config{
|
||||
neko.Service.Server,
|
||||
neko.Service.WebRTC,
|
||||
neko.Service.Remote,
|
||||
neko.Service.Broadcast,
|
||||
neko.Service.WebSocket,
|
||||
}
|
||||
|
||||
cobra.OnInitialize(func() {
|
||||
for _, cfg := range configs {
|
||||
cfg.Set()
|
||||
}
|
||||
neko.Service.Preflight()
|
||||
})
|
||||
|
||||
for _, cfg := range configs {
|
||||
if err := cfg.Init(command); err != nil {
|
||||
log.Panic().Err(err).Msg("unable to run serve command")
|
||||
}
|
||||
}
|
||||
|
||||
root.AddCommand(command)
|
||||
}
|
35
go.mod
Normal file
35
go.mod
Normal file
@ -0,0 +1,35 @@
|
||||
module n.eko.moe/neko
|
||||
|
||||
go 1.13
|
||||
|
||||
require (
|
||||
github.com/coreos/go-etcd v2.0.0+incompatible // indirect
|
||||
github.com/cpuguy83/go-md2man v1.0.10 // indirect
|
||||
github.com/fsnotify/fsnotify v1.4.9 // indirect
|
||||
github.com/go-chi/chi v4.1.0+incompatible
|
||||
github.com/golang/protobuf v1.3.5 // indirect
|
||||
github.com/gorilla/websocket v1.4.2
|
||||
github.com/kataras/go-events v0.0.2
|
||||
github.com/lucas-clemente/quic-go v0.15.2 // indirect
|
||||
github.com/mitchellh/mapstructure v1.2.2 // indirect
|
||||
github.com/pelletier/go-toml v1.7.0 // indirect
|
||||
github.com/pion/ice v0.7.12 // indirect
|
||||
github.com/pion/logging v0.2.2
|
||||
github.com/pion/rtp v1.4.0 // indirect
|
||||
github.com/pion/sdp/v2 v2.3.5 // indirect
|
||||
github.com/pion/turn v1.4.0 // indirect
|
||||
github.com/pion/webrtc/v2 v2.2.4
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/rs/zerolog v1.18.0
|
||||
github.com/spf13/afero v1.2.2 // indirect
|
||||
github.com/spf13/cast v1.3.1 // indirect
|
||||
github.com/spf13/cobra v0.0.7
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/spf13/viper v1.6.2
|
||||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8 // indirect
|
||||
golang.org/x/crypto v0.0.0-20200403201458-baeed622b8d8 // indirect
|
||||
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d // indirect
|
||||
golang.org/x/text v0.3.2 // indirect
|
||||
gopkg.in/ini.v1 v1.55.0 // indirect
|
||||
)
|
470
go.sum
Normal file
470
go.sum
Normal file
@ -0,0 +1,470 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo=
|
||||
dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU=
|
||||
dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU=
|
||||
dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4=
|
||||
dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU=
|
||||
git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/alangpierce/go-forceexport v0.0.0-20160317203124-8f1d6941cd75/go.mod h1:uAXEEpARkRhCZfEvy/y0Jcc888f9tHCc1W7/UeEtreE=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
|
||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
|
||||
github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE=
|
||||
github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
|
||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
|
||||
github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk=
|
||||
github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY=
|
||||
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
||||
github.com/go-chi/chi v4.0.3+incompatible h1:gakN3pDJnzZN5jqFV2TEdF66rTfKeITyR8qu6ekICEY=
|
||||
github.com/go-chi/chi v4.0.3+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
|
||||
github.com/go-chi/chi v4.1.0+incompatible h1:ETj3cggsVIY2Xao5ExCu6YhEh5MD6JTfcBzS37R260w=
|
||||
github.com/go-chi/chi v4.1.0+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
|
||||
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0 h1:28o5sBqPkBsMGnC6b4MvE2TzSr5/AT4c/1fLqVGIwlk=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.5 h1:F768QJ1E9tib+q5Sc8MkdJi1RxLTbRcTf8LJV56aRls=
|
||||
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
|
||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
|
||||
github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
|
||||
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU=
|
||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/kataras/go-events v0.0.2 h1:fhyUPXvUbrjIPmH4vRdrAAGoNzdcwJPQmjhg47m1nMU=
|
||||
github.com/kataras/go-events v0.0.2/go.mod h1:6IxMW59VJdEIqj3bjFGJvGLRdb0WHtrlxPZy9qXctcg=
|
||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/lucas-clemente/quic-go v0.7.1-0.20190401152353-907071221cf9 h1:tbuodUh2vuhOVZAdW3NEUvosFHUMJwUNl7jk/VSEiwc=
|
||||
github.com/lucas-clemente/quic-go v0.7.1-0.20190401152353-907071221cf9/go.mod h1:PpMmPfPKO9nKJ/psF49ESTAGQSdfXxlg1otPbEB2nOw=
|
||||
github.com/lucas-clemente/quic-go v0.15.2 h1:RgxRJ7rPde0Q/uXDeb3/UdblVvxrYGDAG9G9GO78LmI=
|
||||
github.com/lucas-clemente/quic-go v0.15.2/go.mod h1:qxmO5Y4ZMhdNkunGfxuZnZXnJwYpW9vjQkyrZ7BsgUI=
|
||||
github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
|
||||
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/marten-seemann/qpack v0.1.0/go.mod h1:LFt1NU/Ptjip0C2CPkhimBz5CGE3WGDAUWqna+CNTrI=
|
||||
github.com/marten-seemann/qtls v0.2.3 h1:0yWJ43C62LsZt08vuQJDK1uC1czUc3FJeCLPoNAI4vA=
|
||||
github.com/marten-seemann/qtls v0.2.3/go.mod h1:xzjG7avBwGGbdZ8dTGxlBnLArsVKLvwmjgmPuiQEcYk=
|
||||
github.com/marten-seemann/qtls v0.8.0 h1:aj+MPLibzKByw8CmG0WvWgbtBkctYPAXeB11cQJC8mo=
|
||||
github.com/marten-seemann/qtls v0.8.0/go.mod h1:Lao6jDqlCfxyLKYFmZXGm2LSHBgVn+P+ROOex6YkT+k=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.2.2 h1:dxe5oCinTXiTIcfgmZecdCzPmAJKd46KsCWc35r0TV4=
|
||||
github.com/mitchellh/mapstructure v1.2.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
|
||||
github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
|
||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=
|
||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
|
||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/onsi/gomega v1.8.1/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA=
|
||||
github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
|
||||
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/pelletier/go-toml v1.7.0 h1:7utD74fnzVc/cpcyy8sjrlFr5vYpypUixARcHIMIGuI=
|
||||
github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE=
|
||||
github.com/pion/datachannel v1.4.13 h1:ezTn3AtUtXvKemRRjRdUgao/T8bH4ZJwrpOqU8Iz3Ss=
|
||||
github.com/pion/datachannel v1.4.13/go.mod h1:+rBUwEDonA63KXx994DP/ofyyGVAm6AIMvOqQZxjWRU=
|
||||
github.com/pion/datachannel v1.4.16 h1:dvuDC0IBMUDQvwO+gRu0Dv+W5j7rrgNpCmtheb6iYnc=
|
||||
github.com/pion/datachannel v1.4.16/go.mod h1:gRGhxZv7X2/30Qxes4WEXtimKBXcwj/3WsDtBlHnvJY=
|
||||
github.com/pion/dtls/v2 v2.0.0-rc.3 h1:u9utI+EDJOjOWfrkGQsD8WNssPcTwfYIanFB6oI8K+4=
|
||||
github.com/pion/dtls/v2 v2.0.0-rc.3/go.mod h1:x0XH+cN5z+l/+/4nYL8r4sB8g6+0d1Zp2Pfkcoz8BKY=
|
||||
github.com/pion/dtls/v2 v2.0.0-rc.7/go.mod h1:U199DvHpRBN0muE9+tVN4TMy1jvEhZIZ63lk4xkvVSk=
|
||||
github.com/pion/dtls/v2 v2.0.0-rc.9 h1:wPb0JKmYoleAM2o8vQSPaUM+geJq7l0AdeUlPsg19ec=
|
||||
github.com/pion/dtls/v2 v2.0.0-rc.9/go.mod h1:6eFkFvpo0T+odQ+39HFEtOO7LX5cUlFqXdSo4ucZtGg=
|
||||
github.com/pion/ice v0.7.6 h1:EARj1MBq5NYaMtXVhYkK03i0RS/meejNHvZS++K5tSY=
|
||||
github.com/pion/ice v0.7.6/go.mod h1:4xCajahEEvc5w0AM+Ujx/Rr2EczON/fKndi3jLyDdh4=
|
||||
github.com/pion/ice v0.7.10/go.mod h1:YufCtyMmPeZXGTuARCracYfe0mb3EXbcRUS+vHJB5Cs=
|
||||
github.com/pion/ice v0.7.12 h1:Lsh4f0Uvh/vOCXSyj+w5C736LrKt66qAKeA2LFwSkn0=
|
||||
github.com/pion/ice v0.7.12/go.mod h1:yLt/9LAJEZXFtnOBdpq5YGaOF9SsDjVGCvzF3MF4k5k=
|
||||
github.com/pion/logging v0.2.1/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
|
||||
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
|
||||
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
|
||||
github.com/pion/mdns v0.0.4 h1:O4vvVqr4DGX63vzmO6Fw9vpy3lfztVWHGCQfyw0ZLSY=
|
||||
github.com/pion/mdns v0.0.4/go.mod h1:R1sL0p50l42S5lJs91oNdUL58nm0QHrhxnSegr++qC0=
|
||||
github.com/pion/quic v0.1.1 h1:D951FV+TOqI9A0rTF7tHx0Loooqz+nyzjEyj8o3PuMA=
|
||||
github.com/pion/quic v0.1.1/go.mod h1:zEU51v7ru8Mp4AUBJvj6psrSth5eEFNnVQK5K48oV3k=
|
||||
github.com/pion/rtcp v1.2.1 h1:S3yG4KpYAiSmBVqKAfgRa5JdwBNj4zK3RLUa8JYdhak=
|
||||
github.com/pion/rtcp v1.2.1/go.mod h1:a5dj2d6BKIKHl43EnAOIrCczcjESrtPuMgfmL6/K6QM=
|
||||
github.com/pion/rtp v1.1.3/go.mod h1:/l4cvcKd0D3u9JLs2xSVI95YkfXW87a3br3nqmVtSlE=
|
||||
github.com/pion/rtp v1.1.4 h1:P6xh8Y8JfzR7+JAbI79X2M8kfYETaqbuM5Otm+Z+k6U=
|
||||
github.com/pion/rtp v1.1.4/go.mod h1:/l4cvcKd0D3u9JLs2xSVI95YkfXW87a3br3nqmVtSlE=
|
||||
github.com/pion/rtp v1.3.2/go.mod h1:q9wPnA96pu2urCcW/sK/RiDn597bhGoAQQ+y2fDwHuY=
|
||||
github.com/pion/rtp v1.4.0 h1:EkeHEXKuJhZoRUxtL2Ie80vVg9gBH+poT9UoL8M14nw=
|
||||
github.com/pion/rtp v1.4.0/go.mod h1:/l4cvcKd0D3u9JLs2xSVI95YkfXW87a3br3nqmVtSlE=
|
||||
github.com/pion/sctp v1.7.3 h1:Pok18oncuAq/WjNxbyltfBSLvbv/6QSCyVJKYyDWP5M=
|
||||
github.com/pion/sctp v1.7.3/go.mod h1:c6C9jaDGX7f5xeSRVju/140XatpO9sOVe81EwpfzAc8=
|
||||
github.com/pion/sctp v1.7.6 h1:8qZTdJtbKfAns/Hv5L0PAj8FyXcsKhMH1pKUCGisQg4=
|
||||
github.com/pion/sctp v1.7.6/go.mod h1:ichkYQ5tlgCQwEwvgfdcAolqx1nHbYCxo4D7zK/K0X8=
|
||||
github.com/pion/sdp/v2 v2.3.1 h1:45dub4NRdwyDmQCD3GIY7DZuqC49GBUwBdjuetvdOr0=
|
||||
github.com/pion/sdp/v2 v2.3.1/go.mod h1:jccXVYW0fuK6ds2pwKr89SVBDYlCjhgMI6nucl5R5rA=
|
||||
github.com/pion/sdp/v2 v2.3.4/go.mod h1:jccXVYW0fuK6ds2pwKr89SVBDYlCjhgMI6nucl5R5rA=
|
||||
github.com/pion/sdp/v2 v2.3.5 h1:DtS9Z9R+E3/mn2jt+RQKBnneK1g+p3PT25+TkQHodfU=
|
||||
github.com/pion/sdp/v2 v2.3.5/go.mod h1:+ZZf35r1+zbaWYiZLfPutWfx58DAWcGb2QsS3D/s9M8=
|
||||
github.com/pion/srtp v1.2.6 h1:mHQuAMh0P67R7/j1F260u3O+fbRWLyjKLRPZYYvODFM=
|
||||
github.com/pion/srtp v1.2.6/go.mod h1:rd8imc5htjfs99XiEoOjLMEOcVjME63UHx9Ek9IGst0=
|
||||
github.com/pion/srtp v1.3.1 h1:WNDLN41ST0P6cXRpzx97JJW//vChAEo1+Etdqo+UMnM=
|
||||
github.com/pion/srtp v1.3.1/go.mod h1:nxEytDDGTN+eNKJ1l5gzOCWQFuksgijorsSlgEjc40Y=
|
||||
github.com/pion/stun v0.3.3 h1:brYuPl9bN9w/VM7OdNzRSLoqsnwlyNvD9MVeJrHjDQw=
|
||||
github.com/pion/stun v0.3.3/go.mod h1:xrCld6XM+6GWDZdvjPlLMsTU21rNxnO6UO8XsAvHr/M=
|
||||
github.com/pion/transport v0.6.0/go.mod h1:iWZ07doqOosSLMhZ+FXUTq+TamDoXSllxpbGcfkCmbE=
|
||||
github.com/pion/transport v0.8.9/go.mod h1:lpeSM6KJFejVtZf8k0fgeN7zE73APQpTF83WvA1FVP8=
|
||||
github.com/pion/transport v0.8.10 h1:lTiobMEw2PG6BH/mgIVqTV2mBp/mPT+IJLaN8ZxgdHk=
|
||||
github.com/pion/transport v0.8.10/go.mod h1:tBmha/UCjpum5hqTWhfAEs3CO4/tHSg0MYRhSzR+CZ8=
|
||||
github.com/pion/transport v0.10.0 h1:9M12BSneJm6ggGhJyWpDveFOstJsTiQjkLf4M44rm80=
|
||||
github.com/pion/transport v0.10.0/go.mod h1:BnHnUipd0rZQyTVB2SBGojFHT9CBt5C5TcsJSQGkvSE=
|
||||
github.com/pion/turn v1.4.0 h1:7NUMRehQz4fIo53Qv9ui1kJ0Kr1CA82I81RHKHCeM80=
|
||||
github.com/pion/turn v1.4.0/go.mod h1:aDSi6hWX/hd1+gKia9cExZOR0MU95O7zX9p3Gw/P2aU=
|
||||
github.com/pion/turn/v2 v2.0.3 h1:SJUUIbcPoehlyZgMyIUbBBDhI03sBx32x3JuSIBKBWA=
|
||||
github.com/pion/turn/v2 v2.0.3/go.mod h1:kl1hmT3NxcLynpXVnwJgObL8C9NaCyPTeqI2DcCpSZs=
|
||||
github.com/pion/webrtc/v2 v2.1.18 h1:g0VN0xfEUSlVNfQmlCD6yOeXy/tMaktESBmHMnBS3bk=
|
||||
github.com/pion/webrtc/v2 v2.1.18/go.mod h1:m0rKlYgLRZWyhmcMWegpF6xtK1ASxmOg8DAR74ttzQY=
|
||||
github.com/pion/webrtc/v2 v2.2.4 h1:elWyBI/6S2kNoJ5rcTj2EMAvp/fmeiEqiuYbUd0ShRA=
|
||||
github.com/pion/webrtc/v2 v2.2.4/go.mod h1:9vTqkEISnB5AXlggrFSBLGSz/kopQuYUYzHubuFpnCU=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v0.9.3 h1:9iH4JKXLzFbOAdtqv/a+j8aewx2Y8lAjAydhbaScPF8=
|
||||
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||
github.com/prometheus/common v0.4.0 h1:7etb9YClo3a6HjLzfl6rIQaU+FDfi0VSX39io3aQ+DM=
|
||||
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084 h1:sofwID9zm4tzrgykg80hfFph1mryUeLRsUfoocVVmRY=
|
||||
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
|
||||
github.com/rs/zerolog v1.17.2 h1:RMRHFw2+wF7LO0QqtELQwo8hqSmqISyCJeFeAAuWcRo=
|
||||
github.com/rs/zerolog v1.17.2/go.mod h1:9nvC1axdVrAHcu/s9taAVfBuIdTZLVQmKQyvrUjF5+I=
|
||||
github.com/rs/zerolog v1.18.0 h1:CbAm3kP2Tptby1i9sYy2MGRg0uxIN9cyDb59Ys7W8z8=
|
||||
github.com/rs/zerolog v1.18.0/go.mod h1:9nvC1axdVrAHcu/s9taAVfBuIdTZLVQmKQyvrUjF5+I=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY=
|
||||
github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM=
|
||||
github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0=
|
||||
github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
|
||||
github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
|
||||
github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw=
|
||||
github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI=
|
||||
github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU=
|
||||
github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag=
|
||||
github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg=
|
||||
github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw=
|
||||
github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y=
|
||||
github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
|
||||
github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q=
|
||||
github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ=
|
||||
github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I=
|
||||
github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0=
|
||||
github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ=
|
||||
github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk=
|
||||
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4=
|
||||
github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
|
||||
github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE=
|
||||
github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
|
||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc=
|
||||
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
|
||||
github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
|
||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
|
||||
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s=
|
||||
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
|
||||
github.com/spf13/cobra v0.0.7 h1:FfTH+vuMXOas8jmfb5/M7dzEYx7LpcLb7a0LPe34uOU=
|
||||
github.com/spf13/cobra v0.0.7/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
|
||||
github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
|
||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
|
||||
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
|
||||
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
||||
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
|
||||
github.com/spf13/viper v1.6.1 h1:VPZzIkznI1YhVMRi6vNFLHSwhnhReBfgTxIPccpfdZk=
|
||||
github.com/spf13/viper v1.6.1/go.mod h1:t3iDnF5Jlj76alVNuyFBk5oUMCvsrkbvZK0WQdfDi5k=
|
||||
github.com/spf13/viper v1.6.2 h1:7aKfF+e8/k68gda3LOjo5RxiUqddoFxVq4BKBPrxk5E=
|
||||
github.com/spf13/viper v1.6.2/go.mod h1:t3iDnF5Jlj76alVNuyFBk5oUMCvsrkbvZK0WQdfDi5k=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
|
||||
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
|
||||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||
github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU=
|
||||
github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM=
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
|
||||
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
|
||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE=
|
||||
golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191029031824-8986dd9e96cf/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413 h1:ULYEB3JvPRE/IfO+9uO7vKV/xzVTO7XPAwm8xbf4w2g=
|
||||
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200128174031-69ecbb4d6d5d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200403201458-baeed622b8d8 h1:fpnn/HnJONpIu6hkXi1u/7rR0NzilgWr4T0JmWkEitk=
|
||||
golang.org/x/crypto v0.0.0-20200403201458-baeed622b8d8/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190228165749-92fc7df08ae7/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8=
|
||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e h1:3G+cUijn7XD+S4eJFddp53Pv7+slrESplyjG25HgL+k=
|
||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191206220618-eeba5f6aabab h1:FvshnhkKW+LO3HWHodML8kuVX8rnJTxKm9dFPuI68UM=
|
||||
golang.org/x/sys v0.0.0-20191206220618-eeba5f6aabab/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d h1:nc5K6ox/4lTFbMVSL9WRR81ixkcwXThoiF6yf+R9scA=
|
||||
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190828213141-aed303cbaa74/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
|
||||
google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
|
||||
google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg=
|
||||
google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
||||
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
|
||||
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno=
|
||||
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.55.0 h1:E8yzL5unfpW3M6fz/eB7Cb5MQAYSZ7GKo4Qth+N2sgQ=
|
||||
gopkg.in/ini.v1 v1.55.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=
|
||||
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||
sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck=
|
||||
sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=
|
83
internal/broadcast/manager.go
Normal file
83
internal/broadcast/manager.go
Normal file
@ -0,0 +1,83 @@
|
||||
package broadcast
|
||||
|
||||
import (
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"n.eko.moe/neko/internal/gst"
|
||||
"n.eko.moe/neko/internal/types/config"
|
||||
)
|
||||
|
||||
type BroadcastManager struct {
|
||||
logger zerolog.Logger
|
||||
pipeline *gst.Pipeline
|
||||
remote *config.Remote
|
||||
config *config.Broadcast
|
||||
enabled bool
|
||||
url string
|
||||
}
|
||||
|
||||
func New(remote *config.Remote, config *config.Broadcast) *BroadcastManager {
|
||||
return &BroadcastManager{
|
||||
logger: log.With().Str("module", "remote").Logger(),
|
||||
remote: remote,
|
||||
config: config,
|
||||
enabled: false,
|
||||
url: "",
|
||||
}
|
||||
}
|
||||
|
||||
func (manager *BroadcastManager) Start() {
|
||||
if !manager.enabled || manager.IsActive() {
|
||||
return
|
||||
}
|
||||
|
||||
var err error
|
||||
manager.pipeline, err = gst.CreateRTMPPipeline(
|
||||
manager.remote.Device,
|
||||
manager.remote.Display,
|
||||
manager.config.Pipeline,
|
||||
manager.url,
|
||||
)
|
||||
|
||||
manager.logger.Info().
|
||||
Str("audio_device", manager.remote.Device).
|
||||
Str("video_display", manager.remote.Display).
|
||||
Str("rtmp_pipeline_src", manager.pipeline.Src).
|
||||
Msgf("RTMP pipeline is starting...")
|
||||
|
||||
if err != nil {
|
||||
manager.logger.Panic().Err(err).Msg("unable to create rtmp pipeline")
|
||||
return
|
||||
}
|
||||
|
||||
manager.pipeline.Play()
|
||||
}
|
||||
|
||||
func (manager *BroadcastManager) Stop() {
|
||||
if !manager.IsActive() {
|
||||
return
|
||||
}
|
||||
|
||||
manager.pipeline.Stop()
|
||||
manager.pipeline = nil
|
||||
}
|
||||
|
||||
func (manager *BroadcastManager) IsActive() bool {
|
||||
return manager.pipeline != nil
|
||||
}
|
||||
|
||||
func (manager *BroadcastManager) Create(url string) {
|
||||
manager.url = url
|
||||
manager.enabled = true
|
||||
manager.Start()
|
||||
}
|
||||
|
||||
func (manager *BroadcastManager) Destroy() {
|
||||
manager.Stop()
|
||||
manager.enabled = false
|
||||
}
|
||||
|
||||
func (manager *BroadcastManager) GetUrl() string {
|
||||
return manager.url
|
||||
}
|
95
internal/gst/gst.c
Normal file
95
internal/gst/gst.c
Normal file
@ -0,0 +1,95 @@
|
||||
#include "gst.h"
|
||||
|
||||
#include <gst/app/gstappsrc.h>
|
||||
|
||||
typedef struct SampleHandlerUserData {
|
||||
int pipelineId;
|
||||
} SampleHandlerUserData;
|
||||
|
||||
void gstreamer_init(void) {
|
||||
gst_init(NULL, NULL);
|
||||
}
|
||||
|
||||
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), ©, ©_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) {
|
||||
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_play_pipeline(GstElement *pipeline) {
|
||||
gst_element_set_state(pipeline, GST_STATE_PLAYING);
|
||||
}
|
||||
|
||||
void gstreamer_send_stop_pipeline(GstElement *pipeline) {
|
||||
gst_element_set_state(pipeline, GST_STATE_NULL);
|
||||
}
|
||||
|
||||
|
277
internal/gst/gst.go
Normal file
277
internal/gst/gst.go
Normal file
@ -0,0 +1,277 @@
|
||||
package gst
|
||||
|
||||
/*
|
||||
#cgo pkg-config: gstreamer-1.0 gstreamer-app-1.0
|
||||
|
||||
#include "gst.h"
|
||||
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"unsafe"
|
||||
|
||||
"github.com/pion/webrtc/v2"
|
||||
|
||||
"n.eko.moe/neko/internal/types"
|
||||
)
|
||||
|
||||
/*
|
||||
apt-get install \
|
||||
libgstreamer1.0-0 \
|
||||
gstreamer1.0-plugins-base \
|
||||
gstreamer1.0-plugins-good \
|
||||
gstreamer1.0-plugins-bad \
|
||||
gstreamer1.0-plugins-ugly\
|
||||
gstreamer1.0-libav \
|
||||
gstreamer1.0-doc \
|
||||
gstreamer1.0-tools \
|
||||
gstreamer1.0-x \
|
||||
gstreamer1.0-alsa \
|
||||
gstreamer1.0-pulseaudio
|
||||
|
||||
gst-inspect-1.0 --version
|
||||
gst-inspect-1.0 plugin
|
||||
gst-launch-1.0 ximagesrc show-pointer=true use-damage=false ! video/x-raw,framerate=30/1 ! videoconvert ! queue ! vp8enc error-resilient=partitions keyframe-max-dist=10 auto-alt-ref=true cpu-used=5 deadline=1 ! autovideosink
|
||||
gst-launch-1.0 pulsesrc ! audioconvert ! opusenc ! autoaudiosink
|
||||
*/
|
||||
|
||||
// Pipeline is a wrapper for a GStreamer Pipeline
|
||||
type Pipeline struct {
|
||||
Pipeline *C.GstElement
|
||||
Sample chan types.Sample
|
||||
CodecName string
|
||||
ClockRate float32
|
||||
Src string
|
||||
id int
|
||||
}
|
||||
|
||||
var pipelines = make(map[int]*Pipeline)
|
||||
var pipelinesLock sync.Mutex
|
||||
var registry *C.GstRegistry
|
||||
|
||||
const (
|
||||
videoClockRate = 90000
|
||||
audioClockRate = 48000
|
||||
pcmClockRate = 8000
|
||||
videoSrc = "ximagesrc xid=%s show-pointer=true use-damage=false ! video/x-raw ! videoconvert ! queue ! "
|
||||
audioSrc = "pulsesrc device=%s ! audio/x-raw,channels=2 ! audioconvert ! "
|
||||
)
|
||||
|
||||
func init() {
|
||||
C.gstreamer_init()
|
||||
registry = C.gst_registry_get()
|
||||
}
|
||||
|
||||
// CreateRTMPPipeline creates a GStreamer Pipeline
|
||||
func CreateRTMPPipeline(pipelineDevice string, pipelineDisplay string, pipelineSrc string, pipelineRTMP string) (*Pipeline, error) {
|
||||
video := fmt.Sprintf(videoSrc, pipelineDisplay)
|
||||
audio := fmt.Sprintf(audioSrc, pipelineDevice)
|
||||
|
||||
var pipelineStr string
|
||||
if pipelineSrc != "" {
|
||||
pipelineStr = fmt.Sprintf(pipelineSrc, pipelineRTMP, pipelineDevice, pipelineDisplay)
|
||||
} else {
|
||||
pipelineStr = fmt.Sprintf("flvmux name=mux ! rtmpsink location='%s live=1' %s audio/x-raw,channels=2 ! audioconvert ! voaacenc ! mux. %s x264enc bframes=0 key-int-max=60 byte-stream=true tune=zerolatency speed-preset=veryfast ! mux.", pipelineRTMP, audio, video)
|
||||
}
|
||||
|
||||
return CreatePipeline(pipelineStr, "", 0)
|
||||
}
|
||||
|
||||
// CreateAppPipeline creates a GStreamer Pipeline
|
||||
func CreateAppPipeline(codecName string, pipelineDevice string, pipelineSrc string) (*Pipeline, error) {
|
||||
pipelineStr := " ! appsink name=appsink"
|
||||
|
||||
var clockRate float32
|
||||
|
||||
switch codecName {
|
||||
case webrtc.VP8:
|
||||
// https://gstreamer.freedesktop.org/documentation/vpx/vp8enc.html?gi-language=c
|
||||
// gstreamer1.0-plugins-good
|
||||
// vp8enc error-resilient=partitions keyframe-max-dist=10 auto-alt-ref=true cpu-used=5 deadline=1
|
||||
if err := CheckPlugins([]string{"ximagesrc", "vpx"}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
clockRate = videoClockRate
|
||||
|
||||
if pipelineSrc != "" {
|
||||
pipelineStr = fmt.Sprintf(pipelineSrc+pipelineStr, pipelineDevice)
|
||||
} else {
|
||||
pipelineStr = fmt.Sprintf(videoSrc+"vp8enc cpu-used=8 threads=2 deadline=1 error-resilient=partitions keyframe-max-dist=10 auto-alt-ref=true"+pipelineStr, pipelineDevice)
|
||||
}
|
||||
case webrtc.VP9:
|
||||
// https://gstreamer.freedesktop.org/documentation/vpx/vp9enc.html?gi-language=c
|
||||
// gstreamer1.0-plugins-good
|
||||
// vp9enc
|
||||
if err := CheckPlugins([]string{"ximagesrc", "vpx"}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
clockRate = videoClockRate
|
||||
|
||||
// Causes panic! not sure why...
|
||||
if pipelineSrc != "" {
|
||||
pipelineStr = fmt.Sprintf(pipelineSrc+pipelineStr, pipelineDevice)
|
||||
} else {
|
||||
pipelineStr = fmt.Sprintf(videoSrc+"vp9enc"+pipelineStr, pipelineDevice)
|
||||
}
|
||||
case webrtc.H264:
|
||||
// https://gstreamer.freedesktop.org/documentation/openh264/openh264enc.html?gi-language=c#openh264enc
|
||||
// gstreamer1.0-plugins-bad
|
||||
// openh264enc multi-thread=4 complexity=high bitrate=3072000 max-bitrate=4096000
|
||||
if err := CheckPlugins([]string{"ximagesrc"}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
clockRate = videoClockRate
|
||||
|
||||
if pipelineSrc != "" {
|
||||
pipelineStr = fmt.Sprintf(pipelineSrc+pipelineStr, pipelineDevice)
|
||||
} else {
|
||||
pipelineStr = fmt.Sprintf(videoSrc+"openh264enc multi-thread=4 complexity=high bitrate=3072000 max-bitrate=4096000 ! video/x-h264,stream-format=byte-stream"+pipelineStr, pipelineDevice)
|
||||
|
||||
// https://gstreamer.freedesktop.org/documentation/x264/index.html?gi-language=c
|
||||
// gstreamer1.0-plugins-ugly
|
||||
// video/x-raw,format=I420 ! x264enc bframes=0 key-int-max=60 byte-stream=true tune=zerolatency speed-preset=veryfast ! video/x-h264,stream-format=byte-stream
|
||||
if err := CheckPlugins([]string{"openh264"}); err != nil {
|
||||
pipelineStr = fmt.Sprintf(videoSrc+"video/x-raw,format=I420 ! x264enc bframes=0 key-int-max=60 byte-stream=true tune=zerolatency speed-preset=veryfast ! video/x-h264,stream-format=byte-stream"+pipelineStr, pipelineDevice)
|
||||
|
||||
if err := CheckPlugins([]string{"x264"}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
case webrtc.Opus:
|
||||
// https://gstreamer.freedesktop.org/documentation/opus/opusenc.html
|
||||
// gstreamer1.0-plugins-base
|
||||
// opusenc
|
||||
if err := CheckPlugins([]string{"pulseaudio", "opus"}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
clockRate = audioClockRate
|
||||
|
||||
if pipelineSrc != "" {
|
||||
pipelineStr = fmt.Sprintf(pipelineSrc+pipelineStr, pipelineDevice)
|
||||
} else {
|
||||
pipelineStr = fmt.Sprintf(audioSrc+"opusenc"+pipelineStr, pipelineDevice)
|
||||
}
|
||||
case webrtc.G722:
|
||||
// https://gstreamer.freedesktop.org/documentation/libav/avenc_g722.html?gi-language=c
|
||||
// gstreamer1.0-libav
|
||||
// avenc_g722
|
||||
if err := CheckPlugins([]string{"pulseaudio", "libav"}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
clockRate = audioClockRate
|
||||
|
||||
if pipelineSrc != "" {
|
||||
pipelineStr = fmt.Sprintf(pipelineSrc+pipelineStr, pipelineDevice)
|
||||
} else {
|
||||
pipelineStr = fmt.Sprintf(audioSrc+"avenc_g722"+pipelineStr, pipelineDevice)
|
||||
}
|
||||
case webrtc.PCMU:
|
||||
// https://gstreamer.freedesktop.org/documentation/mulaw/mulawenc.html?gi-language=c
|
||||
// gstreamer1.0-plugins-good
|
||||
// audio/x-raw, rate=8000 ! mulawenc
|
||||
if err := CheckPlugins([]string{"pulseaudio", "mulaw"}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
clockRate = pcmClockRate
|
||||
|
||||
if pipelineSrc != "" {
|
||||
pipelineStr = fmt.Sprintf(pipelineSrc+pipelineStr, pipelineDevice)
|
||||
} else {
|
||||
pipelineStr = fmt.Sprintf(audioSrc+"audio/x-raw, rate=8000 ! mulawenc"+pipelineStr, pipelineDevice)
|
||||
}
|
||||
case webrtc.PCMA:
|
||||
// https://gstreamer.freedesktop.org/documentation/alaw/alawenc.html?gi-language=c
|
||||
// gstreamer1.0-plugins-good
|
||||
// audio/x-raw, rate=8000 ! alawenc
|
||||
if err := CheckPlugins([]string{"pulseaudio", "alaw"}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
clockRate = pcmClockRate
|
||||
|
||||
if pipelineSrc != "" {
|
||||
pipelineStr = fmt.Sprintf(pipelineSrc+pipelineStr, pipelineDevice)
|
||||
} else {
|
||||
pipelineStr = fmt.Sprintf(audioSrc+"audio/x-raw, rate=8000 ! alawenc"+pipelineStr, pipelineDevice)
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown codec %s", codecName)
|
||||
}
|
||||
|
||||
return CreatePipeline(pipelineStr, codecName, clockRate)
|
||||
}
|
||||
|
||||
// CreatePipeline creates a GStreamer Pipeline
|
||||
func CreatePipeline(pipelineStr string, codecName string, clockRate float32) (*Pipeline, error) {
|
||||
pipelineStrUnsafe := C.CString(pipelineStr)
|
||||
defer C.free(unsafe.Pointer(pipelineStrUnsafe))
|
||||
|
||||
pipelinesLock.Lock()
|
||||
defer pipelinesLock.Unlock()
|
||||
|
||||
p := &Pipeline{
|
||||
Pipeline: C.gstreamer_send_create_pipeline(pipelineStrUnsafe),
|
||||
Sample: make(chan types.Sample),
|
||||
CodecName: codecName,
|
||||
ClockRate: clockRate,
|
||||
Src: pipelineStr,
|
||||
id: len(pipelines),
|
||||
}
|
||||
|
||||
pipelines[p.id] = p
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// Start starts the GStreamer Pipeline
|
||||
func (p *Pipeline) Start() {
|
||||
C.gstreamer_send_start_pipeline(p.Pipeline, C.int(p.id))
|
||||
}
|
||||
|
||||
// Play starts the GStreamer Pipeline
|
||||
func (p *Pipeline) Play() {
|
||||
C.gstreamer_send_play_pipeline(p.Pipeline)
|
||||
}
|
||||
|
||||
// Stop stops the GStreamer Pipeline
|
||||
func (p *Pipeline) Stop() {
|
||||
C.gstreamer_send_stop_pipeline(p.Pipeline)
|
||||
}
|
||||
|
||||
// gst-inspect-1.0
|
||||
func CheckPlugins(plugins []string) error {
|
||||
var plugin *C.GstPlugin
|
||||
for _, pluginstr := range plugins {
|
||||
plugincstr := C.CString(pluginstr)
|
||||
plugin = C.gst_registry_find_plugin(registry, plugincstr)
|
||||
C.free(unsafe.Pointer(plugincstr))
|
||||
if plugin == nil {
|
||||
return fmt.Errorf("required gstreamer plugin %s not found", pluginstr)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
//export goHandlePipelineBuffer
|
||||
func goHandlePipelineBuffer(buffer unsafe.Pointer, bufferLen C.int, duration C.int, pipelineID C.int) {
|
||||
pipelinesLock.Lock()
|
||||
pipeline, ok := pipelines[int(pipelineID)]
|
||||
pipelinesLock.Unlock()
|
||||
|
||||
if ok {
|
||||
samples := uint32(pipeline.ClockRate * (float32(duration) / 1000000000))
|
||||
pipeline.Sample <- types.Sample{Data: C.GoBytes(buffer, bufferLen), Samples: samples}
|
||||
} else {
|
||||
fmt.Printf("discarding buffer, no pipeline with id %d", int(pipelineID))
|
||||
}
|
||||
C.free(buffer)
|
||||
}
|
19
internal/gst/gst.h
Normal file
19
internal/gst/gst.h
Normal file
@ -0,0 +1,19 @@
|
||||
#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_play_pipeline(GstElement *pipeline);
|
||||
void gstreamer_send_stop_pipeline(GstElement *pipeline);
|
||||
void gstreamer_send_start_mainloop(void);
|
||||
void gstreamer_init(void);
|
||||
|
||||
#endif
|
102
internal/http/endpoint/endpoint.go
Normal file
102
internal/http/endpoint/endpoint.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
17
internal/http/endpoint/error.go
Normal file
17
internal/http/endpoint/error.go
Normal 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
|
||||
}
|
87
internal/http/http.go
Normal file
87
internal/http/http.go
Normal file
@ -0,0 +1,87 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"n.eko.moe/neko/internal/http/endpoint"
|
||||
"n.eko.moe/neko/internal/http/middleware"
|
||||
"n.eko.moe/neko/internal/types"
|
||||
"n.eko.moe/neko/internal/types/config"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
logger zerolog.Logger
|
||||
router *chi.Mux
|
||||
http *http.Server
|
||||
conf *config.Server
|
||||
}
|
||||
|
||||
func New(conf *config.Server, webSocketHandler types.WebSocketHandler) *Server {
|
||||
logger := log.With().Str("module", "webrtc").Logger()
|
||||
|
||||
router := chi.NewRouter()
|
||||
// router.Use(middleware.Recoverer) // Recover from panics without crashing server
|
||||
router.Use(middleware.RequestID) // Create a request ID for each request
|
||||
router.Use(middleware.Logger) // Log API request calls
|
||||
|
||||
router.Get("/ws", func(w http.ResponseWriter, r *http.Request) {
|
||||
webSocketHandler.Upgrade(w, r)
|
||||
})
|
||||
|
||||
fs := http.FileServer(http.Dir(conf.Static))
|
||||
router.Get("/*", func(w http.ResponseWriter, r *http.Request) {
|
||||
if _, err := os.Stat(conf.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("file '%s' is not found", r.RequestURI),
|
||||
}
|
||||
}))
|
||||
|
||||
server := &http.Server{
|
||||
Addr: conf.Bind,
|
||||
Handler: router,
|
||||
}
|
||||
|
||||
return &Server{
|
||||
logger: logger,
|
||||
router: router,
|
||||
http: server,
|
||||
conf: conf,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) Start() {
|
||||
if s.conf.Cert != "" && s.conf.Key != "" {
|
||||
go func() {
|
||||
if err := s.http.ListenAndServeTLS(s.conf.Cert, s.conf.Key); err != http.ErrServerClosed {
|
||||
s.logger.Panic().Err(err).Msg("unable to start https server")
|
||||
}
|
||||
}()
|
||||
s.logger.Info().Msgf("https listening on %s", s.http.Addr)
|
||||
} else {
|
||||
go func() {
|
||||
if err := s.http.ListenAndServe(); err != http.ErrServerClosed {
|
||||
s.logger.Panic().Err(err).Msg("unable to start http server")
|
||||
}
|
||||
}()
|
||||
s.logger.Warn().Msgf("http listening on %s", s.http.Addr)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) Shutdown() error {
|
||||
return s.http.Shutdown(context.Background())
|
||||
}
|
80
internal/http/middleware/logger.go
Normal file
80
internal/http/middleware/logger.go
Normal 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"] = "http"
|
||||
|
||||
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)
|
||||
}
|
12
internal/http/middleware/middleware.go
Normal file
12
internal/http/middleware/middleware.go
Normal 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
|
||||
}
|
24
internal/http/middleware/recover.go
Normal file
24
internal/http/middleware/recover.go
Normal 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)
|
||||
}
|
89
internal/http/middleware/request.go
Normal file
89
internal/http/middleware/request.go
Normal 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)
|
||||
}
|
32
internal/http/response/response.go
Normal file
32
internal/http/response/response.go
Normal 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
|
||||
}
|
235
internal/remote/manager.go
Normal file
235
internal/remote/manager.go
Normal file
@ -0,0 +1,235 @@
|
||||
package remote
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/kataras/go-events"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"n.eko.moe/neko/internal/gst"
|
||||
"n.eko.moe/neko/internal/types"
|
||||
"n.eko.moe/neko/internal/types/config"
|
||||
"n.eko.moe/neko/internal/xorg"
|
||||
)
|
||||
|
||||
type RemoteManager struct {
|
||||
logger zerolog.Logger
|
||||
video *gst.Pipeline
|
||||
audio *gst.Pipeline
|
||||
config *config.Remote
|
||||
broadcast types.BroadcastManager
|
||||
cleanup *time.Ticker
|
||||
shutdown chan bool
|
||||
emmiter events.EventEmmiter
|
||||
streaming bool
|
||||
}
|
||||
|
||||
func New(config *config.Remote, broadcast types.BroadcastManager) *RemoteManager {
|
||||
return &RemoteManager{
|
||||
logger: log.With().Str("module", "remote").Logger(),
|
||||
cleanup: time.NewTicker(1 * time.Second),
|
||||
shutdown: make(chan bool),
|
||||
emmiter: events.New(),
|
||||
config: config,
|
||||
broadcast: broadcast,
|
||||
streaming: false,
|
||||
}
|
||||
}
|
||||
|
||||
func (manager *RemoteManager) VideoCodec() string {
|
||||
return manager.config.VideoCodec
|
||||
}
|
||||
|
||||
func (manager *RemoteManager) AudioCodec() string {
|
||||
return manager.config.AudioCodec
|
||||
}
|
||||
|
||||
func (manager *RemoteManager) Start() {
|
||||
xorg.Display(manager.config.Display)
|
||||
|
||||
if !xorg.ValidScreenSize(manager.config.ScreenWidth, manager.config.ScreenHeight, manager.config.ScreenRate) {
|
||||
manager.logger.Warn().Msgf("invalid screen option %dx%d@%d", manager.config.ScreenWidth, manager.config.ScreenHeight, manager.config.ScreenRate)
|
||||
} else if err := xorg.ChangeScreenSize(manager.config.ScreenWidth, manager.config.ScreenHeight, manager.config.ScreenRate); err != nil {
|
||||
manager.logger.Warn().Err(err).Msg("unable to change screen size")
|
||||
}
|
||||
|
||||
manager.createPipelines()
|
||||
manager.broadcast.Start()
|
||||
|
||||
go func() {
|
||||
defer func() {
|
||||
manager.logger.Info().Msg("shutdown")
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-manager.shutdown:
|
||||
return
|
||||
case sample := <-manager.video.Sample:
|
||||
manager.emmiter.Emit("video", sample)
|
||||
case sample := <-manager.audio.Sample:
|
||||
manager.emmiter.Emit("audio", sample)
|
||||
case <-manager.cleanup.C:
|
||||
xorg.CheckKeys(time.Second * 10)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (manager *RemoteManager) Shutdown() error {
|
||||
manager.logger.Info().Msgf("remote shutting down")
|
||||
manager.video.Stop()
|
||||
manager.audio.Stop()
|
||||
manager.broadcast.Stop()
|
||||
|
||||
manager.cleanup.Stop()
|
||||
manager.shutdown <- true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (manager *RemoteManager) OnVideoFrame(listener func(sample types.Sample)) {
|
||||
manager.emmiter.On("video", func(payload ...interface{}) {
|
||||
listener(payload[0].(types.Sample))
|
||||
})
|
||||
}
|
||||
|
||||
func (manager *RemoteManager) OnAudioFrame(listener func(sample types.Sample)) {
|
||||
manager.emmiter.On("audio", func(payload ...interface{}) {
|
||||
listener(payload[0].(types.Sample))
|
||||
})
|
||||
}
|
||||
|
||||
func (manager *RemoteManager) StartStream() {
|
||||
manager.createPipelines()
|
||||
|
||||
manager.logger.Info().
|
||||
Str("video_display", manager.config.Display).
|
||||
Str("video_codec", manager.config.VideoCodec).
|
||||
Str("audio_device", manager.config.Device).
|
||||
Str("audio_codec", manager.config.AudioCodec).
|
||||
Str("audio_pipeline_src", manager.audio.Src).
|
||||
Str("video_pipeline_src", manager.video.Src).
|
||||
Str("screen_resolution", fmt.Sprintf("%dx%d@%d", manager.config.ScreenWidth, manager.config.ScreenHeight, manager.config.ScreenRate)).
|
||||
Msgf("Pipelines starting...")
|
||||
|
||||
manager.video.Start()
|
||||
manager.audio.Start()
|
||||
manager.streaming = true
|
||||
}
|
||||
|
||||
func (manager *RemoteManager) StopStream() {
|
||||
manager.logger.Info().Msgf("Pipelines shutting down...")
|
||||
manager.video.Stop()
|
||||
manager.audio.Stop()
|
||||
manager.streaming = false
|
||||
}
|
||||
|
||||
func (manager *RemoteManager) Streaming() bool {
|
||||
return manager.streaming
|
||||
}
|
||||
|
||||
func (manager *RemoteManager) createPipelines() {
|
||||
var err error
|
||||
manager.video, err = gst.CreateAppPipeline(
|
||||
manager.config.VideoCodec,
|
||||
manager.config.Display,
|
||||
manager.config.VideoParams,
|
||||
)
|
||||
if err != nil {
|
||||
manager.logger.Panic().Err(err).Msg("unable to create video pipeline")
|
||||
}
|
||||
|
||||
manager.audio, err = gst.CreateAppPipeline(
|
||||
manager.config.AudioCodec,
|
||||
manager.config.Device,
|
||||
manager.config.AudioParams,
|
||||
)
|
||||
if err != nil {
|
||||
manager.logger.Panic().Err(err).Msg("unable to create audio pipeline")
|
||||
}
|
||||
}
|
||||
|
||||
func (manager *RemoteManager) ChangeResolution(width int, height int, rate int) error {
|
||||
if !xorg.ValidScreenSize(width, height, rate) {
|
||||
return fmt.Errorf("unknown configuration")
|
||||
}
|
||||
|
||||
manager.video.Stop()
|
||||
manager.broadcast.Stop()
|
||||
|
||||
defer func() {
|
||||
manager.video.Start()
|
||||
manager.broadcast.Start()
|
||||
|
||||
manager.logger.Info().Msg("starting video pipeline...")
|
||||
}()
|
||||
|
||||
if err := xorg.ChangeScreenSize(width, height, rate); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var err error
|
||||
manager.video, err = gst.CreateAppPipeline(
|
||||
manager.config.VideoCodec,
|
||||
manager.config.Display,
|
||||
manager.config.VideoParams,
|
||||
)
|
||||
if err != nil {
|
||||
manager.logger.Panic().Err(err).Msg("unable to create new video pipeline")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (manager *RemoteManager) Move(x, y int) {
|
||||
xorg.Move(x, y)
|
||||
}
|
||||
|
||||
func (manager *RemoteManager) Scroll(x, y int) {
|
||||
xorg.Scroll(x, y)
|
||||
}
|
||||
|
||||
func (manager *RemoteManager) ButtonDown(code int) error {
|
||||
return xorg.ButtonDown(code)
|
||||
}
|
||||
|
||||
func (manager *RemoteManager) KeyDown(code uint64) error {
|
||||
return xorg.KeyDown(code)
|
||||
}
|
||||
|
||||
func (manager *RemoteManager) ButtonUp(code int) error {
|
||||
return xorg.ButtonUp(code)
|
||||
}
|
||||
|
||||
func (manager *RemoteManager) KeyUp(code uint64) error {
|
||||
return xorg.KeyUp(code)
|
||||
}
|
||||
|
||||
func (manager *RemoteManager) ReadClipboard() string {
|
||||
return xorg.ReadClipboard()
|
||||
}
|
||||
|
||||
func (manager *RemoteManager) WriteClipboard(data string) {
|
||||
xorg.WriteClipboard(data)
|
||||
}
|
||||
|
||||
func (manager *RemoteManager) ResetKeys() {
|
||||
xorg.ResetKeys()
|
||||
}
|
||||
|
||||
func (manager *RemoteManager) ScreenConfigurations() map[int]types.ScreenConfiguration {
|
||||
return xorg.ScreenConfigurations
|
||||
}
|
||||
|
||||
func (manager *RemoteManager) GetScreenSize() *types.ScreenSize {
|
||||
return xorg.GetScreenSize()
|
||||
}
|
||||
|
||||
func (manager *RemoteManager) SetKeyboardLayout(layout string) {
|
||||
xorg.SetKeyboardLayout(layout)
|
||||
}
|
||||
|
||||
func (manager *RemoteManager) SetKeyboardModifiers(NumLock int, CapsLock int, ScrollLock int) {
|
||||
xorg.SetKeyboardModifiers(NumLock, CapsLock, ScrollLock)
|
||||
}
|
189
internal/session/manager.go
Normal file
189
internal/session/manager.go
Normal file
@ -0,0 +1,189 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/kataras/go-events"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"n.eko.moe/neko/internal/types"
|
||||
"n.eko.moe/neko/internal/utils"
|
||||
)
|
||||
|
||||
func New(remote types.RemoteManager) *SessionManager {
|
||||
return &SessionManager{
|
||||
logger: log.With().Str("module", "session").Logger(),
|
||||
host: "",
|
||||
remote: remote,
|
||||
members: make(map[string]*Session),
|
||||
emmiter: events.New(),
|
||||
}
|
||||
}
|
||||
|
||||
type SessionManager struct {
|
||||
logger zerolog.Logger
|
||||
host string
|
||||
remote types.RemoteManager
|
||||
members map[string]*Session
|
||||
emmiter events.EventEmmiter
|
||||
}
|
||||
|
||||
func (manager *SessionManager) New(id string, admin bool, socket types.WebSocket) types.Session {
|
||||
session := &Session{
|
||||
id: id,
|
||||
admin: admin,
|
||||
manager: manager,
|
||||
socket: socket,
|
||||
logger: manager.logger.With().Str("id", id).Logger(),
|
||||
connected: false,
|
||||
}
|
||||
|
||||
manager.members[id] = session
|
||||
manager.emmiter.Emit("created", id, session)
|
||||
|
||||
if manager.remote.Streaming() != true && len(manager.members) > 0 {
|
||||
manager.remote.StartStream()
|
||||
}
|
||||
|
||||
return session
|
||||
}
|
||||
|
||||
func (manager *SessionManager) HasHost() bool {
|
||||
return manager.host != ""
|
||||
}
|
||||
|
||||
func (manager *SessionManager) IsHost(id string) bool {
|
||||
return manager.host == id
|
||||
}
|
||||
|
||||
func (manager *SessionManager) SetHost(id string) error {
|
||||
_, ok := manager.members[id]
|
||||
if ok {
|
||||
manager.host = id
|
||||
manager.emmiter.Emit("host", id)
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("invalid session id %s", id)
|
||||
}
|
||||
|
||||
func (manager *SessionManager) GetHost() (types.Session, bool) {
|
||||
host, ok := manager.members[manager.host]
|
||||
return host, ok
|
||||
}
|
||||
|
||||
func (manager *SessionManager) ClearHost() {
|
||||
id := manager.host
|
||||
manager.host = ""
|
||||
manager.emmiter.Emit("host_cleared", id)
|
||||
}
|
||||
|
||||
func (manager *SessionManager) Has(id string) bool {
|
||||
_, ok := manager.members[id]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (manager *SessionManager) Get(id string) (types.Session, bool) {
|
||||
session, ok := manager.members[id]
|
||||
return session, ok
|
||||
}
|
||||
|
||||
func (manager *SessionManager) Admins() []*types.Member {
|
||||
members := []*types.Member{}
|
||||
for _, session := range manager.members {
|
||||
if !session.connected || !session.admin {
|
||||
continue
|
||||
}
|
||||
|
||||
member := session.Member()
|
||||
if member != nil {
|
||||
members = append(members, member)
|
||||
}
|
||||
}
|
||||
return members
|
||||
}
|
||||
|
||||
func (manager *SessionManager) Members() []*types.Member {
|
||||
members := []*types.Member{}
|
||||
for _, session := range manager.members {
|
||||
if !session.connected {
|
||||
continue
|
||||
}
|
||||
|
||||
member := session.Member()
|
||||
if member != nil {
|
||||
members = append(members, member)
|
||||
}
|
||||
}
|
||||
return members
|
||||
}
|
||||
|
||||
func (manager *SessionManager) Destroy(id string) error {
|
||||
session, ok := manager.members[id]
|
||||
if ok {
|
||||
err := session.destroy()
|
||||
delete(manager.members, id)
|
||||
|
||||
if manager.remote.Streaming() != false && len(manager.members) <= 0 {
|
||||
manager.remote.StopStream()
|
||||
}
|
||||
|
||||
manager.emmiter.Emit("destroyed", id, session)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (manager *SessionManager) Clear() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (manager *SessionManager) Broadcast(v interface{}, exclude interface{}) error {
|
||||
for id, session := range manager.members {
|
||||
if !session.connected {
|
||||
continue
|
||||
}
|
||||
|
||||
if exclude != nil {
|
||||
if in, _ := utils.ArrayIn(id, exclude); in {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if err := session.Send(v); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (manager *SessionManager) OnHost(listener func(id string)) {
|
||||
manager.emmiter.On("host", func(payload ...interface{}) {
|
||||
listener(payload[0].(string))
|
||||
})
|
||||
}
|
||||
|
||||
func (manager *SessionManager) OnHostCleared(listener func(id string)) {
|
||||
manager.emmiter.On("host_cleared", func(payload ...interface{}) {
|
||||
listener(payload[0].(string))
|
||||
})
|
||||
}
|
||||
|
||||
func (manager *SessionManager) OnDestroy(listener func(id string, session types.Session)) {
|
||||
manager.emmiter.On("destroyed", func(payload ...interface{}) {
|
||||
listener(payload[0].(string), payload[1].(*Session))
|
||||
})
|
||||
}
|
||||
|
||||
func (manager *SessionManager) OnCreated(listener func(id string, session types.Session)) {
|
||||
manager.emmiter.On("created", func(payload ...interface{}) {
|
||||
listener(payload[0].(string), payload[1].(*Session))
|
||||
})
|
||||
}
|
||||
|
||||
func (manager *SessionManager) OnConnected(listener func(id string, session types.Session)) {
|
||||
manager.emmiter.On("connected", func(payload ...interface{}) {
|
||||
listener(payload[0].(string), payload[1].(*Session))
|
||||
})
|
||||
}
|
137
internal/session/session.go
Normal file
137
internal/session/session.go
Normal file
@ -0,0 +1,137 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"n.eko.moe/neko/internal/types"
|
||||
"n.eko.moe/neko/internal/types/event"
|
||||
"n.eko.moe/neko/internal/types/message"
|
||||
)
|
||||
|
||||
type Session struct {
|
||||
logger zerolog.Logger
|
||||
id string
|
||||
name string
|
||||
admin bool
|
||||
muted bool
|
||||
connected bool
|
||||
manager *SessionManager
|
||||
socket types.WebSocket
|
||||
peer types.Peer
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func (session *Session) ID() string {
|
||||
return session.id
|
||||
}
|
||||
|
||||
func (session *Session) Name() string {
|
||||
return session.name
|
||||
}
|
||||
|
||||
func (session *Session) Admin() bool {
|
||||
return session.admin
|
||||
}
|
||||
|
||||
func (session *Session) Muted() bool {
|
||||
return session.muted
|
||||
}
|
||||
|
||||
func (session *Session) Connected() bool {
|
||||
return session.connected
|
||||
}
|
||||
|
||||
func (session *Session) Address() string {
|
||||
if session.socket == nil {
|
||||
return ""
|
||||
}
|
||||
return session.socket.Address()
|
||||
}
|
||||
|
||||
func (session *Session) Member() *types.Member {
|
||||
return &types.Member{
|
||||
ID: session.id,
|
||||
Name: session.name,
|
||||
Admin: session.admin,
|
||||
Muted: session.muted,
|
||||
}
|
||||
}
|
||||
|
||||
func (session *Session) SetMuted(muted bool) {
|
||||
session.muted = muted
|
||||
}
|
||||
|
||||
func (session *Session) SetName(name string) error {
|
||||
session.name = name
|
||||
return nil
|
||||
}
|
||||
|
||||
func (session *Session) SetSocket(socket types.WebSocket) error {
|
||||
session.socket = socket
|
||||
return nil
|
||||
}
|
||||
|
||||
func (session *Session) SetPeer(peer types.Peer) error {
|
||||
session.peer = peer
|
||||
return nil
|
||||
}
|
||||
|
||||
func (session *Session) SetConnected(connected bool) error {
|
||||
session.connected = connected
|
||||
if connected {
|
||||
session.manager.emmiter.Emit("connected", session.id, session)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (session *Session) Kick(reason string) error {
|
||||
if session.socket == nil {
|
||||
return nil
|
||||
}
|
||||
if err := session.socket.Send(&message.Disconnect{
|
||||
Event: event.SYSTEM_DISCONNECT,
|
||||
Message: reason,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return session.destroy()
|
||||
}
|
||||
|
||||
func (session *Session) Send(v interface{}) error {
|
||||
if session.socket == nil {
|
||||
return nil
|
||||
}
|
||||
return session.socket.Send(v)
|
||||
}
|
||||
|
||||
func (session *Session) Write(v interface{}) error {
|
||||
if session.socket == nil {
|
||||
return nil
|
||||
}
|
||||
return session.socket.Send(v)
|
||||
}
|
||||
|
||||
func (session *Session) SignalAnswer(sdp string) error {
|
||||
if session.peer == nil {
|
||||
return nil
|
||||
}
|
||||
return session.peer.SignalAnswer(sdp)
|
||||
}
|
||||
|
||||
func (session *Session) destroy() error {
|
||||
if session.socket != nil {
|
||||
if err := session.socket.Destroy(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if session.peer != nil {
|
||||
if err := session.peer.Destroy(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
10
internal/types/broadcast.go
Normal file
10
internal/types/broadcast.go
Normal file
@ -0,0 +1,10 @@
|
||||
package types
|
||||
|
||||
type BroadcastManager interface {
|
||||
Start()
|
||||
Stop()
|
||||
IsActive() bool
|
||||
Create(url string)
|
||||
Destroy()
|
||||
GetUrl() string
|
||||
}
|
23
internal/types/config/broadcast.go
Normal file
23
internal/types/config/broadcast.go
Normal file
@ -0,0 +1,23 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type Broadcast struct {
|
||||
Pipeline string
|
||||
}
|
||||
|
||||
func (Broadcast) Init(cmd *cobra.Command) error {
|
||||
cmd.PersistentFlags().String("broadcast_pipeline", "", "audio codec parameters to use for broadcasting")
|
||||
if err := viper.BindPFlag("broadcast_pipeline", cmd.PersistentFlags().Lookup("broadcast_pipeline")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Broadcast) Set() {
|
||||
s.Pipeline = viper.GetString("broadcast_pipeline")
|
||||
}
|
8
internal/types/config/config.go
Normal file
8
internal/types/config/config.go
Normal file
@ -0,0 +1,8 @@
|
||||
package config
|
||||
|
||||
import "github.com/spf13/cobra"
|
||||
|
||||
type Config interface {
|
||||
Init(cmd *cobra.Command) error
|
||||
Set()
|
||||
}
|
136
internal/types/config/remote.go
Normal file
136
internal/types/config/remote.go
Normal file
@ -0,0 +1,136 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
|
||||
"github.com/pion/webrtc/v2"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type Remote struct {
|
||||
Display string
|
||||
Device string
|
||||
AudioCodec string
|
||||
AudioParams string
|
||||
VideoCodec string
|
||||
VideoParams string
|
||||
ScreenWidth int
|
||||
ScreenHeight int
|
||||
ScreenRate int
|
||||
}
|
||||
|
||||
func (Remote) Init(cmd *cobra.Command) error {
|
||||
cmd.PersistentFlags().String("display", ":99.0", "XDisplay to capture")
|
||||
if err := viper.BindPFlag("display", cmd.PersistentFlags().Lookup("display")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("device", "auto_null.monitor", "audio device to capture")
|
||||
if err := viper.BindPFlag("device", cmd.PersistentFlags().Lookup("device")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("audio", "", "audio codec parameters to use for streaming")
|
||||
if err := viper.BindPFlag("audio", cmd.PersistentFlags().Lookup("audio")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("video", "", "video codec parameters to use for streaming")
|
||||
if err := viper.BindPFlag("video", cmd.PersistentFlags().Lookup("video")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("screen", "1280x720@30", "default screen resolution and framerate")
|
||||
if err := viper.BindPFlag("screen", cmd.PersistentFlags().Lookup("screen")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// video codecs
|
||||
cmd.PersistentFlags().Bool("vp8", false, "use VP8 video codec")
|
||||
if err := viper.BindPFlag("vp8", cmd.PersistentFlags().Lookup("vp8")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Bool("vp9", false, "use VP9 video codec")
|
||||
if err := viper.BindPFlag("vp9", cmd.PersistentFlags().Lookup("vp9")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Bool("h264", false, "use H264 video codec")
|
||||
if err := viper.BindPFlag("h264", cmd.PersistentFlags().Lookup("h264")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// audio codecs
|
||||
cmd.PersistentFlags().Bool("opus", false, "use Opus audio codec")
|
||||
if err := viper.BindPFlag("opus", cmd.PersistentFlags().Lookup("opus")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Bool("g722", false, "use G722 audio codec")
|
||||
if err := viper.BindPFlag("g722", cmd.PersistentFlags().Lookup("g722")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Bool("pcmu", false, "use PCMU audio codec")
|
||||
if err := viper.BindPFlag("pcmu", cmd.PersistentFlags().Lookup("pcmu")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Bool("pcma", false, "use PCMA audio codec")
|
||||
if err := viper.BindPFlag("pcma", cmd.PersistentFlags().Lookup("pcma")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Remote) Set() {
|
||||
videoCodec := webrtc.VP8
|
||||
if viper.GetBool("vp8") {
|
||||
videoCodec = webrtc.VP8
|
||||
} else if viper.GetBool("vp9") {
|
||||
videoCodec = webrtc.VP9
|
||||
} else if viper.GetBool("h264") {
|
||||
videoCodec = webrtc.H264
|
||||
}
|
||||
|
||||
audioCodec := webrtc.Opus
|
||||
if viper.GetBool("opus") {
|
||||
audioCodec = webrtc.Opus
|
||||
} else if viper.GetBool("g722") {
|
||||
audioCodec = webrtc.G722
|
||||
} else if viper.GetBool("pcmu") {
|
||||
audioCodec = webrtc.PCMU
|
||||
} else if viper.GetBool("pcma") {
|
||||
audioCodec = webrtc.PCMA
|
||||
}
|
||||
|
||||
s.Device = viper.GetString("device")
|
||||
s.AudioCodec = audioCodec
|
||||
s.AudioParams = viper.GetString("audio")
|
||||
s.Display = viper.GetString("display")
|
||||
s.VideoCodec = videoCodec
|
||||
s.VideoParams = viper.GetString("video")
|
||||
|
||||
s.ScreenWidth = 1280
|
||||
s.ScreenHeight = 720
|
||||
s.ScreenRate = 30
|
||||
|
||||
r := regexp.MustCompile(`([0-9]{1,4})x([0-9]{1,4})@([0-9]{1,3})`)
|
||||
res := r.FindStringSubmatch(viper.GetString("screen"))
|
||||
|
||||
if len(res) > 0 {
|
||||
width, err1 := strconv.ParseInt(res[1], 10, 64)
|
||||
height, err2 := strconv.ParseInt(res[2], 10, 64)
|
||||
rate, err3 := strconv.ParseInt(res[3], 10, 64)
|
||||
|
||||
if err1 == nil && err2 == nil && err3 == nil {
|
||||
s.ScreenWidth = int(width)
|
||||
s.ScreenHeight = int(height)
|
||||
s.ScreenRate = int(rate)
|
||||
}
|
||||
}
|
||||
}
|
37
internal/types/config/root.go
Normal file
37
internal/types/config/root.go
Normal 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")
|
||||
}
|
44
internal/types/config/server.go
Normal file
44
internal/types/config/server.go
Normal file
@ -0,0 +1,44 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
Cert string
|
||||
Key string
|
||||
Bind string
|
||||
Static string
|
||||
}
|
||||
|
||||
func (Server) 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("static", "./www", "path to neko client files to serve")
|
||||
if err := viper.BindPFlag("static", cmd.PersistentFlags().Lookup("static")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) Set() {
|
||||
s.Cert = viper.GetString("cert")
|
||||
s.Key = viper.GetString("key")
|
||||
s.Bind = viper.GetString("bind")
|
||||
s.Static = viper.GetString("static")
|
||||
}
|
79
internal/types/config/webrtc.go
Normal file
79
internal/types/config/webrtc.go
Normal file
@ -0,0 +1,79 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"n.eko.moe/neko/internal/utils"
|
||||
)
|
||||
|
||||
type WebRTC struct {
|
||||
ICELite bool
|
||||
ICEServers []string
|
||||
EphemeralMin uint16
|
||||
EphemeralMax uint16
|
||||
NAT1To1IPs []string
|
||||
}
|
||||
|
||||
func (WebRTC) Init(cmd *cobra.Command) error {
|
||||
cmd.PersistentFlags().String("epr", "59000-59100", "limits the pool of ephemeral ports that ICE UDP connections can allocate from")
|
||||
if err := viper.BindPFlag("epr", cmd.PersistentFlags().Lookup("epr")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().StringSlice("nat1to1", []string{}, "sets a list of external IP addresses of 1:1 (D)NAT and a candidate type for which the external IP address is used")
|
||||
if err := viper.BindPFlag("nat1to1", cmd.PersistentFlags().Lookup("nat1to1")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Bool("icelite", false, "configures whether or not the ice agent should be a lite agent")
|
||||
if err := viper.BindPFlag("icelite", cmd.PersistentFlags().Lookup("icelite")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().StringSlice("iceserver", []string{"stun:stun.l.google.com:19302"}, "describes a single STUN and TURN server that can be used by the ICEAgent to establish a connection with a peer")
|
||||
if err := viper.BindPFlag("iceserver", cmd.PersistentFlags().Lookup("iceserver")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *WebRTC) Set() {
|
||||
s.ICELite = viper.GetBool("icelite")
|
||||
s.ICEServers = viper.GetStringSlice("iceserver")
|
||||
s.NAT1To1IPs = viper.GetStringSlice("nat1to1")
|
||||
|
||||
if len(s.NAT1To1IPs) == 0 {
|
||||
ip, err := utils.GetIP()
|
||||
if err == nil {
|
||||
s.NAT1To1IPs = append(s.NAT1To1IPs, ip)
|
||||
}
|
||||
}
|
||||
|
||||
min := uint16(59000)
|
||||
max := uint16(59100)
|
||||
epr := viper.GetString("epr")
|
||||
ports := strings.SplitN(epr, "-", -1)
|
||||
if len(ports) > 1 {
|
||||
start, err := strconv.ParseUint(ports[0], 10, 16)
|
||||
if err == nil {
|
||||
min = uint16(start)
|
||||
}
|
||||
|
||||
end, err := strconv.ParseUint(ports[1], 10, 16)
|
||||
if err == nil {
|
||||
max = uint16(end)
|
||||
}
|
||||
}
|
||||
|
||||
if min > max {
|
||||
s.EphemeralMin = max
|
||||
s.EphemeralMax = min
|
||||
} else {
|
||||
s.EphemeralMin = min
|
||||
s.EphemeralMax = max
|
||||
}
|
||||
}
|
37
internal/types/config/websocket.go
Normal file
37
internal/types/config/websocket.go
Normal file
@ -0,0 +1,37 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type WebSocket struct {
|
||||
Password string
|
||||
AdminPassword string
|
||||
Proxy bool
|
||||
}
|
||||
|
||||
func (WebSocket) Init(cmd *cobra.Command) error {
|
||||
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("password_admin", "admin", "admin password for connecting to stream")
|
||||
if err := viper.BindPFlag("password_admin", cmd.PersistentFlags().Lookup("password_admin")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Bool("proxy", false, "enable reverse proxy mode")
|
||||
if err := viper.BindPFlag("proxy", cmd.PersistentFlags().Lookup("proxy")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *WebSocket) Set() {
|
||||
s.Password = viper.GetString("password")
|
||||
s.AdminPassword = viper.GetString("password_admin")
|
||||
s.Proxy = viper.GetBool("proxy")
|
||||
}
|
55
internal/types/event/events.go
Normal file
55
internal/types/event/events.go
Normal file
@ -0,0 +1,55 @@
|
||||
package event
|
||||
|
||||
const (
|
||||
SYSTEM_DISCONNECT = "system/disconnect"
|
||||
)
|
||||
|
||||
const (
|
||||
SIGNAL_ANSWER = "signal/answer"
|
||||
SIGNAL_PROVIDE = "signal/provide"
|
||||
)
|
||||
|
||||
const (
|
||||
MEMBER_LIST = "member/list"
|
||||
MEMBER_CONNECTED = "member/connected"
|
||||
MEMBER_DISCONNECTED = "member/disconnected"
|
||||
)
|
||||
|
||||
const (
|
||||
CONTROL_LOCKED = "control/locked"
|
||||
CONTROL_RELEASE = "control/release"
|
||||
CONTROL_REQUEST = "control/request"
|
||||
CONTROL_REQUESTING = "control/requesting"
|
||||
CONTROL_GIVE = "control/give"
|
||||
CONTROL_CLIPBOARD = "control/clipboard"
|
||||
CONTROL_KEYBOARD = "control/keyboard"
|
||||
)
|
||||
|
||||
const (
|
||||
CHAT_MESSAGE = "chat/message"
|
||||
CHAT_EMOTE = "chat/emote"
|
||||
)
|
||||
|
||||
const (
|
||||
SCREEN_CONFIGURATIONS = "screen/configurations"
|
||||
SCREEN_RESOLUTION = "screen/resolution"
|
||||
SCREEN_SET = "screen/set"
|
||||
)
|
||||
|
||||
const (
|
||||
BORADCAST_STATUS = "broadcast/status"
|
||||
BORADCAST_CREATE = "broadcast/create"
|
||||
BORADCAST_DESTROY = "broadcast/destroy"
|
||||
)
|
||||
|
||||
const (
|
||||
ADMIN_BAN = "admin/ban"
|
||||
ADMIN_KICK = "admin/kick"
|
||||
ADMIN_LOCK = "admin/lock"
|
||||
ADMIN_MUTE = "admin/mute"
|
||||
ADMIN_UNLOCK = "admin/unlock"
|
||||
ADMIN_UNMUTE = "admin/unmute"
|
||||
ADMIN_CONTROL = "admin/control"
|
||||
ADMIN_RELEASE = "admin/release"
|
||||
ADMIN_GIVE = "admin/give"
|
||||
)
|
14
internal/types/keys.go
Normal file
14
internal/types/keys.go
Normal file
@ -0,0 +1,14 @@
|
||||
package types
|
||||
|
||||
type Button struct {
|
||||
Name string
|
||||
Code int
|
||||
Keysym int
|
||||
}
|
||||
|
||||
type Key struct {
|
||||
Name string
|
||||
Value string
|
||||
Code int
|
||||
Keysym int
|
||||
}
|
123
internal/types/message/messages.go
Normal file
123
internal/types/message/messages.go
Normal file
@ -0,0 +1,123 @@
|
||||
package message
|
||||
|
||||
import (
|
||||
"n.eko.moe/neko/internal/types"
|
||||
)
|
||||
|
||||
type Message struct {
|
||||
Event string `json:"event"`
|
||||
}
|
||||
|
||||
type Disconnect struct {
|
||||
Event string `json:"event"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type SignalProvide struct {
|
||||
Event string `json:"event"`
|
||||
ID string `json:"id"`
|
||||
SDP string `json:"sdp"`
|
||||
Lite bool `json:"lite"`
|
||||
ICE []string `json:"ice"`
|
||||
}
|
||||
|
||||
type SignalAnswer struct {
|
||||
Event string `json:"event"`
|
||||
DisplayName string `json:"displayname"`
|
||||
SDP string `json:"sdp"`
|
||||
}
|
||||
|
||||
type MembersList struct {
|
||||
Event string `json:"event"`
|
||||
Memebers []*types.Member `json:"members"`
|
||||
}
|
||||
|
||||
type Member struct {
|
||||
Event string `json:"event"`
|
||||
*types.Member
|
||||
}
|
||||
type MemberDisconnected struct {
|
||||
Event string `json:"event"`
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
type Clipboard struct {
|
||||
Event string `json:"event"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
type Keyboard struct {
|
||||
Event string `json:"event"`
|
||||
Layout *string `json:"layout,omitempty"`
|
||||
CapsLock *bool `json:"capsLock,omitempty"`
|
||||
NumLock *bool `json:"numLock,omitempty"`
|
||||
ScrollLock *bool `json:"scrollLock,omitempty"`
|
||||
}
|
||||
|
||||
type Control struct {
|
||||
Event string `json:"event"`
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
type ControlTarget struct {
|
||||
Event string `json:"event"`
|
||||
ID string `json:"id"`
|
||||
Target string `json:"target"`
|
||||
}
|
||||
|
||||
type ChatReceive struct {
|
||||
Event string `json:"event"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
type ChatSend struct {
|
||||
Event string `json:"event"`
|
||||
ID string `json:"id"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
type EmoteReceive struct {
|
||||
Event string `json:"event"`
|
||||
Emote string `json:"emote"`
|
||||
}
|
||||
|
||||
type EmoteSend struct {
|
||||
Event string `json:"event"`
|
||||
ID string `json:"id"`
|
||||
Emote string `json:"emote"`
|
||||
}
|
||||
|
||||
type Admin struct {
|
||||
Event string `json:"event"`
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
type AdminTarget struct {
|
||||
Event string `json:"event"`
|
||||
Target string `json:"target"`
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
type ScreenResolution struct {
|
||||
Event string `json:"event"`
|
||||
ID string `json:"id,omitempty"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
Rate int `json:"rate"`
|
||||
}
|
||||
|
||||
type ScreenConfigurations struct {
|
||||
Event string `json:"event"`
|
||||
Configurations map[int]types.ScreenConfiguration `json:"configurations"`
|
||||
}
|
||||
|
||||
type BroadcastStatus struct {
|
||||
Event string `json:"event"`
|
||||
URL string `json:"url"`
|
||||
IsActive bool `json:"isActive"`
|
||||
}
|
||||
|
||||
type BroadcastCreate struct {
|
||||
Event string `json:"event"`
|
||||
URL string `json:"url"`
|
||||
}
|
27
internal/types/remote.go
Normal file
27
internal/types/remote.go
Normal file
@ -0,0 +1,27 @@
|
||||
package types
|
||||
|
||||
type RemoteManager interface {
|
||||
VideoCodec() string
|
||||
AudioCodec() string
|
||||
Start()
|
||||
Shutdown() error
|
||||
OnVideoFrame(listener func(sample Sample))
|
||||
OnAudioFrame(listener func(sample Sample))
|
||||
StartStream()
|
||||
StopStream()
|
||||
Streaming() bool
|
||||
ChangeResolution(width int, height int, rate int) error
|
||||
GetScreenSize() *ScreenSize
|
||||
ScreenConfigurations() map[int]ScreenConfiguration
|
||||
Move(x, y int)
|
||||
Scroll(x, y int)
|
||||
ButtonDown(code int) error
|
||||
KeyDown(code uint64) error
|
||||
ButtonUp(code int) error
|
||||
KeyUp(code uint64) error
|
||||
ReadClipboard() string
|
||||
WriteClipboard(data string)
|
||||
ResetKeys()
|
||||
SetKeyboardLayout(layout string)
|
||||
SetKeyboardModifiers(NumLock int, CapsLock int, ScrollLock int)
|
||||
}
|
48
internal/types/session.go
Normal file
48
internal/types/session.go
Normal file
@ -0,0 +1,48 @@
|
||||
package types
|
||||
|
||||
type Member struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"displayname"`
|
||||
Admin bool `json:"admin"`
|
||||
Muted bool `json:"muted"`
|
||||
}
|
||||
|
||||
type Session interface {
|
||||
ID() string
|
||||
Name() string
|
||||
Admin() bool
|
||||
Muted() bool
|
||||
Connected() bool
|
||||
Member() *Member
|
||||
SetMuted(muted bool)
|
||||
SetName(name string) error
|
||||
SetConnected(connected bool) error
|
||||
SetSocket(socket WebSocket) error
|
||||
SetPeer(peer Peer) error
|
||||
Address() string
|
||||
Kick(message string) error
|
||||
Write(v interface{}) error
|
||||
Send(v interface{}) error
|
||||
SignalAnswer(sdp string) error
|
||||
}
|
||||
|
||||
type SessionManager interface {
|
||||
New(id string, admin bool, socket WebSocket) Session
|
||||
HasHost() bool
|
||||
IsHost(id string) bool
|
||||
SetHost(id string) error
|
||||
GetHost() (Session, bool)
|
||||
ClearHost()
|
||||
Has(id string) bool
|
||||
Get(id string) (Session, bool)
|
||||
Members() []*Member
|
||||
Admins() []*Member
|
||||
Destroy(id string) error
|
||||
Clear() error
|
||||
Broadcast(v interface{}, exclude interface{}) error
|
||||
OnHost(listener func(id string))
|
||||
OnHostCleared(listener func(id string))
|
||||
OnDestroy(listener func(id string, session Session))
|
||||
OnCreated(listener func(id string, session Session))
|
||||
OnConnected(listener func(id string, session Session))
|
||||
}
|
18
internal/types/webrtc.go
Normal file
18
internal/types/webrtc.go
Normal file
@ -0,0 +1,18 @@
|
||||
package types
|
||||
|
||||
type Sample struct {
|
||||
Data []byte
|
||||
Samples uint32
|
||||
}
|
||||
|
||||
type WebRTCManager interface {
|
||||
Start()
|
||||
Shutdown() error
|
||||
CreatePeer(id string, session Session) (string, bool, []string, error)
|
||||
}
|
||||
|
||||
type Peer interface {
|
||||
SignalAnswer(sdp string) error
|
||||
WriteData(v interface{}) error
|
||||
Destroy() error
|
||||
}
|
15
internal/types/webscoket.go
Normal file
15
internal/types/webscoket.go
Normal file
@ -0,0 +1,15 @@
|
||||
package types
|
||||
|
||||
import "net/http"
|
||||
|
||||
type WebSocket interface {
|
||||
Address() string
|
||||
Send(v interface{}) error
|
||||
Destroy() error
|
||||
}
|
||||
|
||||
type WebSocketHandler interface {
|
||||
Start() error
|
||||
Shutdown() error
|
||||
Upgrade(w http.ResponseWriter, r *http.Request) error
|
||||
}
|
13
internal/types/xorg.go
Normal file
13
internal/types/xorg.go
Normal file
@ -0,0 +1,13 @@
|
||||
package types
|
||||
|
||||
type ScreenSize struct {
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
Rate int16 `json:"rate"`
|
||||
}
|
||||
|
||||
type ScreenConfiguration struct {
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
Rates map[int]int16 `json:"rates"`
|
||||
}
|
24
internal/utils/array.go
Normal file
24
internal/utils/array.go
Normal file
@ -0,0 +1,24 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
)
|
||||
|
||||
func ArrayIn(val interface{}, array interface{}) (exists bool, index int) {
|
||||
exists = false
|
||||
index = -1
|
||||
|
||||
switch reflect.TypeOf(array).Kind() {
|
||||
case reflect.Slice:
|
||||
s := reflect.ValueOf(array)
|
||||
for i := 0; i < s.Len(); i++ {
|
||||
if reflect.DeepEqual(val, s.Index(i).Interface()) == true {
|
||||
index = i
|
||||
exists = true
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
34
internal/utils/color.go
Normal file
34
internal/utils/color.go
Normal 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...)
|
||||
}
|
35
internal/utils/ip.go
Normal file
35
internal/utils/ip.go
Normal file
@ -0,0 +1,35 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// dig @resolver1.opendns.com ANY myip.opendns.com +short -4
|
||||
|
||||
func GetIP() (string, error) {
|
||||
rsp, err := http.Get("http://checkip.amazonaws.com")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer rsp.Body.Close()
|
||||
|
||||
buf, err := ioutil.ReadAll(rsp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(bytes.TrimSpace(buf)), nil
|
||||
}
|
||||
|
||||
func ReadUserIP(r *http.Request) string {
|
||||
IPAddress := r.Header.Get("X-Real-Ip")
|
||||
if IPAddress == "" {
|
||||
IPAddress = r.Header.Get("X-Forwarded-For")
|
||||
}
|
||||
if IPAddress == "" {
|
||||
IPAddress = r.RemoteAddr
|
||||
}
|
||||
return IPAddress
|
||||
}
|
10
internal/utils/json.go
Normal file
10
internal/utils/json.go
Normal file
@ -0,0 +1,10 @@
|
||||
package utils
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
func Unmarshal(in interface{}, raw []byte, callback func() error) error {
|
||||
if err := json.Unmarshal(raw, &in); err != nil {
|
||||
return err
|
||||
}
|
||||
return callback()
|
||||
}
|
98
internal/utils/uid.go
Normal file
98
internal/utils/uid.go
Normal 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
|
||||
}
|
137
internal/webrtc/handle.go
Normal file
137
internal/webrtc/handle.go
Normal file
@ -0,0 +1,137 @@
|
||||
package webrtc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"strconv"
|
||||
|
||||
"github.com/pion/webrtc/v2"
|
||||
)
|
||||
|
||||
const OP_MOVE = 0x01
|
||||
const OP_SCROLL = 0x02
|
||||
const OP_KEY_DOWN = 0x03
|
||||
const OP_KEY_UP = 0x04
|
||||
const OP_KEY_CLK = 0x05
|
||||
|
||||
type PayloadHeader struct {
|
||||
Event uint8
|
||||
Length uint16
|
||||
}
|
||||
|
||||
type PayloadMove struct {
|
||||
PayloadHeader
|
||||
X uint16
|
||||
Y uint16
|
||||
}
|
||||
|
||||
type PayloadScroll struct {
|
||||
PayloadHeader
|
||||
X int16
|
||||
Y int16
|
||||
}
|
||||
|
||||
type PayloadKey struct {
|
||||
PayloadHeader
|
||||
Key uint64
|
||||
}
|
||||
|
||||
func (manager *WebRTCManager) handle(id string, msg webrtc.DataChannelMessage) error {
|
||||
if !manager.sessions.IsHost(id) {
|
||||
return nil
|
||||
}
|
||||
|
||||
buffer := bytes.NewBuffer(msg.Data)
|
||||
header := &PayloadHeader{}
|
||||
hbytes := make([]byte, 3)
|
||||
|
||||
if _, err := buffer.Read(hbytes); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := binary.Read(bytes.NewBuffer(hbytes), binary.LittleEndian, header); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
buffer = bytes.NewBuffer(msg.Data)
|
||||
|
||||
switch header.Event {
|
||||
case OP_MOVE:
|
||||
payload := &PayloadMove{}
|
||||
if err := binary.Read(buffer, binary.LittleEndian, payload); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
manager.remote.Move(int(payload.X), int(payload.Y))
|
||||
break
|
||||
case OP_SCROLL:
|
||||
payload := &PayloadScroll{}
|
||||
if err := binary.Read(buffer, binary.LittleEndian, payload); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
manager.logger.
|
||||
Debug().
|
||||
Str("x", strconv.Itoa(int(payload.X))).
|
||||
Str("y", strconv.Itoa(int(payload.Y))).
|
||||
Msg("scroll")
|
||||
|
||||
manager.remote.Scroll(int(payload.X), int(payload.Y))
|
||||
break
|
||||
case OP_KEY_DOWN:
|
||||
payload := &PayloadKey{}
|
||||
if err := binary.Read(buffer, binary.LittleEndian, payload); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if payload.Key < 8 {
|
||||
err := manager.remote.ButtonDown(int(payload.Key))
|
||||
if err != nil {
|
||||
manager.logger.Warn().Err(err).Msg("button down failed")
|
||||
return nil
|
||||
}
|
||||
|
||||
manager.logger.Debug().Msgf("button down %d", payload.Key)
|
||||
} else {
|
||||
err := manager.remote.KeyDown(uint64(payload.Key))
|
||||
if err != nil {
|
||||
manager.logger.Warn().Err(err).Msg("key down failed")
|
||||
return nil
|
||||
}
|
||||
|
||||
manager.logger.Debug().Msgf("key down %d", payload.Key)
|
||||
}
|
||||
|
||||
break
|
||||
case OP_KEY_UP:
|
||||
payload := &PayloadKey{}
|
||||
err := binary.Read(buffer, binary.LittleEndian, payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if payload.Key < 8 {
|
||||
err := manager.remote.ButtonUp(int(payload.Key))
|
||||
if err != nil {
|
||||
manager.logger.Warn().Err(err).Msg("button up failed")
|
||||
return nil
|
||||
}
|
||||
|
||||
manager.logger.Debug().Msgf("button up %d", payload.Key)
|
||||
} else {
|
||||
err := manager.remote.KeyUp(uint64(payload.Key))
|
||||
if err != nil {
|
||||
manager.logger.Warn().Err(err).Msg("key up failed")
|
||||
return nil
|
||||
}
|
||||
|
||||
manager.logger.Debug().Msgf("key up %d", payload.Key)
|
||||
}
|
||||
break
|
||||
case OP_KEY_CLK:
|
||||
// unused
|
||||
break
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
66
internal/webrtc/logger.go
Normal file
66
internal/webrtc/logger.go
Normal file
@ -0,0 +1,66 @@
|
||||
package webrtc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/pion/logging"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
type nulllog struct{}
|
||||
|
||||
func (l nulllog) Trace(msg string) {}
|
||||
func (l nulllog) Tracef(format string, args ...interface{}) {}
|
||||
func (l nulllog) Debug(msg string) {}
|
||||
func (l nulllog) Debugf(format string, args ...interface{}) {}
|
||||
func (l nulllog) Info(msg string) {}
|
||||
func (l nulllog) Infof(format string, args ...interface{}) {}
|
||||
func (l nulllog) Warn(msg string) {}
|
||||
func (l nulllog) Warnf(format string, args ...interface{}) {}
|
||||
func (l nulllog) Error(msg string) {}
|
||||
func (l nulllog) Errorf(format string, args ...interface{}) {}
|
||||
|
||||
type logger struct {
|
||||
logger zerolog.Logger
|
||||
subsystem string
|
||||
}
|
||||
|
||||
func (l logger) Trace(msg string) { l.logger.Trace().Msg(msg) }
|
||||
func (l logger) Tracef(format string, args ...interface{}) { l.logger.Trace().Msgf(format, args...) }
|
||||
func (l logger) Debug(msg string) { l.logger.Debug().Msg(msg) }
|
||||
func (l logger) Debugf(format string, args ...interface{}) { l.logger.Debug().Msgf(format, args...) }
|
||||
func (l logger) Info(msg string) {
|
||||
if strings.Contains(msg, "packetio.Buffer is full") {
|
||||
//l.logger.Panic().Msg(msg)
|
||||
return
|
||||
}
|
||||
l.logger.Info().Msg(msg)
|
||||
}
|
||||
func (l logger) Infof(format string, args ...interface{}) {
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
if strings.Contains(msg, "packetio.Buffer is full") {
|
||||
// l.logger.Panic().Msg(msg)
|
||||
return
|
||||
}
|
||||
l.logger.Info().Msg(msg)
|
||||
}
|
||||
func (l logger) Warn(msg string) { l.logger.Warn().Msg(msg) }
|
||||
func (l logger) Warnf(format string, args ...interface{}) { l.logger.Warn().Msgf(format, args...) }
|
||||
func (l logger) Error(msg string) { l.logger.Error().Msg(msg) }
|
||||
func (l logger) Errorf(format string, args ...interface{}) { l.logger.Error().Msgf(format, args...) }
|
||||
|
||||
type loggerFactory struct {
|
||||
logger zerolog.Logger
|
||||
}
|
||||
|
||||
func (l loggerFactory) NewLogger(subsystem string) logging.LeveledLogger {
|
||||
if subsystem == "sctp" {
|
||||
return nulllog{}
|
||||
}
|
||||
|
||||
return logger{
|
||||
subsystem: subsystem,
|
||||
logger: l.logger.With().Str("subsystem", subsystem).Logger(),
|
||||
}
|
||||
}
|
38
internal/webrtc/peer.go
Normal file
38
internal/webrtc/peer.go
Normal file
@ -0,0 +1,38 @@
|
||||
package webrtc
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/pion/webrtc/v2"
|
||||
)
|
||||
|
||||
type Peer struct {
|
||||
id string
|
||||
api *webrtc.API
|
||||
engine *webrtc.MediaEngine
|
||||
manager *WebRTCManager
|
||||
settings *webrtc.SettingEngine
|
||||
connection *webrtc.PeerConnection
|
||||
configuration *webrtc.Configuration
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func (peer *Peer) SignalAnswer(sdp string) error {
|
||||
return peer.connection.SetRemoteDescription(webrtc.SessionDescription{SDP: sdp, Type: webrtc.SDPTypeAnswer})
|
||||
}
|
||||
|
||||
func (peer *Peer) WriteData(v interface{}) error {
|
||||
peer.mu.Lock()
|
||||
defer peer.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (peer *Peer) Destroy() error {
|
||||
if peer.connection != nil && peer.connection.ConnectionState() == webrtc.PeerConnectionStateConnected {
|
||||
if err := peer.connection.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
201
internal/webrtc/webrtc.go
Normal file
201
internal/webrtc/webrtc.go
Normal file
@ -0,0 +1,201 @@
|
||||
package webrtc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"strings"
|
||||
|
||||
"github.com/pion/webrtc/v2"
|
||||
"github.com/pion/webrtc/v2/pkg/media"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"n.eko.moe/neko/internal/types"
|
||||
"n.eko.moe/neko/internal/types/config"
|
||||
)
|
||||
|
||||
func New(sessions types.SessionManager, remote types.RemoteManager, config *config.WebRTC) *WebRTCManager {
|
||||
return &WebRTCManager{
|
||||
logger: log.With().Str("module", "webrtc").Logger(),
|
||||
remote: remote,
|
||||
sessions: sessions,
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
type WebRTCManager struct {
|
||||
logger zerolog.Logger
|
||||
videoTrack *webrtc.Track
|
||||
audioTrack *webrtc.Track
|
||||
videoCodec *webrtc.RTPCodec
|
||||
audioCodec *webrtc.RTPCodec
|
||||
sessions types.SessionManager
|
||||
remote types.RemoteManager
|
||||
config *config.WebRTC
|
||||
}
|
||||
|
||||
func (manager *WebRTCManager) Start() {
|
||||
var err error
|
||||
manager.audioTrack, manager.audioCodec, err = manager.createTrack(manager.remote.AudioCodec())
|
||||
if err != nil {
|
||||
manager.logger.Panic().Err(err).Msg("unable to create audio track")
|
||||
}
|
||||
|
||||
manager.remote.OnAudioFrame(func(sample types.Sample) {
|
||||
if err := manager.audioTrack.WriteSample(media.Sample(sample)); err != nil && err != io.ErrClosedPipe {
|
||||
manager.logger.Warn().Err(err).Msg("audio pipeline failed to write")
|
||||
}
|
||||
})
|
||||
|
||||
manager.videoTrack, manager.videoCodec, err = manager.createTrack(manager.remote.VideoCodec())
|
||||
if err != nil {
|
||||
manager.logger.Panic().Err(err).Msg("unable to create video track")
|
||||
}
|
||||
|
||||
manager.remote.OnVideoFrame(func(sample types.Sample) {
|
||||
if err := manager.videoTrack.WriteSample(media.Sample(sample)); err != nil && err != io.ErrClosedPipe {
|
||||
manager.logger.Warn().Err(err).Msg("video pipeline failed to write")
|
||||
}
|
||||
})
|
||||
|
||||
manager.logger.Info().
|
||||
Str("ice_lite", fmt.Sprintf("%t", manager.config.ICELite)).
|
||||
Str("ice_servers", strings.Join(manager.config.ICEServers, ",")).
|
||||
Str("ephemeral_port_range", fmt.Sprintf("%d-%d", manager.config.EphemeralMin, manager.config.EphemeralMax)).
|
||||
Str("nat_ips", strings.Join(manager.config.NAT1To1IPs, ",")).
|
||||
Msgf("webrtc starting")
|
||||
}
|
||||
|
||||
func (manager *WebRTCManager) Shutdown() error {
|
||||
manager.logger.Info().Msgf("webrtc shutting down")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (manager *WebRTCManager) CreatePeer(id string, session types.Session) (string, bool, []string, error) {
|
||||
configuration := &webrtc.Configuration{
|
||||
ICEServers: []webrtc.ICEServer{
|
||||
{
|
||||
URLs: manager.config.ICEServers,
|
||||
},
|
||||
},
|
||||
SDPSemantics: webrtc.SDPSemanticsUnifiedPlanWithFallback,
|
||||
}
|
||||
|
||||
settings := webrtc.SettingEngine{
|
||||
LoggerFactory: loggerFactory{
|
||||
logger: manager.logger,
|
||||
},
|
||||
}
|
||||
|
||||
if manager.config.ICELite {
|
||||
configuration = &webrtc.Configuration{
|
||||
SDPSemantics: webrtc.SDPSemanticsUnifiedPlanWithFallback,
|
||||
}
|
||||
settings.SetLite(true)
|
||||
}
|
||||
|
||||
settings.SetEphemeralUDPPortRange(manager.config.EphemeralMin, manager.config.EphemeralMax)
|
||||
settings.SetNAT1To1IPs(manager.config.NAT1To1IPs, webrtc.ICECandidateTypeHost)
|
||||
|
||||
// Create MediaEngine based off sdp
|
||||
engine := webrtc.MediaEngine{}
|
||||
|
||||
engine.RegisterCodec(manager.audioCodec)
|
||||
engine.RegisterCodec(manager.videoCodec)
|
||||
|
||||
// Create API with MediaEngine and SettingEngine
|
||||
api := webrtc.NewAPI(webrtc.WithMediaEngine(engine), webrtc.WithSettingEngine(settings))
|
||||
|
||||
// Create new peer connection
|
||||
connection, err := api.NewPeerConnection(*configuration)
|
||||
if err != nil {
|
||||
return "", manager.config.ICELite, manager.config.ICEServers, err
|
||||
}
|
||||
|
||||
if _, err = connection.AddTransceiverFromTrack(manager.videoTrack, webrtc.RtpTransceiverInit{
|
||||
Direction: webrtc.RTPTransceiverDirectionSendonly,
|
||||
}); err != nil {
|
||||
return "", manager.config.ICELite, manager.config.ICEServers, err
|
||||
}
|
||||
|
||||
if _, err = connection.AddTransceiverFromTrack(manager.audioTrack, webrtc.RtpTransceiverInit{
|
||||
Direction: webrtc.RTPTransceiverDirectionSendonly,
|
||||
}); err != nil {
|
||||
return "", manager.config.ICELite, manager.config.ICEServers, err
|
||||
}
|
||||
|
||||
description, err := connection.CreateOffer(nil)
|
||||
if err != nil {
|
||||
return "", manager.config.ICELite, manager.config.ICEServers, err
|
||||
}
|
||||
|
||||
connection.OnDataChannel(func(d *webrtc.DataChannel) {
|
||||
d.OnMessage(func(msg webrtc.DataChannelMessage) {
|
||||
if err = manager.handle(id, msg); err != nil {
|
||||
manager.logger.Warn().Err(err).Msg("data handle failed")
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
connection.SetLocalDescription(description)
|
||||
connection.OnConnectionStateChange(func(state webrtc.PeerConnectionState) {
|
||||
switch state {
|
||||
case webrtc.PeerConnectionStateDisconnected:
|
||||
case webrtc.PeerConnectionStateFailed:
|
||||
manager.logger.Info().Str("id", id).Msg("peer disconnected")
|
||||
manager.sessions.Destroy(id)
|
||||
break
|
||||
case webrtc.PeerConnectionStateConnected:
|
||||
manager.logger.Info().Str("id", id).Msg("peer connected")
|
||||
if err = session.SetConnected(true); err != nil {
|
||||
manager.logger.Warn().Err(err).Msg("unable to set connected on peer")
|
||||
manager.sessions.Destroy(id)
|
||||
}
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
if err := session.SetPeer(&Peer{
|
||||
id: id,
|
||||
api: api,
|
||||
engine: &engine,
|
||||
manager: manager,
|
||||
settings: &settings,
|
||||
connection: connection,
|
||||
configuration: configuration,
|
||||
}); err != nil {
|
||||
return "", manager.config.ICELite, manager.config.ICEServers, err
|
||||
}
|
||||
|
||||
return description.SDP, manager.config.ICELite, manager.config.ICEServers, nil
|
||||
}
|
||||
|
||||
func (m *WebRTCManager) createTrack(codecName string) (*webrtc.Track, *webrtc.RTPCodec, error) {
|
||||
var codec *webrtc.RTPCodec
|
||||
switch codecName {
|
||||
case webrtc.VP8:
|
||||
codec = webrtc.NewRTPVP8Codec(webrtc.DefaultPayloadTypeVP8, 90000)
|
||||
case webrtc.VP9:
|
||||
codec = webrtc.NewRTPVP9Codec(webrtc.DefaultPayloadTypeVP9, 90000)
|
||||
case webrtc.H264:
|
||||
codec = webrtc.NewRTPH264Codec(webrtc.DefaultPayloadTypeH264, 90000)
|
||||
case webrtc.Opus:
|
||||
codec = webrtc.NewRTPOpusCodec(webrtc.DefaultPayloadTypeOpus, 48000)
|
||||
case webrtc.G722:
|
||||
codec = webrtc.NewRTPG722Codec(webrtc.DefaultPayloadTypeG722, 8000)
|
||||
case webrtc.PCMU:
|
||||
codec = webrtc.NewRTPPCMUCodec(webrtc.DefaultPayloadTypePCMU, 8000)
|
||||
case webrtc.PCMA:
|
||||
codec = webrtc.NewRTPPCMACodec(webrtc.DefaultPayloadTypePCMA, 8000)
|
||||
default:
|
||||
return nil, nil, fmt.Errorf("unknown codec %s", codecName)
|
||||
}
|
||||
|
||||
track, err := webrtc.NewTrack(codec.PayloadType, rand.Uint32(), "stream", "stream", codec)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return track, codec, nil
|
||||
}
|
298
internal/websocket/admin.go
Normal file
298
internal/websocket/admin.go
Normal file
@ -0,0 +1,298 @@
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"n.eko.moe/neko/internal/types"
|
||||
"n.eko.moe/neko/internal/types/event"
|
||||
"n.eko.moe/neko/internal/types/message"
|
||||
)
|
||||
|
||||
func (h *MessageHandler) adminLock(id string, session types.Session) error {
|
||||
if !session.Admin() {
|
||||
h.logger.Debug().Msg("user not admin")
|
||||
return nil
|
||||
}
|
||||
|
||||
if h.locked {
|
||||
h.logger.Debug().Msg("server already locked...")
|
||||
return nil
|
||||
}
|
||||
|
||||
h.locked = true
|
||||
|
||||
if err := h.sessions.Broadcast(
|
||||
message.Admin{
|
||||
Event: event.ADMIN_LOCK,
|
||||
ID: id,
|
||||
}, nil); err != nil {
|
||||
h.logger.Warn().Err(err).Msgf("broadcasting event %s has failed", event.ADMIN_LOCK)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *MessageHandler) adminUnlock(id string, session types.Session) error {
|
||||
if !session.Admin() {
|
||||
h.logger.Debug().Msg("user not admin")
|
||||
return nil
|
||||
}
|
||||
|
||||
if !h.locked {
|
||||
h.logger.Debug().Msg("server not locked...")
|
||||
return nil
|
||||
}
|
||||
|
||||
h.locked = false
|
||||
|
||||
if err := h.sessions.Broadcast(
|
||||
message.Admin{
|
||||
Event: event.ADMIN_UNLOCK,
|
||||
ID: id,
|
||||
}, nil); err != nil {
|
||||
h.logger.Warn().Err(err).Msgf("broadcasting event %s has failed", event.ADMIN_UNLOCK)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *MessageHandler) adminControl(id string, session types.Session) error {
|
||||
if !session.Admin() {
|
||||
h.logger.Debug().Msg("user not admin")
|
||||
return nil
|
||||
}
|
||||
|
||||
host, ok := h.sessions.GetHost()
|
||||
|
||||
h.sessions.SetHost(id)
|
||||
|
||||
if ok {
|
||||
if err := h.sessions.Broadcast(
|
||||
message.AdminTarget{
|
||||
Event: event.ADMIN_CONTROL,
|
||||
ID: id,
|
||||
Target: host.ID(),
|
||||
}, nil); err != nil {
|
||||
h.logger.Warn().Err(err).Msgf("broadcasting event %s has failed", event.ADMIN_CONTROL)
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := h.sessions.Broadcast(
|
||||
message.Admin{
|
||||
Event: event.ADMIN_CONTROL,
|
||||
ID: id,
|
||||
}, nil); err != nil {
|
||||
h.logger.Warn().Err(err).Msgf("broadcasting event %s has failed", event.ADMIN_CONTROL)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *MessageHandler) adminRelease(id string, session types.Session) error {
|
||||
if !session.Admin() {
|
||||
h.logger.Debug().Msg("user not admin")
|
||||
return nil
|
||||
}
|
||||
|
||||
host, ok := h.sessions.GetHost()
|
||||
|
||||
h.sessions.ClearHost()
|
||||
|
||||
if ok {
|
||||
if err := h.sessions.Broadcast(
|
||||
message.AdminTarget{
|
||||
Event: event.ADMIN_RELEASE,
|
||||
ID: id,
|
||||
Target: host.ID(),
|
||||
}, nil); err != nil {
|
||||
h.logger.Warn().Err(err).Msgf("broadcasting event %s has failed", event.ADMIN_RELEASE)
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := h.sessions.Broadcast(
|
||||
message.Admin{
|
||||
Event: event.ADMIN_RELEASE,
|
||||
ID: id,
|
||||
}, nil); err != nil {
|
||||
h.logger.Warn().Err(err).Msgf("broadcasting event %s has failed", event.ADMIN_RELEASE)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *MessageHandler) adminGive(id string, session types.Session, payload *message.Admin) error {
|
||||
if !session.Admin() {
|
||||
h.logger.Debug().Msg("user not admin")
|
||||
return nil
|
||||
}
|
||||
|
||||
if !h.sessions.Has(payload.ID) {
|
||||
h.logger.Debug().Str("id", payload.ID).Msg("user does not exist")
|
||||
return nil
|
||||
}
|
||||
|
||||
// set host
|
||||
h.sessions.SetHost(payload.ID)
|
||||
|
||||
// let everyone know
|
||||
if err := h.sessions.Broadcast(
|
||||
message.AdminTarget{
|
||||
Event: event.CONTROL_GIVE,
|
||||
ID: id,
|
||||
Target: payload.ID,
|
||||
}, nil); err != nil {
|
||||
h.logger.Warn().Err(err).Msgf("broadcasting event %s has failed", event.CONTROL_LOCKED)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *MessageHandler) adminMute(id string, session types.Session, payload *message.Admin) error {
|
||||
if !session.Admin() {
|
||||
h.logger.Debug().Msg("user not admin")
|
||||
return nil
|
||||
}
|
||||
|
||||
target, ok := h.sessions.Get(payload.ID)
|
||||
if !ok {
|
||||
h.logger.Debug().Str("id", payload.ID).Msg("can't find session id")
|
||||
return nil
|
||||
}
|
||||
|
||||
if target.Admin() {
|
||||
h.logger.Debug().Msg("target is an admin, baling")
|
||||
return nil
|
||||
}
|
||||
|
||||
target.SetMuted(true)
|
||||
|
||||
if err := h.sessions.Broadcast(
|
||||
message.AdminTarget{
|
||||
Event: event.ADMIN_MUTE,
|
||||
Target: target.ID(),
|
||||
ID: id,
|
||||
}, nil); err != nil {
|
||||
h.logger.Warn().Err(err).Msgf("broadcasting event %s has failed", event.ADMIN_UNMUTE)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *MessageHandler) adminUnmute(id string, session types.Session, payload *message.Admin) error {
|
||||
if !session.Admin() {
|
||||
h.logger.Debug().Msg("user not admin")
|
||||
return nil
|
||||
}
|
||||
|
||||
target, ok := h.sessions.Get(payload.ID)
|
||||
if !ok {
|
||||
h.logger.Debug().Str("id", payload.ID).Msg("can't find target session")
|
||||
return nil
|
||||
}
|
||||
|
||||
target.SetMuted(false)
|
||||
|
||||
if err := h.sessions.Broadcast(
|
||||
message.AdminTarget{
|
||||
Event: event.ADMIN_UNMUTE,
|
||||
Target: target.ID(),
|
||||
ID: id,
|
||||
}, nil); err != nil {
|
||||
h.logger.Warn().Err(err).Msgf("broadcasting event %s has failed", event.ADMIN_UNMUTE)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *MessageHandler) adminKick(id string, session types.Session, payload *message.Admin) error {
|
||||
if !session.Admin() {
|
||||
h.logger.Debug().Msg("user not admin")
|
||||
return nil
|
||||
}
|
||||
|
||||
target, ok := h.sessions.Get(payload.ID)
|
||||
if !ok {
|
||||
h.logger.Debug().Str("id", payload.ID).Msg("can't find session id")
|
||||
return nil
|
||||
}
|
||||
|
||||
if target.Admin() {
|
||||
h.logger.Debug().Msg("target is an admin, baling")
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := target.Kick("kicked"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := h.sessions.Broadcast(
|
||||
message.AdminTarget{
|
||||
Event: event.ADMIN_KICK,
|
||||
Target: target.ID(),
|
||||
ID: id,
|
||||
}, []string{payload.ID}); err != nil {
|
||||
h.logger.Warn().Err(err).Msgf("broadcasting event %s has failed", event.ADMIN_KICK)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *MessageHandler) adminBan(id string, session types.Session, payload *message.Admin) error {
|
||||
if !session.Admin() {
|
||||
h.logger.Debug().Msg("user not admin")
|
||||
return nil
|
||||
}
|
||||
|
||||
target, ok := h.sessions.Get(payload.ID)
|
||||
if !ok {
|
||||
h.logger.Debug().Str("id", payload.ID).Msg("can't find session id")
|
||||
return nil
|
||||
}
|
||||
|
||||
if target.Admin() {
|
||||
h.logger.Debug().Msg("target is an admin, baling")
|
||||
return nil
|
||||
}
|
||||
|
||||
remote := target.Address()
|
||||
if remote == "" {
|
||||
h.logger.Debug().Msg("no remote address, baling")
|
||||
return nil
|
||||
}
|
||||
|
||||
address := strings.SplitN(remote, ":", -1)
|
||||
if len(address[0]) < 1 {
|
||||
h.logger.Debug().Str("address", remote).Msg("no remote address, baling")
|
||||
return nil
|
||||
}
|
||||
|
||||
h.logger.Debug().Str("address", remote).Msg("adding address to banned")
|
||||
|
||||
h.banned[address[0]] = true
|
||||
|
||||
if err := target.Kick("banned"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := h.sessions.Broadcast(
|
||||
message.AdminTarget{
|
||||
Event: event.ADMIN_BAN,
|
||||
Target: target.ID(),
|
||||
ID: id,
|
||||
}, []string{payload.ID}); err != nil {
|
||||
h.logger.Warn().Err(err).Msgf("broadcasting event %s has failed", event.ADMIN_BAN)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
56
internal/websocket/broadcast.go
Normal file
56
internal/websocket/broadcast.go
Normal file
@ -0,0 +1,56 @@
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"n.eko.moe/neko/internal/types"
|
||||
"n.eko.moe/neko/internal/types/event"
|
||||
"n.eko.moe/neko/internal/types/message"
|
||||
)
|
||||
|
||||
func (h *MessageHandler) boradcastCreate(session types.Session, payload *message.BroadcastCreate) error {
|
||||
if !session.Admin() {
|
||||
h.logger.Debug().Msg("user not admin")
|
||||
return nil
|
||||
}
|
||||
|
||||
h.broadcast.Create(payload.URL)
|
||||
|
||||
if err := h.boradcastStatus(session); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *MessageHandler) boradcastDestroy(session types.Session) error {
|
||||
if !session.Admin() {
|
||||
h.logger.Debug().Msg("user not admin")
|
||||
return nil
|
||||
}
|
||||
|
||||
h.broadcast.Destroy()
|
||||
|
||||
if err := h.boradcastStatus(session); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *MessageHandler) boradcastStatus(session types.Session) error {
|
||||
if !session.Admin() {
|
||||
h.logger.Debug().Msg("user not admin")
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := session.Send(
|
||||
message.BroadcastStatus{
|
||||
Event: event.BORADCAST_STATUS,
|
||||
IsActive: h.broadcast.IsActive(),
|
||||
URL: h.broadcast.GetUrl(),
|
||||
}); err != nil {
|
||||
h.logger.Warn().Err(err).Msgf("sending event %s has failed", event.BORADCAST_STATUS)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
41
internal/websocket/chat.go
Normal file
41
internal/websocket/chat.go
Normal file
@ -0,0 +1,41 @@
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"n.eko.moe/neko/internal/types"
|
||||
"n.eko.moe/neko/internal/types/event"
|
||||
"n.eko.moe/neko/internal/types/message"
|
||||
)
|
||||
|
||||
func (h *MessageHandler) chat(id string, session types.Session, payload *message.ChatReceive) error {
|
||||
if session.Muted() {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := h.sessions.Broadcast(
|
||||
message.ChatSend{
|
||||
Event: event.CHAT_MESSAGE,
|
||||
Content: payload.Content,
|
||||
ID: id,
|
||||
}, nil); err != nil {
|
||||
h.logger.Warn().Err(err).Msgf("broadcasting event %s has failed", event.CONTROL_RELEASE)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *MessageHandler) chatEmote(id string, session types.Session, payload *message.EmoteReceive) error {
|
||||
if session.Muted() {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := h.sessions.Broadcast(
|
||||
message.EmoteSend{
|
||||
Event: event.CHAT_EMOTE,
|
||||
Emote: payload.Emote,
|
||||
ID: id,
|
||||
}, nil); err != nil {
|
||||
h.logger.Warn().Err(err).Msgf("broadcasting event %s has failed", event.CONTROL_RELEASE)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
163
internal/websocket/control.go
Normal file
163
internal/websocket/control.go
Normal file
@ -0,0 +1,163 @@
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"n.eko.moe/neko/internal/types"
|
||||
"n.eko.moe/neko/internal/types/event"
|
||||
"n.eko.moe/neko/internal/types/message"
|
||||
)
|
||||
|
||||
func (h *MessageHandler) controlRelease(id string, session types.Session) error {
|
||||
|
||||
// check if session is host
|
||||
if !h.sessions.IsHost(id) {
|
||||
h.logger.Debug().Str("id", id).Msg("is not the host")
|
||||
return nil
|
||||
}
|
||||
|
||||
// release host
|
||||
h.logger.Debug().Str("id", id).Msgf("host called %s", event.CONTROL_RELEASE)
|
||||
h.sessions.ClearHost()
|
||||
|
||||
// tell everyone
|
||||
if err := h.sessions.Broadcast(
|
||||
message.Control{
|
||||
Event: event.CONTROL_RELEASE,
|
||||
ID: id,
|
||||
}, nil); err != nil {
|
||||
h.logger.Warn().Err(err).Msgf("broadcasting event %s has failed", event.CONTROL_RELEASE)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *MessageHandler) controlRequest(id string, session types.Session) error {
|
||||
// check for host
|
||||
if !h.sessions.HasHost() {
|
||||
// set host
|
||||
h.sessions.SetHost(id)
|
||||
|
||||
// let everyone know
|
||||
if err := h.sessions.Broadcast(
|
||||
message.Control{
|
||||
Event: event.CONTROL_LOCKED,
|
||||
ID: id,
|
||||
}, nil); err != nil {
|
||||
h.logger.Warn().Err(err).Msgf("broadcasting event %s has failed", event.CONTROL_LOCKED)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// get host
|
||||
host, ok := h.sessions.GetHost()
|
||||
if ok {
|
||||
|
||||
// tell session there is a host
|
||||
if err := session.Send(message.Control{
|
||||
Event: event.CONTROL_REQUEST,
|
||||
ID: host.ID(),
|
||||
}); err != nil {
|
||||
h.logger.Warn().Err(err).Str("id", id).Msgf("sending event %s has failed", event.CONTROL_REQUEST)
|
||||
return err
|
||||
}
|
||||
|
||||
// tell host session wants to be host
|
||||
if err := host.Send(message.Control{
|
||||
Event: event.CONTROL_REQUESTING,
|
||||
ID: id,
|
||||
}); err != nil {
|
||||
h.logger.Warn().Err(err).Str("id", host.ID()).Msgf("sending event %s has failed", event.CONTROL_REQUESTING)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *MessageHandler) controlGive(id string, session types.Session, payload *message.Control) error {
|
||||
// check if session is host
|
||||
if !h.sessions.IsHost(id) {
|
||||
h.logger.Debug().Str("id", id).Msg("is not the host")
|
||||
return nil
|
||||
}
|
||||
|
||||
if !h.sessions.Has(payload.ID) {
|
||||
h.logger.Debug().Str("id", payload.ID).Msg("user does not exist")
|
||||
return nil
|
||||
}
|
||||
|
||||
// set host
|
||||
h.sessions.SetHost(payload.ID)
|
||||
|
||||
// let everyone know
|
||||
if err := h.sessions.Broadcast(
|
||||
message.ControlTarget{
|
||||
Event: event.CONTROL_GIVE,
|
||||
ID: id,
|
||||
Target: payload.ID,
|
||||
}, nil); err != nil {
|
||||
h.logger.Warn().Err(err).Msgf("broadcasting event %s has failed", event.CONTROL_LOCKED)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *MessageHandler) controlClipboard(id string, session types.Session, payload *message.Clipboard) error {
|
||||
// check if session is host
|
||||
if !h.sessions.IsHost(id) {
|
||||
h.logger.Debug().Str("id", id).Msg("is not the host")
|
||||
return nil
|
||||
}
|
||||
|
||||
h.remote.WriteClipboard(payload.Text)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *MessageHandler) controlKeyboard(id string, session types.Session, payload *message.Keyboard) error {
|
||||
// check if session is host
|
||||
if !h.sessions.IsHost(id) {
|
||||
h.logger.Debug().Str("id", id).Msg("is not the host")
|
||||
return nil
|
||||
}
|
||||
|
||||
// change layout
|
||||
if payload.Layout != nil {
|
||||
h.remote.SetKeyboardLayout(*payload.Layout)
|
||||
}
|
||||
|
||||
// set num lock
|
||||
var NumLock = 0
|
||||
if payload.NumLock == nil {
|
||||
NumLock = -1
|
||||
} else if *payload.NumLock {
|
||||
NumLock = 1
|
||||
}
|
||||
|
||||
// set caps lock
|
||||
var CapsLock = 0
|
||||
if payload.CapsLock == nil {
|
||||
CapsLock = -1
|
||||
} else if *payload.CapsLock {
|
||||
CapsLock = 1
|
||||
}
|
||||
|
||||
// set scroll lock
|
||||
var ScrollLock = 0
|
||||
if payload.ScrollLock == nil {
|
||||
ScrollLock = -1
|
||||
} else if *payload.ScrollLock {
|
||||
ScrollLock = 1
|
||||
}
|
||||
|
||||
h.logger.Debug().
|
||||
Int("NumLock", NumLock).
|
||||
Int("CapsLock", CapsLock).
|
||||
Int("ScrollLock", ScrollLock).
|
||||
Msg("setting keyboard modifiers")
|
||||
|
||||
h.remote.SetKeyboardModifiers(NumLock, CapsLock, ScrollLock)
|
||||
return nil
|
||||
}
|
179
internal/websocket/handler.go
Normal file
179
internal/websocket/handler.go
Normal file
@ -0,0 +1,179 @@
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"n.eko.moe/neko/internal/types"
|
||||
"n.eko.moe/neko/internal/types/event"
|
||||
"n.eko.moe/neko/internal/types/message"
|
||||
"n.eko.moe/neko/internal/utils"
|
||||
)
|
||||
|
||||
type MessageHandler struct {
|
||||
logger zerolog.Logger
|
||||
sessions types.SessionManager
|
||||
webrtc types.WebRTCManager
|
||||
remote types.RemoteManager
|
||||
broadcast types.BroadcastManager
|
||||
banned map[string]bool
|
||||
locked bool
|
||||
}
|
||||
|
||||
func (h *MessageHandler) Connected(id string, socket *WebSocket) (bool, string, error) {
|
||||
address := socket.Address()
|
||||
if address == "" {
|
||||
h.logger.Debug().Msg("no remote address")
|
||||
} else {
|
||||
ok, banned := h.banned[address]
|
||||
if ok && banned {
|
||||
h.logger.Debug().Str("address", address).Msg("banned")
|
||||
return false, "banned", nil
|
||||
}
|
||||
}
|
||||
|
||||
if h.locked {
|
||||
session, ok := h.sessions.Get(id)
|
||||
if !ok || !session.Admin() {
|
||||
h.logger.Debug().Msg("server locked")
|
||||
return false, "locked", nil
|
||||
}
|
||||
}
|
||||
|
||||
return true, "", nil
|
||||
}
|
||||
|
||||
func (h *MessageHandler) Disconnected(id string) error {
|
||||
if h.locked && len(h.sessions.Admins()) == 0 {
|
||||
h.locked = false
|
||||
}
|
||||
|
||||
return h.sessions.Destroy(id)
|
||||
}
|
||||
|
||||
func (h *MessageHandler) Message(id string, raw []byte) error {
|
||||
header := message.Message{}
|
||||
if err := json.Unmarshal(raw, &header); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
session, ok := h.sessions.Get(id)
|
||||
if !ok {
|
||||
errors.Errorf("unknown session id %s", id)
|
||||
}
|
||||
|
||||
switch header.Event {
|
||||
// Signal Events
|
||||
case event.SIGNAL_ANSWER:
|
||||
payload := &message.SignalAnswer{}
|
||||
return errors.Wrapf(
|
||||
utils.Unmarshal(payload, raw, func() error {
|
||||
return h.signalAnswer(id, session, payload)
|
||||
}), "%s failed", header.Event)
|
||||
|
||||
// Control Events
|
||||
case event.CONTROL_RELEASE:
|
||||
return errors.Wrapf(h.controlRelease(id, session), "%s failed", header.Event)
|
||||
case event.CONTROL_REQUEST:
|
||||
return errors.Wrapf(h.controlRequest(id, session), "%s failed", header.Event)
|
||||
case event.CONTROL_GIVE:
|
||||
payload := &message.Control{}
|
||||
return errors.Wrapf(
|
||||
utils.Unmarshal(payload, raw, func() error {
|
||||
return h.controlGive(id, session, payload)
|
||||
}), "%s failed", header.Event)
|
||||
case event.CONTROL_CLIPBOARD:
|
||||
payload := &message.Clipboard{}
|
||||
return errors.Wrapf(
|
||||
utils.Unmarshal(payload, raw, func() error {
|
||||
return h.controlClipboard(id, session, payload)
|
||||
}), "%s failed", header.Event)
|
||||
case event.CONTROL_KEYBOARD:
|
||||
payload := &message.Keyboard{}
|
||||
return errors.Wrapf(
|
||||
utils.Unmarshal(payload, raw, func() error {
|
||||
return h.controlKeyboard(id, session, payload)
|
||||
}), "%s failed", header.Event)
|
||||
|
||||
|
||||
// Chat Events
|
||||
case event.CHAT_MESSAGE:
|
||||
payload := &message.ChatReceive{}
|
||||
return errors.Wrapf(
|
||||
utils.Unmarshal(payload, raw, func() error {
|
||||
return h.chat(id, session, payload)
|
||||
}), "%s failed", header.Event)
|
||||
case event.CHAT_EMOTE:
|
||||
payload := &message.EmoteReceive{}
|
||||
return errors.Wrapf(
|
||||
utils.Unmarshal(payload, raw, func() error {
|
||||
return h.chatEmote(id, session, payload)
|
||||
}), "%s failed", header.Event)
|
||||
|
||||
// Screen Events
|
||||
case event.SCREEN_RESOLUTION:
|
||||
return errors.Wrapf(h.screenResolution(id, session), "%s failed", header.Event)
|
||||
case event.SCREEN_CONFIGURATIONS:
|
||||
return errors.Wrapf(h.screenConfigurations(id, session), "%s failed", header.Event)
|
||||
case event.SCREEN_SET:
|
||||
payload := &message.ScreenResolution{}
|
||||
return errors.Wrapf(
|
||||
utils.Unmarshal(payload, raw, func() error {
|
||||
return h.screenSet(id, session, payload)
|
||||
}), "%s failed", header.Event)
|
||||
|
||||
// Boradcast Events
|
||||
case event.BORADCAST_CREATE:
|
||||
payload := &message.BroadcastCreate{}
|
||||
return errors.Wrapf(
|
||||
utils.Unmarshal(payload, raw, func() error {
|
||||
return h.boradcastCreate(session, payload)
|
||||
}), "%s failed", header.Event)
|
||||
case event.BORADCAST_DESTROY:
|
||||
return errors.Wrapf(h.boradcastDestroy(session), "%s failed", header.Event)
|
||||
|
||||
// Admin Events
|
||||
case event.ADMIN_LOCK:
|
||||
return errors.Wrapf(h.adminLock(id, session), "%s failed", header.Event)
|
||||
case event.ADMIN_UNLOCK:
|
||||
return errors.Wrapf(h.adminUnlock(id, session), "%s failed", header.Event)
|
||||
case event.ADMIN_CONTROL:
|
||||
return errors.Wrapf(h.adminControl(id, session), "%s failed", header.Event)
|
||||
case event.ADMIN_RELEASE:
|
||||
return errors.Wrapf(h.adminRelease(id, session), "%s failed", header.Event)
|
||||
case event.ADMIN_GIVE:
|
||||
payload := &message.Admin{}
|
||||
return errors.Wrapf(
|
||||
utils.Unmarshal(payload, raw, func() error {
|
||||
return h.adminGive(id, session, payload)
|
||||
}), "%s failed", header.Event)
|
||||
case event.ADMIN_BAN:
|
||||
payload := &message.Admin{}
|
||||
return errors.Wrapf(
|
||||
utils.Unmarshal(payload, raw, func() error {
|
||||
return h.adminBan(id, session, payload)
|
||||
}), "%s failed", header.Event)
|
||||
case event.ADMIN_KICK:
|
||||
payload := &message.Admin{}
|
||||
return errors.Wrapf(
|
||||
utils.Unmarshal(payload, raw, func() error {
|
||||
return h.adminKick(id, session, payload)
|
||||
}), "%s failed", header.Event)
|
||||
case event.ADMIN_MUTE:
|
||||
payload := &message.Admin{}
|
||||
return errors.Wrapf(
|
||||
utils.Unmarshal(payload, raw, func() error {
|
||||
return h.adminMute(id, session, payload)
|
||||
}), "%s failed", header.Event)
|
||||
case event.ADMIN_UNMUTE:
|
||||
payload := &message.Admin{}
|
||||
return errors.Wrapf(
|
||||
utils.Unmarshal(payload, raw, func() error {
|
||||
return h.adminUnmute(id, session, payload)
|
||||
}), "%s failed", header.Event)
|
||||
default:
|
||||
return errors.Errorf("unknown message event %s", header.Event)
|
||||
}
|
||||
}
|
66
internal/websocket/screen.go
Normal file
66
internal/websocket/screen.go
Normal file
@ -0,0 +1,66 @@
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"n.eko.moe/neko/internal/types"
|
||||
"n.eko.moe/neko/internal/types/event"
|
||||
"n.eko.moe/neko/internal/types/message"
|
||||
)
|
||||
|
||||
func (h *MessageHandler) screenSet(id string, session types.Session, payload *message.ScreenResolution) error {
|
||||
if !session.Admin() {
|
||||
h.logger.Debug().Msg("user not admin")
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := h.remote.ChangeResolution(payload.Width, payload.Height, payload.Rate); err != nil {
|
||||
h.logger.Warn().Err(err).Msgf("unable to change screen size")
|
||||
return err
|
||||
}
|
||||
|
||||
if err := h.sessions.Broadcast(
|
||||
message.ScreenResolution{
|
||||
Event: event.SCREEN_RESOLUTION,
|
||||
ID: id,
|
||||
Width: payload.Width,
|
||||
Height: payload.Height,
|
||||
Rate: payload.Rate,
|
||||
}, nil); err != nil {
|
||||
h.logger.Warn().Err(err).Msgf("broadcasting event %s has failed", event.SCREEN_RESOLUTION)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *MessageHandler) screenResolution(id string, session types.Session) error {
|
||||
if size := h.remote.GetScreenSize(); size != nil {
|
||||
if err := session.Send(message.ScreenResolution{
|
||||
Event: event.SCREEN_RESOLUTION,
|
||||
Width: size.Width,
|
||||
Height: size.Height,
|
||||
Rate: int(size.Rate),
|
||||
}); err != nil {
|
||||
h.logger.Warn().Err(err).Msgf("sending event %s has failed", event.SCREEN_RESOLUTION)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *MessageHandler) screenConfigurations(id string, session types.Session) error {
|
||||
if !session.Admin() {
|
||||
h.logger.Debug().Msg("user not admin")
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := session.Send(message.ScreenConfigurations{
|
||||
Event: event.SCREEN_CONFIGURATIONS,
|
||||
Configurations: h.remote.ScreenConfigurations(),
|
||||
}); err != nil {
|
||||
h.logger.Warn().Err(err).Msgf("sending event %s has failed", event.SCREEN_CONFIGURATIONS)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
93
internal/websocket/session.go
Normal file
93
internal/websocket/session.go
Normal file
@ -0,0 +1,93 @@
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"n.eko.moe/neko/internal/types"
|
||||
"n.eko.moe/neko/internal/types/event"
|
||||
"n.eko.moe/neko/internal/types/message"
|
||||
)
|
||||
|
||||
func (h *MessageHandler) SessionCreated(id string, session types.Session) error {
|
||||
// send sdp and id over to client
|
||||
if err := h.signalProvide(id, session); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if session.Admin() {
|
||||
// send screen configurations if admin
|
||||
if err := h.screenConfigurations(id, session); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// send broadcast status if admin
|
||||
if err := h.boradcastStatus(session); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *MessageHandler) SessionConnected(id string, session types.Session) error {
|
||||
// send list of members to session
|
||||
if err := session.Send(message.MembersList{
|
||||
Event: event.MEMBER_LIST,
|
||||
Memebers: h.sessions.Members(),
|
||||
}); err != nil {
|
||||
h.logger.Warn().Str("id", id).Err(err).Msgf("sending event %s has failed", event.MEMBER_LIST)
|
||||
return err
|
||||
}
|
||||
|
||||
// send screen current resolution
|
||||
if err := h.screenResolution(id, session); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// tell session there is a host
|
||||
host, ok := h.sessions.GetHost()
|
||||
if ok {
|
||||
if err := session.Send(message.Control{
|
||||
Event: event.CONTROL_LOCKED,
|
||||
ID: host.ID(),
|
||||
}); err != nil {
|
||||
h.logger.Warn().Str("id", id).Err(err).Msgf("sending event %s has failed", event.CONTROL_LOCKED)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// let everyone know there is a new session
|
||||
if err := h.sessions.Broadcast(
|
||||
message.Member{
|
||||
Event: event.MEMBER_CONNECTED,
|
||||
Member: session.Member(),
|
||||
}, nil); err != nil {
|
||||
h.logger.Warn().Err(err).Msgf("broadcasting event %s has failed", event.CONTROL_RELEASE)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *MessageHandler) SessionDestroyed(id string) error {
|
||||
// clear host if exists
|
||||
if h.sessions.IsHost(id) {
|
||||
h.sessions.ClearHost()
|
||||
if err := h.sessions.Broadcast(message.Control{
|
||||
Event: event.CONTROL_RELEASE,
|
||||
ID: id,
|
||||
}, nil); err != nil {
|
||||
h.logger.Warn().Err(err).Msgf("broadcasting event %s has failed", event.CONTROL_RELEASE)
|
||||
}
|
||||
}
|
||||
|
||||
// let everyone know session disconnected
|
||||
if err := h.sessions.Broadcast(
|
||||
message.MemberDisconnected{
|
||||
Event: event.MEMBER_DISCONNECTED,
|
||||
ID: id,
|
||||
}, nil); err != nil {
|
||||
h.logger.Warn().Err(err).Msgf("broadcasting event %s has failed", event.MEMBER_DISCONNECTED)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
38
internal/websocket/signal.go
Normal file
38
internal/websocket/signal.go
Normal file
@ -0,0 +1,38 @@
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"n.eko.moe/neko/internal/types"
|
||||
"n.eko.moe/neko/internal/types/event"
|
||||
"n.eko.moe/neko/internal/types/message"
|
||||
)
|
||||
|
||||
func (h *MessageHandler) signalProvide(id string, session types.Session) error {
|
||||
sdp, lite, ice, err := h.webrtc.CreatePeer(id, session)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := session.Send(message.SignalProvide{
|
||||
Event: event.SIGNAL_PROVIDE,
|
||||
ID: id,
|
||||
SDP: sdp,
|
||||
Lite: lite,
|
||||
ICE: ice,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *MessageHandler) signalAnswer(id string, session types.Session, payload *message.SignalAnswer) error {
|
||||
if err := session.SetName(payload.DisplayName); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := session.SignalAnswer(payload.SDP); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
55
internal/websocket/socket.go
Normal file
55
internal/websocket/socket.go
Normal file
@ -0,0 +1,55 @@
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
type WebSocket struct {
|
||||
id string
|
||||
address string
|
||||
ws *WebSocketHandler
|
||||
connection *websocket.Conn
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func (socket *WebSocket) Address() string {
|
||||
//remote := socket.connection.RemoteAddr()
|
||||
address := strings.SplitN(socket.address, ":", -1)
|
||||
if len(address[0]) < 1 {
|
||||
return socket.address
|
||||
}
|
||||
return address[0]
|
||||
}
|
||||
|
||||
func (socket *WebSocket) Send(v interface{}) error {
|
||||
socket.mu.Lock()
|
||||
defer socket.mu.Unlock()
|
||||
if socket.connection == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
raw, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
socket.ws.logger.Debug().
|
||||
Str("session", socket.id).
|
||||
Str("address", socket.connection.RemoteAddr().String()).
|
||||
Str("raw", string(raw)).
|
||||
Msg("sending message to client")
|
||||
|
||||
return socket.connection.WriteMessage(websocket.TextMessage, raw)
|
||||
}
|
||||
|
||||
func (socket *WebSocket) Destroy() error {
|
||||
if socket.connection == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return socket.connection.Close()
|
||||
}
|
268
internal/websocket/websocket.go
Normal file
268
internal/websocket/websocket.go
Normal file
@ -0,0 +1,268 @@
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"n.eko.moe/neko/internal/types"
|
||||
"n.eko.moe/neko/internal/types/config"
|
||||
"n.eko.moe/neko/internal/types/event"
|
||||
"n.eko.moe/neko/internal/types/message"
|
||||
"n.eko.moe/neko/internal/utils"
|
||||
)
|
||||
|
||||
func New(sessions types.SessionManager, remote types.RemoteManager, broadcast types.BroadcastManager, webrtc types.WebRTCManager, conf *config.WebSocket) *WebSocketHandler {
|
||||
logger := log.With().Str("module", "websocket").Logger()
|
||||
|
||||
return &WebSocketHandler{
|
||||
logger: logger,
|
||||
conf: conf,
|
||||
sessions: sessions,
|
||||
remote: remote,
|
||||
upgrader: websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
return true
|
||||
},
|
||||
},
|
||||
handler: &MessageHandler{
|
||||
logger: logger.With().Str("subsystem", "handler").Logger(),
|
||||
remote: remote,
|
||||
broadcast: broadcast,
|
||||
sessions: sessions,
|
||||
webrtc: webrtc,
|
||||
banned: make(map[string]bool),
|
||||
locked: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Send pings to peer with this period. Must be less than pongWait.
|
||||
const pingPeriod = 60 * time.Second
|
||||
|
||||
type WebSocketHandler struct {
|
||||
logger zerolog.Logger
|
||||
upgrader websocket.Upgrader
|
||||
sessions types.SessionManager
|
||||
remote types.RemoteManager
|
||||
conf *config.WebSocket
|
||||
handler *MessageHandler
|
||||
shutdown chan bool
|
||||
}
|
||||
|
||||
func (ws *WebSocketHandler) Start() error {
|
||||
ws.sessions.OnCreated(func(id string, session types.Session) {
|
||||
if err := ws.handler.SessionCreated(id, session); err != nil {
|
||||
ws.logger.Warn().Str("id", id).Err(err).Msg("session created with and error")
|
||||
} else {
|
||||
ws.logger.Debug().Str("id", id).Msg("session created")
|
||||
}
|
||||
})
|
||||
|
||||
ws.sessions.OnConnected(func(id string, session types.Session) {
|
||||
if err := ws.handler.SessionConnected(id, session); err != nil {
|
||||
ws.logger.Warn().Str("id", id).Err(err).Msg("session connected with and error")
|
||||
} else {
|
||||
ws.logger.Debug().Str("id", id).Msg("session connected")
|
||||
}
|
||||
})
|
||||
|
||||
ws.sessions.OnDestroy(func(id string, session types.Session) {
|
||||
if err := ws.handler.SessionDestroyed(id); err != nil {
|
||||
ws.logger.Warn().Str("id", id).Err(err).Msg("session destroyed with and error")
|
||||
} else {
|
||||
ws.logger.Debug().Str("id", id).Msg("session destroyed")
|
||||
}
|
||||
})
|
||||
|
||||
go func() {
|
||||
defer func() {
|
||||
ws.logger.Info().Msg("shutdown")
|
||||
}()
|
||||
|
||||
current := ws.remote.ReadClipboard()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ws.shutdown:
|
||||
return
|
||||
default:
|
||||
if ws.sessions.HasHost() {
|
||||
text := ws.remote.ReadClipboard()
|
||||
if text != current {
|
||||
session, ok := ws.sessions.GetHost()
|
||||
if ok {
|
||||
session.Send(message.Clipboard{
|
||||
Event: event.CONTROL_CLIPBOARD,
|
||||
Text: text,
|
||||
})
|
||||
}
|
||||
current = text
|
||||
}
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ws *WebSocketHandler) Shutdown() error {
|
||||
ws.shutdown <- true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ws *WebSocketHandler) Upgrade(w http.ResponseWriter, r *http.Request) error {
|
||||
ws.logger.Debug().Msg("attempting to upgrade connection")
|
||||
|
||||
connection, err := ws.upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
ws.logger.Error().Err(err).Msg("failed to upgrade connection")
|
||||
return err
|
||||
}
|
||||
|
||||
id, ip, admin, err := ws.authenticate(r)
|
||||
if err != nil {
|
||||
ws.logger.Warn().Err(err).Msg("authentication failed")
|
||||
|
||||
if err = connection.WriteJSON(message.Disconnect{
|
||||
Event: event.SYSTEM_DISCONNECT,
|
||||
Message: "invalid_password",
|
||||
}); err != nil {
|
||||
ws.logger.Error().Err(err).Msg("failed to send disconnect")
|
||||
}
|
||||
|
||||
if err = connection.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
socket := &WebSocket{
|
||||
id: id,
|
||||
ws: ws,
|
||||
address: ip,
|
||||
connection: connection,
|
||||
}
|
||||
|
||||
ok, reason, err := ws.handler.Connected(id, socket)
|
||||
if err != nil {
|
||||
ws.logger.Error().Err(err).Msg("connection failed")
|
||||
return err
|
||||
}
|
||||
|
||||
if !ok {
|
||||
if err = connection.WriteJSON(message.Disconnect{
|
||||
Event: event.SYSTEM_DISCONNECT,
|
||||
Message: reason,
|
||||
}); err != nil {
|
||||
ws.logger.Error().Err(err).Msg("failed to send disconnect")
|
||||
}
|
||||
|
||||
if err = connection.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
ws.sessions.New(id, admin, socket)
|
||||
|
||||
ws.logger.
|
||||
Debug().
|
||||
Str("session", id).
|
||||
Str("address", connection.RemoteAddr().String()).
|
||||
Msg("new connection created")
|
||||
|
||||
defer func() {
|
||||
ws.logger.
|
||||
Debug().
|
||||
Str("session", id).
|
||||
Str("address", connection.RemoteAddr().String()).
|
||||
Msg("session ended")
|
||||
}()
|
||||
|
||||
ws.handle(connection, id)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ws *WebSocketHandler) authenticate(r *http.Request) (string, string, bool, error) {
|
||||
ip := r.RemoteAddr
|
||||
|
||||
if ws.conf.Proxy {
|
||||
ip = utils.ReadUserIP(r)
|
||||
}
|
||||
|
||||
id, err := utils.NewUID(32)
|
||||
if err != nil {
|
||||
return "", ip, false, err
|
||||
}
|
||||
|
||||
passwords, ok := r.URL.Query()["password"]
|
||||
if !ok || len(passwords[0]) < 1 {
|
||||
return "", ip, false, fmt.Errorf("no password provided")
|
||||
}
|
||||
|
||||
if passwords[0] == ws.conf.AdminPassword {
|
||||
return id, ip, true, nil
|
||||
}
|
||||
|
||||
if passwords[0] == ws.conf.Password {
|
||||
return id, ip, false, nil
|
||||
}
|
||||
|
||||
return "", ip, false, fmt.Errorf("invalid password: %s", passwords[0])
|
||||
}
|
||||
|
||||
func (ws *WebSocketHandler) handle(connection *websocket.Conn, id string) {
|
||||
bytes := make(chan []byte)
|
||||
cancel := make(chan struct{})
|
||||
ticker := time.NewTicker(pingPeriod)
|
||||
|
||||
go func() {
|
||||
defer func() {
|
||||
ticker.Stop()
|
||||
ws.logger.Debug().Str("address", connection.RemoteAddr().String()).Msg("handle socket ending")
|
||||
ws.handler.Disconnected(id)
|
||||
}()
|
||||
|
||||
for {
|
||||
_, raw, err := connection.ReadMessage()
|
||||
if err != nil {
|
||||
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
|
||||
ws.logger.Warn().Err(err).Msg("read message error")
|
||||
} else {
|
||||
ws.logger.Debug().Err(err).Msg("read message error")
|
||||
}
|
||||
close(cancel)
|
||||
break
|
||||
}
|
||||
bytes <- raw
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case raw := <-bytes:
|
||||
ws.logger.Debug().
|
||||
Str("session", id).
|
||||
Str("address", connection.RemoteAddr().String()).
|
||||
Str("raw", string(raw)).
|
||||
Msg("received message from client")
|
||||
if err := ws.handler.Message(id, raw); err != nil {
|
||||
ws.logger.Error().Err(err).Msg("message handler has failed")
|
||||
}
|
||||
case <-cancel:
|
||||
return
|
||||
case _ = <-ticker.C:
|
||||
if err := connection.WriteMessage(websocket.PingMessage, nil); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
195
internal/xorg/xorg.c
Normal file
195
internal/xorg/xorg.c
Normal file
@ -0,0 +1,195 @@
|
||||
#include "xorg.h"
|
||||
|
||||
static clipboard_c *CLIPBOARD = NULL;
|
||||
static Display *DISPLAY = NULL;
|
||||
static char *NAME = ":0.0";
|
||||
static int REGISTERED = 0;
|
||||
static int DIRTY = 0;
|
||||
|
||||
Display *getXDisplay(void) {
|
||||
/* Close the display if displayName has changed */
|
||||
if (DIRTY) {
|
||||
XDisplayClose();
|
||||
DIRTY = 0;
|
||||
}
|
||||
|
||||
if (DISPLAY == NULL) {
|
||||
/* First try the user set displayName */
|
||||
DISPLAY = XOpenDisplay(NAME);
|
||||
|
||||
/* Then try using environment variable DISPLAY */
|
||||
if (DISPLAY == NULL) {
|
||||
DISPLAY = XOpenDisplay(NULL);
|
||||
}
|
||||
|
||||
if (DISPLAY == NULL) {
|
||||
fputs("Could not open main display\n", stderr);
|
||||
} else if (!REGISTERED) {
|
||||
atexit(&XDisplayClose);
|
||||
REGISTERED = 1;
|
||||
}
|
||||
}
|
||||
|
||||
return DISPLAY;
|
||||
}
|
||||
|
||||
clipboard_c *getClipboard(void) {
|
||||
if (CLIPBOARD == NULL) {
|
||||
CLIPBOARD = clipboard_new(NULL);
|
||||
}
|
||||
return CLIPBOARD;
|
||||
}
|
||||
|
||||
void XDisplayClose(void) {
|
||||
if (DISPLAY != NULL) {
|
||||
XCloseDisplay(DISPLAY);
|
||||
DISPLAY = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
void XDisplaySet(char *input) {
|
||||
NAME = strdup(input);
|
||||
DIRTY = 1;
|
||||
}
|
||||
|
||||
void XMove(int x, int y) {
|
||||
Display *display = getXDisplay();
|
||||
XWarpPointer(display, None, DefaultRootWindow(display), 0, 0, 0, 0, x, y);
|
||||
XSync(display, 0);
|
||||
}
|
||||
|
||||
void XScroll(int x, int y) {
|
||||
int ydir = 4; /* Button 4 is up, 5 is down. */
|
||||
int xdir = 6;
|
||||
|
||||
Display *display = getXDisplay();
|
||||
|
||||
if (y < 0) {
|
||||
ydir = 5;
|
||||
}
|
||||
|
||||
if (x < 0) {
|
||||
xdir = 7;
|
||||
}
|
||||
|
||||
int xi;
|
||||
int yi;
|
||||
|
||||
for (xi = 0; xi < abs(x); xi++) {
|
||||
XTestFakeButtonEvent(display, xdir, 1, CurrentTime);
|
||||
XTestFakeButtonEvent(display, xdir, 0, CurrentTime);
|
||||
}
|
||||
|
||||
for (yi = 0; yi < abs(y); yi++) {
|
||||
XTestFakeButtonEvent(display, ydir, 1, CurrentTime);
|
||||
XTestFakeButtonEvent(display, ydir, 0, CurrentTime);
|
||||
}
|
||||
|
||||
XSync(display, 0);
|
||||
}
|
||||
|
||||
void XButton(unsigned int button, int down) {
|
||||
if (button != 0) {
|
||||
Display *display = getXDisplay();
|
||||
XTestFakeButtonEvent(display, button, down, CurrentTime);
|
||||
XSync(display, 0);
|
||||
}
|
||||
}
|
||||
|
||||
void XKey(unsigned long key, int down) {
|
||||
if (key != 0) {
|
||||
Display *display = getXDisplay();
|
||||
KeyCode code = XKeysymToKeycode(display, key);
|
||||
|
||||
// Map non-existing keysyms to new keycodes
|
||||
if(code == 0) {
|
||||
int min, max, numcodes;
|
||||
XDisplayKeycodes(display, &min, &max);
|
||||
XGetKeyboardMapping(display, min, max-min, &numcodes);
|
||||
|
||||
code = (max-min+1)*numcodes;
|
||||
KeySym keysym_list[numcodes];
|
||||
for(int i=0;i<numcodes;i++) keysym_list[i] = key;
|
||||
XChangeKeyboardMapping(display, code, numcodes, keysym_list, 1);
|
||||
}
|
||||
|
||||
XTestFakeKeyEvent(display, code, down, CurrentTime);
|
||||
XSync(display, 0);
|
||||
}
|
||||
}
|
||||
|
||||
void XClipboardSet(char *src) {
|
||||
clipboard_c *cb = getClipboard();
|
||||
clipboard_set_text_ex(cb, src, strlen(src), 0);
|
||||
}
|
||||
|
||||
char *XClipboardGet() {
|
||||
clipboard_c *cb = getClipboard();
|
||||
return clipboard_text_ex(cb, NULL, 0);
|
||||
}
|
||||
|
||||
void XGetScreenConfigurations() {
|
||||
Display *display = getXDisplay();
|
||||
Window root = RootWindow(display, 0);
|
||||
XRRScreenSize *xrrs;
|
||||
int num_sizes;
|
||||
|
||||
xrrs = XRRSizes(display, 0, &num_sizes);
|
||||
for(int i = 0; i < num_sizes; i ++) {
|
||||
short *rates;
|
||||
int num_rates;
|
||||
|
||||
goCreateScreenSize(i, xrrs[i].width, xrrs[i].height, xrrs[i].mwidth, xrrs[i].mheight);
|
||||
rates = XRRRates(display, 0, i, &num_rates);
|
||||
for (int j = 0; j < num_rates; j ++) {
|
||||
goSetScreenRates(i, j, rates[j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void XSetScreenConfiguration(int index, short rate) {
|
||||
Display *display = getXDisplay();
|
||||
Window root = RootWindow(display, 0);
|
||||
XRRSetScreenConfigAndRate(display, XRRGetScreenInfo(display, root), root, index, RR_Rotate_0, rate, CurrentTime);
|
||||
}
|
||||
|
||||
int XGetScreenSize() {
|
||||
Display *display = getXDisplay();
|
||||
XRRScreenConfiguration *conf = XRRGetScreenInfo(display, RootWindow(display, 0));
|
||||
Rotation original_rotation;
|
||||
return XRRConfigCurrentConfiguration(conf, &original_rotation);
|
||||
}
|
||||
|
||||
short XGetScreenRate() {
|
||||
Display *display = getXDisplay();
|
||||
XRRScreenConfiguration *conf = XRRGetScreenInfo(display, RootWindow(display, 0));
|
||||
return XRRConfigCurrentRate(conf);
|
||||
}
|
||||
|
||||
void SetKeyboardLayout(char *layout) {
|
||||
// TOOD: refactor, use native API.
|
||||
char cmd[13] = "setxkbmap ";
|
||||
strncat(cmd, layout, 2);
|
||||
system(cmd);
|
||||
}
|
||||
|
||||
void SetKeyboardModifiers(int num_lock, int caps_lock, int scroll_lock) {
|
||||
Display *display = getXDisplay();
|
||||
|
||||
if (num_lock != -1) {
|
||||
XkbLockModifiers(display, XkbUseCoreKbd, 16, num_lock * 16);
|
||||
}
|
||||
|
||||
if (caps_lock != -1) {
|
||||
XkbLockModifiers(display, XkbUseCoreKbd, 2, caps_lock * 2);
|
||||
}
|
||||
|
||||
if (scroll_lock != -1) {
|
||||
XKeyboardControl values;
|
||||
values.led_mode = scroll_lock ? LedModeOn : LedModeOff;
|
||||
values.led = 3;
|
||||
XChangeKeyboardControl(display, KBLedMode, &values);
|
||||
}
|
||||
|
||||
XFlush(display);
|
||||
}
|
247
internal/xorg/xorg.go
Normal file
247
internal/xorg/xorg.go
Normal file
@ -0,0 +1,247 @@
|
||||
package xorg
|
||||
|
||||
/*
|
||||
#cgo linux CFLAGS: -I/usr/src -I/usr/local/include/
|
||||
#cgo linux LDFLAGS: /usr/local/lib/libclipboard.a -L/usr/src -L/usr/local/lib -lX11 -lXtst -lXrandr -lxcb
|
||||
|
||||
#include "xorg.h"
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
"unsafe"
|
||||
"regexp"
|
||||
|
||||
"n.eko.moe/neko/internal/types"
|
||||
)
|
||||
|
||||
var ScreenConfigurations = make(map[int]types.ScreenConfiguration)
|
||||
|
||||
var debounce_button = make(map[int]time.Time)
|
||||
var debounce_key = make(map[uint64]time.Time)
|
||||
var mu = sync.Mutex{}
|
||||
|
||||
func init() {
|
||||
C.XGetScreenConfigurations()
|
||||
}
|
||||
|
||||
func Display(display string) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
displayUnsafe := C.CString(display)
|
||||
defer C.free(unsafe.Pointer(displayUnsafe))
|
||||
|
||||
C.XDisplaySet(displayUnsafe)
|
||||
}
|
||||
|
||||
func Move(x, y int) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
C.XMove(C.int(x), C.int(y))
|
||||
}
|
||||
|
||||
func Scroll(x, y int) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
C.XScroll(C.int(x), C.int(y))
|
||||
}
|
||||
|
||||
func ButtonDown(code int) 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 uint64) 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.ulong(code), C.int(1))
|
||||
return nil
|
||||
}
|
||||
|
||||
func ButtonUp(code int) 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 uint64) 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.ulong(code), C.int(0))
|
||||
return nil
|
||||
}
|
||||
|
||||
func ReadClipboard() string {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
clipboardUnsafe := C.XClipboardGet()
|
||||
defer C.free(unsafe.Pointer(clipboardUnsafe))
|
||||
|
||||
return C.GoString(clipboardUnsafe)
|
||||
}
|
||||
|
||||
func WriteClipboard(data string) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
clipboardUnsafe := C.CString(data)
|
||||
defer C.free(unsafe.Pointer(clipboardUnsafe))
|
||||
|
||||
C.XClipboardSet(clipboardUnsafe)
|
||||
}
|
||||
|
||||
func ResetKeys() {
|
||||
for code := range debounce_button {
|
||||
ButtonUp(code)
|
||||
|
||||
delete(debounce_button, code)
|
||||
}
|
||||
for code := range debounce_key {
|
||||
KeyUp(code)
|
||||
|
||||
delete(debounce_key, code)
|
||||
}
|
||||
}
|
||||
|
||||
func CheckKeys(duration time.Duration) {
|
||||
t := time.Now()
|
||||
for code, start := range debounce_button {
|
||||
if t.Sub(start) < duration {
|
||||
continue
|
||||
}
|
||||
ButtonUp(code)
|
||||
|
||||
delete(debounce_button, code)
|
||||
}
|
||||
for code, start := range debounce_key {
|
||||
if t.Sub(start) < duration {
|
||||
continue
|
||||
}
|
||||
KeyUp(code)
|
||||
|
||||
delete(debounce_key, code)
|
||||
}
|
||||
}
|
||||
|
||||
func ValidScreenSize(width int, height int, rate int) bool {
|
||||
for _, size := range ScreenConfigurations {
|
||||
if size.Width == width && size.Height == height {
|
||||
for _, fps := range size.Rates {
|
||||
if int16(rate) == fps {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func ChangeScreenSize(width int, height int, rate int) error {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
for index, size := range ScreenConfigurations {
|
||||
if size.Width == width && size.Height == height {
|
||||
for _, fps := range size.Rates {
|
||||
if int16(rate) == fps {
|
||||
C.XSetScreenConfiguration(C.int(index), C.short(fps))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("unknown configuration")
|
||||
}
|
||||
|
||||
func GetScreenSize() *types.ScreenSize {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
index := int(C.XGetScreenSize())
|
||||
rate := int16(C.XGetScreenRate())
|
||||
|
||||
if conf, ok := ScreenConfigurations[index]; ok {
|
||||
return &types.ScreenSize{
|
||||
Width: conf.Width,
|
||||
Height: conf.Height,
|
||||
Rate: rate,
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func SetKeyboardLayout(layout string) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
if !regexp.MustCompile(`^[a-zA-Z]+$`).MatchString(layout) {
|
||||
return
|
||||
}
|
||||
|
||||
layoutUnsafe := C.CString(layout)
|
||||
defer C.free(unsafe.Pointer(layoutUnsafe))
|
||||
|
||||
C.SetKeyboardLayout(layoutUnsafe)
|
||||
}
|
||||
|
||||
func SetKeyboardModifiers(num_lock int, caps_lock int, scroll_lock int) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
C.SetKeyboardModifiers(C.int(num_lock), C.int(caps_lock), C.int(scroll_lock))
|
||||
}
|
||||
|
||||
//export goCreateScreenSize
|
||||
func goCreateScreenSize(index C.int, width C.int, height C.int, mwidth C.int, mheight C.int) {
|
||||
ScreenConfigurations[int(index)] = types.ScreenConfiguration{
|
||||
Width: int(width),
|
||||
Height: int(height),
|
||||
Rates: make(map[int]int16),
|
||||
}
|
||||
}
|
||||
|
||||
//export goSetScreenRates
|
||||
func goSetScreenRates(index C.int, rate_index C.int, rate C.short) {
|
||||
ScreenConfigurations[int(index)].Rates[int(rate_index)] = int16(rate)
|
||||
}
|
46
internal/xorg/xorg.h
Normal file
46
internal/xorg/xorg.h
Normal file
@ -0,0 +1,46 @@
|
||||
#pragma once
|
||||
|
||||
#ifndef XDISPLAY_H
|
||||
#define XDISPLAY_H
|
||||
|
||||
#include <X11/Xlib.h>
|
||||
#include <X11/XKBlib.h>
|
||||
#include <X11/extensions/Xrandr.h>
|
||||
#include <X11/extensions/XTest.h>
|
||||
#include <libclipboard.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h> /* For fputs() */
|
||||
#include <string.h> /* For strdup() */
|
||||
|
||||
extern void goCreateScreenSize(int index, int width, int height, int mwidth, int mheight);
|
||||
extern void goSetScreenRates(int index, int rate_index, short rate);
|
||||
|
||||
/* Returns the main display, closed either on exit or when closeMainDisplay()
|
||||
* is invoked. This removes a bit of the overhead of calling XOpenDisplay() &
|
||||
* XCloseDisplay() everytime the main display needs to be used.
|
||||
*
|
||||
* Note that this is almost certainly not thread safe. */
|
||||
Display *getXDisplay(void);
|
||||
clipboard_c *getClipboard(void);
|
||||
|
||||
void XMove(int x, int y);
|
||||
void XScroll(int x, int y);
|
||||
void XButton(unsigned int button, int down);
|
||||
void XKey(unsigned long key, int down);
|
||||
|
||||
void XClipboardSet(char *src);
|
||||
char *XClipboardGet();
|
||||
|
||||
void XGetScreenConfigurations();
|
||||
void XSetScreenConfiguration(int index, short rate);
|
||||
int XGetScreenSize();
|
||||
short XGetScreenRate();
|
||||
|
||||
void XDisplayClose(void);
|
||||
void XDisplaySet(char *input);
|
||||
|
||||
void SetKeyboardLayout(char *layout);
|
||||
void SetKeyboardModifiers(int num_lock, int caps_lock, int scroll_lock);
|
||||
#endif
|
||||
|
8
neko.code-workspace
Normal file
8
neko.code-workspace
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": "."
|
||||
}
|
||||
],
|
||||
"settings": {}
|
||||
}
|
184
neko.go
Normal file
184
neko.go
Normal file
@ -0,0 +1,184 @@
|
||||
package neko
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
|
||||
"n.eko.moe/neko/internal/broadcast"
|
||||
"n.eko.moe/neko/internal/http"
|
||||
"n.eko.moe/neko/internal/remote"
|
||||
"n.eko.moe/neko/internal/session"
|
||||
"n.eko.moe/neko/internal/types/config"
|
||||
"n.eko.moe/neko/internal/webrtc"
|
||||
"n.eko.moe/neko/internal/websocket"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const Header = `&34
|
||||
_ __ __
|
||||
/ | / /__ / /______ \ /\
|
||||
/ |/ / _ \/ //_/ __ \ ) ( ')
|
||||
/ /| / __/ ,< / /_/ / ( / )
|
||||
/_/ |_/\___/_/|_|\____/ \(__)|
|
||||
&1&37 nurdism/neko &33%s v%s&0
|
||||
`
|
||||
|
||||
var (
|
||||
//
|
||||
buildDate = "dev"
|
||||
//
|
||||
gitCommit = "dev"
|
||||
//
|
||||
gitBranch = "dev"
|
||||
|
||||
// Major version when you make incompatible API changes,
|
||||
major = "dev"
|
||||
// Minor version when you add functionality in a backwards-compatible manner, and
|
||||
minor = "dev"
|
||||
// Patch version when you make backwards-compatible bug fixeneko.
|
||||
patch = "dev"
|
||||
)
|
||||
|
||||
var Service *Neko
|
||||
|
||||
func init() {
|
||||
Service = &Neko{
|
||||
Version: &Version{
|
||||
Major: major,
|
||||
Minor: minor,
|
||||
Patch: patch,
|
||||
GitCommit: gitCommit,
|
||||
GitBranch: gitBranch,
|
||||
BuildDate: buildDate,
|
||||
GoVersion: runtime.Version(),
|
||||
Compiler: runtime.Compiler,
|
||||
Platform: fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH),
|
||||
},
|
||||
Root: &config.Root{},
|
||||
Server: &config.Server{},
|
||||
Remote: &config.Remote{},
|
||||
Broadcast: &config.Broadcast{},
|
||||
WebRTC: &config.WebRTC{},
|
||||
WebSocket: &config.WebSocket{},
|
||||
}
|
||||
}
|
||||
|
||||
type Version struct {
|
||||
Major string
|
||||
Minor string
|
||||
Patch string
|
||||
GitCommit string
|
||||
GitBranch string
|
||||
BuildDate string
|
||||
GoVersion string
|
||||
Compiler string
|
||||
Platform string
|
||||
}
|
||||
|
||||
func (i *Version) String() string {
|
||||
return fmt.Sprintf("%s.%s.%s %s", i.Major, i.Minor, i.Patch, i.GitCommit)
|
||||
}
|
||||
|
||||
func (i *Version) Details() string {
|
||||
return fmt.Sprintf(
|
||||
"%s\n%s\n%s\n%s\n%s\n%s\n%s\n",
|
||||
fmt.Sprintf("Version %s.%s.%s", i.Major, i.Minor, i.Patch),
|
||||
fmt.Sprintf("GitCommit %s", i.GitCommit),
|
||||
fmt.Sprintf("GitBranch %s", i.GitBranch),
|
||||
fmt.Sprintf("BuildDate %s", i.BuildDate),
|
||||
fmt.Sprintf("GoVersion %s", i.GoVersion),
|
||||
fmt.Sprintf("Compiler %s", i.Compiler),
|
||||
fmt.Sprintf("Platform %s", i.Platform),
|
||||
)
|
||||
}
|
||||
|
||||
type Neko struct {
|
||||
Version *Version
|
||||
Root *config.Root
|
||||
Remote *config.Remote
|
||||
Broadcast *config.Broadcast
|
||||
Server *config.Server
|
||||
WebRTC *config.WebRTC
|
||||
WebSocket *config.WebSocket
|
||||
|
||||
logger zerolog.Logger
|
||||
server *http.Server
|
||||
sessionManager *session.SessionManager
|
||||
remoteManager *remote.RemoteManager
|
||||
broadcastManager *broadcast.BroadcastManager
|
||||
webRTCManager *webrtc.WebRTCManager
|
||||
webSocketHandler *websocket.WebSocketHandler
|
||||
}
|
||||
|
||||
func (neko *Neko) Preflight() {
|
||||
neko.logger = log.With().Str("service", "neko").Logger()
|
||||
}
|
||||
|
||||
func (neko *Neko) Start() {
|
||||
broadcastManager := broadcast.New(neko.Remote, neko.Broadcast)
|
||||
|
||||
remoteManager := remote.New(neko.Remote, broadcastManager)
|
||||
remoteManager.Start()
|
||||
|
||||
sessionManager := session.New(remoteManager)
|
||||
|
||||
webRTCManager := webrtc.New(sessionManager, remoteManager, neko.WebRTC)
|
||||
webRTCManager.Start()
|
||||
|
||||
webSocketHandler := websocket.New(sessionManager, remoteManager, broadcastManager, webRTCManager, neko.WebSocket)
|
||||
webSocketHandler.Start()
|
||||
|
||||
server := http.New(neko.Server, webSocketHandler)
|
||||
server.Start()
|
||||
|
||||
neko.sessionManager = sessionManager
|
||||
neko.remoteManager = remoteManager
|
||||
neko.webRTCManager = webRTCManager
|
||||
neko.webSocketHandler = webSocketHandler
|
||||
neko.server = server
|
||||
}
|
||||
|
||||
func (neko *Neko) Shutdown() {
|
||||
if err := neko.remoteManager.Shutdown(); err != nil {
|
||||
neko.logger.Err(err).Msg("remote manager shutdown with an error")
|
||||
} else {
|
||||
neko.logger.Debug().Msg("remote manager shutdown")
|
||||
}
|
||||
|
||||
if err := neko.webRTCManager.Shutdown(); err != nil {
|
||||
neko.logger.Err(err).Msg("webrtc manager shutdown with an error")
|
||||
} else {
|
||||
neko.logger.Debug().Msg("webrtc manager shutdown")
|
||||
}
|
||||
|
||||
if err := neko.webSocketHandler.Shutdown(); err != nil {
|
||||
neko.logger.Err(err).Msg("websocket handler shutdown with an error")
|
||||
} else {
|
||||
neko.logger.Debug().Msg("websocket handler shutdown")
|
||||
}
|
||||
|
||||
if err := neko.server.Shutdown(); err != nil {
|
||||
neko.logger.Err(err).Msg("server shutdown with an error")
|
||||
} else {
|
||||
neko.logger.Debug().Msg("server shutdown")
|
||||
}
|
||||
}
|
||||
|
||||
func (neko *Neko) ServeCommand(cmd *cobra.Command, args []string) {
|
||||
neko.logger.Info().Msg("starting neko server")
|
||||
neko.Start()
|
||||
neko.logger.Info().Msg("neko ready")
|
||||
|
||||
quit := make(chan os.Signal)
|
||||
signal.Notify(quit, os.Interrupt)
|
||||
sig := <-quit
|
||||
|
||||
neko.logger.Warn().Msgf("received %s, attempting graceful shutdown: \n", sig)
|
||||
neko.Shutdown()
|
||||
neko.logger.Info().Msg("shutdown complete")
|
||||
}
|
Loading…
Reference in New Issue
Block a user