mirror of
https://github.com/m1k1o/neko.git
synced 2025-05-22 13:37:11 +02:00
post megre cleanup.
This commit is contained in:
parent
3e1def9041
commit
5b96fd5f5e
284 changed files with 7103 additions and 11307 deletions
12
.gitignore
vendored
12
.gitignore
vendored
|
@ -34,19 +34,7 @@ bin
|
|||
# Environment files
|
||||
*.env
|
||||
|
||||
#
|
||||
# Neko files
|
||||
#
|
||||
|
||||
bin/
|
||||
.idea
|
||||
.env.development
|
||||
|
||||
runtime/fonts/*
|
||||
!runtime/fonts/.gitkeep
|
||||
|
||||
runtime/icon-theme/*
|
||||
!runtime/icon-theme/.gitkeep
|
||||
|
||||
plugins/*
|
||||
!plugins/.gitkeep
|
||||
|
|
87
build
87
build
|
@ -1,87 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
#
|
||||
# aborting if any command returns a non-zero value
|
||||
set -e
|
||||
|
||||
#
|
||||
# do not build plugins when passing "core" as first argument
|
||||
if [ "$1" = "core" ];
|
||||
then
|
||||
skip_plugins="true"
|
||||
fi
|
||||
|
||||
#
|
||||
# set git build variables if git exists
|
||||
if git status > /dev/null 2>&1 && [ -z $GIT_COMMIT ] && [ -z $GIT_BRANCH ] && [ -z $GIT_TAG ];
|
||||
then
|
||||
GIT_COMMIT=`git rev-parse --short HEAD`
|
||||
GIT_BRANCH=`git rev-parse --symbolic-full-name --abbrev-ref HEAD`
|
||||
GIT_TAG=`git tag --points-at $GIT_COMMIT | head -n 1`
|
||||
fi
|
||||
|
||||
#
|
||||
# load server dependencies
|
||||
go get -v -t -d .
|
||||
|
||||
#
|
||||
# build server
|
||||
go build \
|
||||
-o bin/neko \
|
||||
-ldflags "
|
||||
-s -w
|
||||
-X 'github.com/demodesk/neko.buildDate=`date -u +'%Y-%m-%dT%H:%M:%SZ'`'
|
||||
-X 'github.com/demodesk/neko.gitCommit=${GIT_COMMIT}'
|
||||
-X 'github.com/demodesk/neko.gitBranch=${GIT_BRANCH}'
|
||||
-X 'github.com/demodesk/neko.gitTag=${GIT_TAG}'
|
||||
" \
|
||||
cmd/neko/main.go;
|
||||
|
||||
#
|
||||
# ensure plugins folder exists
|
||||
mkdir -p bin/plugins
|
||||
|
||||
#
|
||||
# if plugins are ignored
|
||||
if [ "$skip_plugins" = "true" ];
|
||||
then
|
||||
echo "Not building plugins..."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
#
|
||||
# if plugins directory does not exist
|
||||
if [ ! -d "./plugins" ];
|
||||
then
|
||||
echo "No plugins directory found, skipping..."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
#
|
||||
# remove old plugins
|
||||
rm -f bin/plugins/*
|
||||
|
||||
#
|
||||
# build plugins
|
||||
for plugPath in ./plugins/*; do
|
||||
if [ ! -d $plugPath ];
|
||||
then
|
||||
continue
|
||||
fi
|
||||
|
||||
pushd $plugPath
|
||||
|
||||
echo "Building plugin: $plugPath"
|
||||
|
||||
if [ ! -f "go.plug.mod" ];
|
||||
then
|
||||
echo "go.plug.mod not found, skipping..."
|
||||
popd
|
||||
continue
|
||||
fi
|
||||
|
||||
# build plugin
|
||||
go build -modfile=go.plug.mod -buildmode=plugin -buildvcs=false -o "../../bin/plugins/${plugPath##*/}.so"
|
||||
|
||||
popd
|
||||
done
|
|
@ -1,18 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/demodesk/neko"
|
||||
"github.com/demodesk/neko/cmd"
|
||||
"github.com/demodesk/neko/pkg/utils"
|
||||
)
|
||||
|
||||
func main() {
|
||||
fmt.Print(utils.Colorf(neko.Header, "server", neko.Version))
|
||||
if err := cmd.Execute(); err != nil {
|
||||
log.Panic().Err(err).Msg("failed to execute command")
|
||||
}
|
||||
}
|
165
cmd/root.go
165
cmd/root.go
|
@ -1,165 +0,0 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/diode"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/demodesk/neko"
|
||||
"github.com/demodesk/neko/internal/config"
|
||||
)
|
||||
|
||||
func Execute() error {
|
||||
// properly log unhandled panics
|
||||
defer func() {
|
||||
panicVal := recover()
|
||||
if panicVal != nil {
|
||||
log.Panic().Msgf("%v", panicVal)
|
||||
}
|
||||
}()
|
||||
|
||||
return root.Execute()
|
||||
}
|
||||
|
||||
var root = &cobra.Command{
|
||||
Use: "neko",
|
||||
Short: "neko streaming server",
|
||||
Long: `neko streaming server`,
|
||||
Version: neko.Version.String(),
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootConfig := config.Root{}
|
||||
|
||||
cobra.OnInitialize(func() {
|
||||
//////
|
||||
// configs
|
||||
//////
|
||||
|
||||
config := viper.GetString("config") // Use config file from the flag.
|
||||
if config == "" {
|
||||
config = os.Getenv("NEKO_CONFIG") // Use config file from the environment variable.
|
||||
}
|
||||
|
||||
if config != "" {
|
||||
viper.SetConfigFile(config)
|
||||
} else {
|
||||
if runtime.GOOS == "linux" {
|
||||
viper.AddConfigPath("/etc/neko/")
|
||||
}
|
||||
|
||||
viper.AddConfigPath(".")
|
||||
viper.SetConfigName("neko")
|
||||
}
|
||||
|
||||
viper.SetEnvPrefix("NEKO")
|
||||
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
||||
viper.AutomaticEnv() // read in environment variables that match
|
||||
|
||||
// read config values
|
||||
err := viper.ReadInConfig()
|
||||
if err != nil {
|
||||
_, notFound := err.(viper.ConfigFileNotFoundError)
|
||||
if !notFound {
|
||||
log.Fatal().Err(err).Msg("unable to read config file")
|
||||
}
|
||||
}
|
||||
|
||||
// get full config file path
|
||||
config = viper.ConfigFileUsed()
|
||||
|
||||
// set root config values
|
||||
rootConfig.Set()
|
||||
|
||||
//////
|
||||
// logs
|
||||
//////
|
||||
var logWriter io.Writer
|
||||
|
||||
// log to a directory instead of stderr
|
||||
if rootConfig.LogDir != "" {
|
||||
if _, err := os.Stat(rootConfig.LogDir); os.IsNotExist(err) {
|
||||
_ = os.Mkdir(rootConfig.LogDir, os.ModePerm)
|
||||
}
|
||||
|
||||
latest := filepath.Join(rootConfig.LogDir, "neko-latest.log")
|
||||
if _, err := os.Stat(latest); err == nil {
|
||||
err = os.Rename(latest, filepath.Join(rootConfig.LogDir, "neko."+time.Now().Format("2006-01-02T15-04-05Z07-00")+".log"))
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("failed to rotate log file")
|
||||
}
|
||||
}
|
||||
|
||||
logf, err := os.OpenFile(latest, os.O_RDWR|os.O_CREATE, 0666)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("failed to open log file")
|
||||
}
|
||||
|
||||
logWriter = diode.NewWriter(logf, 1000, 10*time.Millisecond, func(missed int) {
|
||||
fmt.Printf("logger dropped %d messages", missed)
|
||||
})
|
||||
} else {
|
||||
logWriter = os.Stderr
|
||||
}
|
||||
|
||||
// log console output instead of json
|
||||
if !rootConfig.LogJson {
|
||||
logWriter = zerolog.ConsoleWriter{
|
||||
Out: logWriter,
|
||||
NoColor: rootConfig.LogNocolor,
|
||||
}
|
||||
}
|
||||
|
||||
// save new logger output
|
||||
log.Logger = log.Output(logWriter)
|
||||
|
||||
// set custom log level
|
||||
if rootConfig.LogLevel != zerolog.NoLevel {
|
||||
zerolog.SetGlobalLevel(rootConfig.LogLevel)
|
||||
}
|
||||
|
||||
// set custom log tiem format
|
||||
if rootConfig.LogTime != "" {
|
||||
zerolog.TimeFieldFormat = rootConfig.LogTime
|
||||
}
|
||||
|
||||
timeFormat := rootConfig.LogTime
|
||||
if rootConfig.LogTime == zerolog.TimeFormatUnix {
|
||||
timeFormat = "UNIX"
|
||||
}
|
||||
|
||||
logger := log.With().
|
||||
Str("config", config).
|
||||
Str("log-level", zerolog.GlobalLevel().String()).
|
||||
Bool("log-json", rootConfig.LogJson).
|
||||
Str("log-time", timeFormat).
|
||||
Str("log-dir", rootConfig.LogDir).
|
||||
Logger()
|
||||
|
||||
if config == "" {
|
||||
logger.Warn().Msg("preflight complete without config file")
|
||||
} else {
|
||||
if _, err := os.Stat(config); os.IsNotExist(err) {
|
||||
logger.Err(err).Msg("preflight complete with nonexistent config file")
|
||||
} else {
|
||||
logger.Info().Msg("preflight complete with config file")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if err := rootConfig.Init(root); err != nil {
|
||||
log.Panic().Err(err).Msg("unable to run root command")
|
||||
}
|
||||
|
||||
root.SetVersionTemplate(neko.Version.Details())
|
||||
}
|
212
cmd/serve.go
212
cmd/serve.go
|
@ -1,212 +0,0 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/signal"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/demodesk/neko/internal/api"
|
||||
"github.com/demodesk/neko/internal/capture"
|
||||
"github.com/demodesk/neko/internal/config"
|
||||
"github.com/demodesk/neko/internal/desktop"
|
||||
"github.com/demodesk/neko/internal/http"
|
||||
"github.com/demodesk/neko/internal/member"
|
||||
"github.com/demodesk/neko/internal/plugins"
|
||||
"github.com/demodesk/neko/internal/session"
|
||||
"github.com/demodesk/neko/internal/webrtc"
|
||||
"github.com/demodesk/neko/internal/websocket"
|
||||
)
|
||||
|
||||
func init() {
|
||||
service := serve{}
|
||||
|
||||
command := &cobra.Command{
|
||||
Use: "serve",
|
||||
Short: "serve neko streaming server",
|
||||
Long: `serve neko streaming server`,
|
||||
PreRun: service.PreRun,
|
||||
Run: service.Run,
|
||||
}
|
||||
|
||||
if err := service.Init(command); err != nil {
|
||||
log.Panic().Err(err).Msg("unable to initialize configuration")
|
||||
}
|
||||
|
||||
root.AddCommand(command)
|
||||
}
|
||||
|
||||
type serve struct {
|
||||
logger zerolog.Logger
|
||||
|
||||
configs struct {
|
||||
Desktop config.Desktop
|
||||
Capture config.Capture
|
||||
WebRTC config.WebRTC
|
||||
Member config.Member
|
||||
Session config.Session
|
||||
Plugins config.Plugins
|
||||
Server config.Server
|
||||
}
|
||||
|
||||
managers struct {
|
||||
desktop *desktop.DesktopManagerCtx
|
||||
capture *capture.CaptureManagerCtx
|
||||
webRTC *webrtc.WebRTCManagerCtx
|
||||
member *member.MemberManagerCtx
|
||||
session *session.SessionManagerCtx
|
||||
webSocket *websocket.WebSocketManagerCtx
|
||||
plugins *plugins.ManagerCtx
|
||||
api *api.ApiManagerCtx
|
||||
http *http.HttpManagerCtx
|
||||
}
|
||||
}
|
||||
|
||||
func (c *serve) Init(cmd *cobra.Command) error {
|
||||
if err := c.configs.Desktop.Init(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.configs.Capture.Init(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.configs.WebRTC.Init(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.configs.Member.Init(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.configs.Session.Init(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.configs.Plugins.Init(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.configs.Server.Init(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *serve) PreRun(cmd *cobra.Command, args []string) {
|
||||
c.logger = log.With().Str("service", "neko").Logger()
|
||||
|
||||
c.configs.Desktop.Set()
|
||||
c.configs.Capture.Set()
|
||||
c.configs.WebRTC.Set()
|
||||
c.configs.Member.Set()
|
||||
c.configs.Session.Set()
|
||||
c.configs.Plugins.Set()
|
||||
c.configs.Server.Set()
|
||||
}
|
||||
|
||||
func (c *serve) Start(cmd *cobra.Command) {
|
||||
c.managers.session = session.New(
|
||||
&c.configs.Session,
|
||||
)
|
||||
|
||||
c.managers.member = member.New(
|
||||
c.managers.session,
|
||||
&c.configs.Member,
|
||||
)
|
||||
|
||||
if err := c.managers.member.Connect(); err != nil {
|
||||
c.logger.Panic().Err(err).Msg("unable to connect to member manager")
|
||||
}
|
||||
|
||||
c.managers.desktop = desktop.New(
|
||||
&c.configs.Desktop,
|
||||
)
|
||||
c.managers.desktop.Start()
|
||||
|
||||
c.managers.capture = capture.New(
|
||||
c.managers.desktop,
|
||||
&c.configs.Capture,
|
||||
)
|
||||
c.managers.capture.Start()
|
||||
|
||||
c.managers.webRTC = webrtc.New(
|
||||
c.managers.desktop,
|
||||
c.managers.capture,
|
||||
&c.configs.WebRTC,
|
||||
)
|
||||
c.managers.webRTC.Start()
|
||||
|
||||
c.managers.webSocket = websocket.New(
|
||||
c.managers.session,
|
||||
c.managers.desktop,
|
||||
c.managers.capture,
|
||||
c.managers.webRTC,
|
||||
)
|
||||
c.managers.webSocket.Start()
|
||||
|
||||
c.managers.api = api.New(
|
||||
c.managers.session,
|
||||
c.managers.member,
|
||||
c.managers.desktop,
|
||||
c.managers.capture,
|
||||
)
|
||||
|
||||
c.managers.plugins = plugins.New(
|
||||
&c.configs.Plugins,
|
||||
)
|
||||
|
||||
// init and set configuration now
|
||||
// this means it won't be in --help
|
||||
c.managers.plugins.InitConfigs(cmd)
|
||||
c.managers.plugins.SetConfigs()
|
||||
|
||||
c.managers.plugins.Start(
|
||||
c.managers.session,
|
||||
c.managers.webSocket,
|
||||
c.managers.api,
|
||||
)
|
||||
|
||||
c.managers.http = http.New(
|
||||
c.managers.webSocket,
|
||||
c.managers.api,
|
||||
&c.configs.Server,
|
||||
)
|
||||
c.managers.http.Start()
|
||||
}
|
||||
|
||||
func (c *serve) Shutdown() {
|
||||
var err error
|
||||
|
||||
err = c.managers.http.Shutdown()
|
||||
c.logger.Err(err).Msg("http manager shutdown")
|
||||
|
||||
err = c.managers.plugins.Shutdown()
|
||||
c.logger.Err(err).Msg("plugins manager shutdown")
|
||||
|
||||
err = c.managers.webSocket.Shutdown()
|
||||
c.logger.Err(err).Msg("websocket manager shutdown")
|
||||
|
||||
err = c.managers.webRTC.Shutdown()
|
||||
c.logger.Err(err).Msg("webrtc manager shutdown")
|
||||
|
||||
err = c.managers.capture.Shutdown()
|
||||
c.logger.Err(err).Msg("capture manager shutdown")
|
||||
|
||||
err = c.managers.desktop.Shutdown()
|
||||
c.logger.Err(err).Msg("desktop manager shutdown")
|
||||
|
||||
err = c.managers.member.Disconnect()
|
||||
c.logger.Err(err).Msg("member manager disconnect")
|
||||
}
|
||||
|
||||
func (c *serve) Run(cmd *cobra.Command, args []string) {
|
||||
c.logger.Info().Msg("starting neko server")
|
||||
c.Start(cmd)
|
||||
c.logger.Info().Msg("neko ready")
|
||||
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, os.Interrupt)
|
||||
sig := <-quit
|
||||
|
||||
c.logger.Warn().Msgf("received %s, attempting graceful shutdown", sig)
|
||||
c.Shutdown()
|
||||
c.logger.Info().Msg("shutdown complete")
|
||||
}
|
68
go.mod
68
go.mod
|
@ -1,68 +0,0 @@
|
|||
module github.com/demodesk/neko
|
||||
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/PaesslerAG/gval v1.2.2
|
||||
github.com/go-chi/chi v1.5.5
|
||||
github.com/go-chi/cors v1.2.1
|
||||
github.com/gorilla/websocket v1.5.1
|
||||
github.com/kataras/go-events v0.0.3
|
||||
github.com/pion/ice/v2 v2.3.12
|
||||
github.com/pion/interceptor v0.1.25
|
||||
github.com/pion/logging v0.2.2
|
||||
github.com/pion/rtcp v1.2.13
|
||||
github.com/pion/webrtc/v3 v3.2.24
|
||||
github.com/prometheus/client_golang v1.18.0
|
||||
github.com/rs/zerolog v1.31.0
|
||||
github.com/spf13/cobra v1.8.0
|
||||
github.com/spf13/viper v1.18.2
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.1.1 // indirect
|
||||
github.com/pion/datachannel v1.5.5 // indirect
|
||||
github.com/pion/dtls/v2 v2.2.9 // indirect
|
||||
github.com/pion/mdns v0.0.9 // indirect
|
||||
github.com/pion/randutil v0.1.0 // indirect
|
||||
github.com/pion/rtp v1.8.3 // indirect
|
||||
github.com/pion/sctp v1.8.9 // indirect
|
||||
github.com/pion/sdp/v3 v3.0.6 // indirect
|
||||
github.com/pion/srtp/v2 v2.0.18 // indirect
|
||||
github.com/pion/stun v0.6.1 // indirect
|
||||
github.com/pion/transport/v2 v2.2.4 // indirect
|
||||
github.com/pion/turn/v2 v2.1.4 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus/client_model v0.5.0 // indirect
|
||||
github.com/prometheus/common v0.46.0 // indirect
|
||||
github.com/prometheus/procfs v0.12.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||
github.com/shopspring/decimal v1.3.1 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/afero v1.11.0 // indirect
|
||||
github.com/spf13/cast v1.6.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/stretchr/testify v1.8.4 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/crypto v0.18.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a // indirect
|
||||
golang.org/x/net v0.20.0 // indirect
|
||||
golang.org/x/sys v0.16.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
google.golang.org/protobuf v1.32.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
305
go.sum
305
go.sum
|
@ -1,305 +0,0 @@
|
|||
github.com/PaesslerAG/gval v1.2.2 h1:Y7iBzhgE09IGTt5QgGQ2IdaYYYOU134YGHBThD+wm9E=
|
||||
github.com/PaesslerAG/gval v1.2.2/go.mod h1:XRFLwvmkTEdYziLdaCeCa5ImcGVrfQbeNUbVR+C6xac=
|
||||
github.com/PaesslerAG/jsonpath v0.1.0 h1:gADYeifvlqK3R3i2cR5B4DGgxLXIPb3TRTH1mGi0jPI=
|
||||
github.com/PaesslerAG/jsonpath v0.1.0/go.mod h1:4BzmtoM/PI8fPO4aQGIusjGxGir2BzcV0grWtFzq1Y8=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/go-chi/chi v1.5.5 h1:vOB/HbEMt9QqBqErz07QehcOKHaWFtuj87tTDVz2qXE=
|
||||
github.com/go-chi/chi v1.5.5/go.mod h1:C9JqLr3tIYjDOZpzn+BCuxY8z8vmca43EeMgyZt7irw=
|
||||
github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
|
||||
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
|
||||
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
|
||||
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/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/kataras/go-events v0.0.3 h1:o5YK53uURXtrlg7qE/vovxd/yKOJcLuFtPQbf1rYMC4=
|
||||
github.com/kataras/go-events v0.0.3/go.mod h1:bFBgtzwwzrag7kQmGuU1ZaVxhK2qseYPQomXoVEMsj4=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
|
||||
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
|
||||
github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI=
|
||||
github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
||||
github.com/pion/datachannel v1.5.5 h1:10ef4kwdjije+M9d7Xm9im2Y3O6A6ccQb0zcqZcJew8=
|
||||
github.com/pion/datachannel v1.5.5/go.mod h1:iMz+lECmfdCMqFRhXhcA/219B0SQlbpoR2V118yimL0=
|
||||
github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
|
||||
github.com/pion/dtls/v2 v2.2.9 h1:K+D/aVf9/REahQvqk6G5JavdrD8W1PWDKC11UlwN7ts=
|
||||
github.com/pion/dtls/v2 v2.2.9/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
|
||||
github.com/pion/ice/v2 v2.3.11/go.mod h1:hPcLC3kxMa+JGRzMHqQzjoSj3xtE9F+eoncmXLlCL4E=
|
||||
github.com/pion/ice/v2 v2.3.12 h1:NWKW2b3+oSZS3klbQMIEWQ0i52Kuo0KBg505a5kQv4s=
|
||||
github.com/pion/ice/v2 v2.3.12/go.mod h1:hPcLC3kxMa+JGRzMHqQzjoSj3xtE9F+eoncmXLlCL4E=
|
||||
github.com/pion/interceptor v0.1.25 h1:pwY9r7P6ToQ3+IF0bajN0xmk/fNw/suTgaTdlwTDmhc=
|
||||
github.com/pion/interceptor v0.1.25/go.mod h1:wkbPYAak5zKsfpVDYMtEfWEy8D4zL+rpxCxPImLOg3Y=
|
||||
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.8/go.mod h1:hYE72WX8WDveIhg7fmXgMKivD3Puklk0Ymzog0lSyaI=
|
||||
github.com/pion/mdns v0.0.9 h1:7Ue5KZsqq8EuqStnpPWV33vYYEH0+skdDN5L7EiEsI4=
|
||||
github.com/pion/mdns v0.0.9/go.mod h1:2JA5exfxwzXiCihmxpTKgFUpiQws2MnipoPK09vecIc=
|
||||
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
||||
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
||||
github.com/pion/rtcp v1.2.10/go.mod h1:ztfEwXZNLGyF1oQDttz/ZKIBaeeg/oWbRYqzBM9TL1I=
|
||||
github.com/pion/rtcp v1.2.12/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
|
||||
github.com/pion/rtcp v1.2.13 h1:+EQijuisKwm/8VBs8nWllr0bIndR7Lf7cZG200mpbNo=
|
||||
github.com/pion/rtcp v1.2.13/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
|
||||
github.com/pion/rtp v1.8.2/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
|
||||
github.com/pion/rtp v1.8.3 h1:VEHxqzSVQxCkKDSHro5/4IUUG1ea+MFdqR2R3xSpNU8=
|
||||
github.com/pion/rtp v1.8.3/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
|
||||
github.com/pion/sctp v1.8.5/go.mod h1:SUFFfDpViyKejTAdwD1d/HQsCu+V/40cCs2nZIvC3s0=
|
||||
github.com/pion/sctp v1.8.8/go.mod h1:igF9nZBrjh5AtmKc7U30jXltsFHicFCXSmWA2GWRaWs=
|
||||
github.com/pion/sctp v1.8.9 h1:TP5ZVxV5J7rz7uZmbyvnUvsn7EJ2x/5q9uhsTtXbI3g=
|
||||
github.com/pion/sctp v1.8.9/go.mod h1:cMLT45jqw3+jiJCrtHVwfQLnfR0MGZ4rgOJwUOIqLkI=
|
||||
github.com/pion/sdp/v3 v3.0.6 h1:WuDLhtuFUUVpTfus9ILC4HRyHsW6TdugjEX/QY9OiUw=
|
||||
github.com/pion/sdp/v3 v3.0.6/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0YQw=
|
||||
github.com/pion/srtp/v2 v2.0.18 h1:vKpAXfawO9RtTRKZJbG4y0v1b11NZxQnxRl85kGuUlo=
|
||||
github.com/pion/srtp/v2 v2.0.18/go.mod h1:0KJQjA99A6/a0DOVTu1PhDSw0CXF2jTkqOoMg3ODqdA=
|
||||
github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4=
|
||||
github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8=
|
||||
github.com/pion/transport v0.14.1 h1:XSM6olwW+o8J4SCmOBb/BpwZypkHeyM0PGFCxNQBr40=
|
||||
github.com/pion/transport v0.14.1/go.mod h1:4tGmbk00NeYA3rUa9+n+dzCCoKkcy3YlYb99Jn2fNnI=
|
||||
github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g=
|
||||
github.com/pion/transport/v2 v2.2.2/go.mod h1:OJg3ojoBJopjEeECq2yJdXH9YVrUJ1uQ++NjXLOUorc=
|
||||
github.com/pion/transport/v2 v2.2.3/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
|
||||
github.com/pion/transport/v2 v2.2.4 h1:41JJK6DZQYSeVLxILA2+F4ZkKb4Xd/tFJZRFZQ9QAlo=
|
||||
github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
|
||||
github.com/pion/transport/v3 v3.0.1 h1:gDTlPJwROfSfz6QfSi0ZmeCSkFcnWWiiR9ES0ouANiM=
|
||||
github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0=
|
||||
github.com/pion/turn/v2 v2.1.3/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY=
|
||||
github.com/pion/turn/v2 v2.1.4 h1:2xn8rduI5W6sCZQkEnIUDAkrBQNl2eYIBCHMZ3QMmP8=
|
||||
github.com/pion/turn/v2 v2.1.4/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY=
|
||||
github.com/pion/webrtc/v3 v3.2.24 h1:MiFL5DMo2bDaaIFWr0DDpwiV/L4EGbLZb+xoRvfEo1Y=
|
||||
github.com/pion/webrtc/v3 v3.2.24/go.mod h1:1CaT2fcZzZ6VZA+O1i9yK2DU4EOcXVvSbWG9pr5jefs=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk=
|
||||
github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA=
|
||||
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
|
||||
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
|
||||
github.com/prometheus/common v0.46.0 h1:doXzt5ybi1HBKpsZOL0sSkaNHJJqkyfEWZGGqqScV0Y=
|
||||
github.com/prometheus/common v0.46.0/go.mod h1:Tp0qkxpb9Jsg54QMe+EAmqXkSV7Evdy1BTn+g2pa/hQ=
|
||||
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
|
||||
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A=
|
||||
github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
|
||||
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
|
||||
github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
|
||||
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
|
||||
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
|
||||
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
|
||||
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
|
||||
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
|
||||
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
|
||||
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.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=
|
||||
github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
|
||||
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
|
||||
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
|
||||
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
|
||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a h1:Q8/wZp0KX97QFTc2ywcOE0YRjZPVIx+MXInMzdvQqcA=
|
||||
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
|
||||
golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
|
||||
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
|
||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo=
|
||||
golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
|
||||
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
|
||||
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
@ -1,24 +0,0 @@
|
|||
package room
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/demodesk/neko/pkg/utils"
|
||||
)
|
||||
|
||||
func (h *RoomHandler) settingsGet(w http.ResponseWriter, r *http.Request) error {
|
||||
settings := h.sessions.Settings()
|
||||
return utils.HttpSuccess(w, settings)
|
||||
}
|
||||
|
||||
func (h *RoomHandler) settingsSet(w http.ResponseWriter, r *http.Request) error {
|
||||
settings := h.sessions.Settings()
|
||||
|
||||
if err := utils.HttpJsonRequest(w, r, &settings); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
h.sessions.UpdateSettings(settings)
|
||||
|
||||
return utils.HttpSuccess(w)
|
||||
}
|
|
@ -1,156 +0,0 @@
|
|||
package capture
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/demodesk/neko/pkg/gst"
|
||||
"github.com/demodesk/neko/pkg/types"
|
||||
)
|
||||
|
||||
type BroacastManagerCtx struct {
|
||||
logger zerolog.Logger
|
||||
mu sync.Mutex
|
||||
|
||||
pipeline gst.Pipeline
|
||||
pipelineMu sync.Mutex
|
||||
pipelineFn func(url string) (string, error)
|
||||
|
||||
url string
|
||||
started bool
|
||||
|
||||
// metrics
|
||||
pipelinesCounter prometheus.Counter
|
||||
pipelinesActive prometheus.Gauge
|
||||
}
|
||||
|
||||
func broadcastNew(pipelineFn func(url string) (string, error), defaultUrl string) *BroacastManagerCtx {
|
||||
logger := log.With().
|
||||
Str("module", "capture").
|
||||
Str("submodule", "broadcast").
|
||||
Logger()
|
||||
|
||||
return &BroacastManagerCtx{
|
||||
logger: logger,
|
||||
pipelineFn: pipelineFn,
|
||||
url: defaultUrl,
|
||||
started: defaultUrl != "",
|
||||
|
||||
// metrics
|
||||
pipelinesCounter: promauto.NewCounter(prometheus.CounterOpts{
|
||||
Name: "pipelines_total",
|
||||
Namespace: "neko",
|
||||
Subsystem: "capture",
|
||||
Help: "Total number of created pipelines.",
|
||||
ConstLabels: map[string]string{
|
||||
"submodule": "broadcast",
|
||||
"video_id": "main",
|
||||
"codec_name": "-",
|
||||
"codec_type": "-",
|
||||
},
|
||||
}),
|
||||
pipelinesActive: promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "pipelines_active",
|
||||
Namespace: "neko",
|
||||
Subsystem: "capture",
|
||||
Help: "Total number of active pipelines.",
|
||||
ConstLabels: map[string]string{
|
||||
"submodule": "broadcast",
|
||||
"video_id": "main",
|
||||
"codec_name": "-",
|
||||
"codec_type": "-",
|
||||
},
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
func (manager *BroacastManagerCtx) shutdown() {
|
||||
manager.logger.Info().Msgf("shutdown")
|
||||
|
||||
manager.destroyPipeline()
|
||||
}
|
||||
|
||||
func (manager *BroacastManagerCtx) Start(url string) error {
|
||||
manager.mu.Lock()
|
||||
defer manager.mu.Unlock()
|
||||
|
||||
err := manager.createPipeline()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
manager.url = url
|
||||
manager.started = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (manager *BroacastManagerCtx) Stop() {
|
||||
manager.mu.Lock()
|
||||
defer manager.mu.Unlock()
|
||||
|
||||
manager.started = false
|
||||
manager.destroyPipeline()
|
||||
}
|
||||
|
||||
func (manager *BroacastManagerCtx) Started() bool {
|
||||
manager.mu.Lock()
|
||||
defer manager.mu.Unlock()
|
||||
|
||||
return manager.started
|
||||
}
|
||||
|
||||
func (manager *BroacastManagerCtx) Url() string {
|
||||
manager.mu.Lock()
|
||||
defer manager.mu.Unlock()
|
||||
|
||||
return manager.url
|
||||
}
|
||||
|
||||
func (manager *BroacastManagerCtx) createPipeline() error {
|
||||
manager.pipelineMu.Lock()
|
||||
defer manager.pipelineMu.Unlock()
|
||||
|
||||
if manager.pipeline != nil {
|
||||
return types.ErrCapturePipelineAlreadyExists
|
||||
}
|
||||
|
||||
pipelineStr, err := manager.pipelineFn(manager.url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
manager.logger.Info().
|
||||
Str("url", manager.url).
|
||||
Str("src", pipelineStr).
|
||||
Msgf("starting pipeline")
|
||||
|
||||
manager.pipeline, err = gst.CreatePipeline(pipelineStr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
manager.pipeline.Play()
|
||||
manager.pipelinesCounter.Inc()
|
||||
manager.pipelinesActive.Set(1)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (manager *BroacastManagerCtx) destroyPipeline() {
|
||||
manager.pipelineMu.Lock()
|
||||
defer manager.pipelineMu.Unlock()
|
||||
|
||||
if manager.pipeline == nil {
|
||||
return
|
||||
}
|
||||
|
||||
manager.pipeline.Destroy()
|
||||
manager.logger.Info().Msgf("destroying pipeline")
|
||||
manager.pipeline = nil
|
||||
|
||||
manager.pipelinesActive.Set(0)
|
||||
}
|
|
@ -1,269 +0,0 @@
|
|||
package capture
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/demodesk/neko/internal/config"
|
||||
"github.com/demodesk/neko/pkg/types"
|
||||
"github.com/demodesk/neko/pkg/types/codec"
|
||||
)
|
||||
|
||||
type CaptureManagerCtx struct {
|
||||
logger zerolog.Logger
|
||||
desktop types.DesktopManager
|
||||
config *config.Capture
|
||||
|
||||
// sinks
|
||||
broadcast *BroacastManagerCtx
|
||||
screencast *ScreencastManagerCtx
|
||||
audio *StreamSinkManagerCtx
|
||||
video *StreamSelectorManagerCtx
|
||||
|
||||
// sources
|
||||
webcam *StreamSrcManagerCtx
|
||||
microphone *StreamSrcManagerCtx
|
||||
}
|
||||
|
||||
func New(desktop types.DesktopManager, config *config.Capture) *CaptureManagerCtx {
|
||||
logger := log.With().Str("module", "capture").Logger()
|
||||
|
||||
videos := map[string]types.StreamSinkManager{}
|
||||
for video_id, cnf := range config.VideoPipelines {
|
||||
pipelineConf := cnf
|
||||
|
||||
createPipeline := func() (string, error) {
|
||||
if pipelineConf.GstPipeline != "" {
|
||||
// replace {display} with valid display
|
||||
return strings.Replace(pipelineConf.GstPipeline, "{display}", config.Display, 1), nil
|
||||
}
|
||||
|
||||
screen := desktop.GetScreenSize()
|
||||
pipeline, err := pipelineConf.GetPipeline(screen)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return fmt.Sprintf(
|
||||
"ximagesrc display-name=%s show-pointer=false use-damage=false "+
|
||||
"%s ! appsink name=appsink", config.Display, pipeline,
|
||||
), nil
|
||||
}
|
||||
|
||||
// trigger function to catch evaluation errors at startup
|
||||
pipeline, err := createPipeline()
|
||||
if err != nil {
|
||||
logger.Panic().Err(err).
|
||||
Str("video_id", video_id).
|
||||
Msg("failed to create video pipeline")
|
||||
}
|
||||
|
||||
logger.Info().
|
||||
Str("video_id", video_id).
|
||||
Str("pipeline", pipeline).
|
||||
Msg("syntax check for video stream pipeline passed")
|
||||
|
||||
// append to videos
|
||||
videos[video_id] = streamSinkNew(config.VideoCodec, createPipeline, video_id)
|
||||
}
|
||||
|
||||
return &CaptureManagerCtx{
|
||||
logger: logger,
|
||||
desktop: desktop,
|
||||
config: config,
|
||||
|
||||
// sinks
|
||||
broadcast: broadcastNew(func(url string) (string, error) {
|
||||
if config.BroadcastPipeline != "" {
|
||||
var pipeline = config.BroadcastPipeline
|
||||
// replace {display} with valid display
|
||||
pipeline = strings.Replace(pipeline, "{display}", config.Display, 1)
|
||||
// replace {device} with valid device
|
||||
pipeline = strings.Replace(pipeline, "{device}", config.AudioDevice, 1)
|
||||
// replace {url} with valid URL
|
||||
return strings.Replace(pipeline, "{url}", url, 1), nil
|
||||
}
|
||||
|
||||
return fmt.Sprintf(
|
||||
"flvmux name=mux ! rtmpsink location='%s live=1' "+
|
||||
"pulsesrc device=%s "+
|
||||
"! audio/x-raw,channels=2 "+
|
||||
"! audioconvert "+
|
||||
"! queue "+
|
||||
"! voaacenc bitrate=%d "+
|
||||
"! mux. "+
|
||||
"ximagesrc display-name=%s show-pointer=true use-damage=false "+
|
||||
"! video/x-raw "+
|
||||
"! videoconvert "+
|
||||
"! queue "+
|
||||
"! x264enc threads=4 bitrate=%d key-int-max=15 byte-stream=true tune=zerolatency speed-preset=%s "+
|
||||
"! mux.", url, config.AudioDevice, config.BroadcastAudioBitrate*1000, config.Display, config.BroadcastVideoBitrate, config.BroadcastPreset,
|
||||
), nil
|
||||
}, config.BroadcastUrl),
|
||||
screencast: screencastNew(config.ScreencastEnabled, func() string {
|
||||
if config.ScreencastPipeline != "" {
|
||||
// replace {display} with valid display
|
||||
return strings.Replace(config.ScreencastPipeline, "{display}", config.Display, 1)
|
||||
}
|
||||
|
||||
return fmt.Sprintf(
|
||||
"ximagesrc display-name=%s show-pointer=true use-damage=false "+
|
||||
"! video/x-raw,framerate=%s "+
|
||||
"! videoconvert "+
|
||||
"! queue "+
|
||||
"! jpegenc quality=%s "+
|
||||
"! appsink name=appsink", config.Display, config.ScreencastRate, config.ScreencastQuality,
|
||||
)
|
||||
}()),
|
||||
|
||||
audio: streamSinkNew(config.AudioCodec, func() (string, error) {
|
||||
if config.AudioPipeline != "" {
|
||||
// replace {device} with valid device
|
||||
return strings.Replace(config.AudioPipeline, "{device}", config.AudioDevice, 1), nil
|
||||
}
|
||||
|
||||
return fmt.Sprintf(
|
||||
"pulsesrc device=%s "+
|
||||
"! audio/x-raw,channels=2 "+
|
||||
"! audioconvert "+
|
||||
"! queue "+
|
||||
"! %s "+
|
||||
"! appsink name=appsink", config.AudioDevice, config.AudioCodec.Pipeline,
|
||||
), nil
|
||||
}, "audio"),
|
||||
video: streamSelectorNew(config.VideoCodec, videos, config.VideoIDs),
|
||||
|
||||
// sources
|
||||
webcam: streamSrcNew(config.WebcamEnabled, map[string]string{
|
||||
codec.VP8().Name: "appsrc format=time is-live=true do-timestamp=true name=appsrc " +
|
||||
fmt.Sprintf("! application/x-rtp, payload=%d, encoding-name=VP8-DRAFT-IETF-01 ", codec.VP8().PayloadType) +
|
||||
"! rtpvp8depay " +
|
||||
"! decodebin " +
|
||||
"! videoconvert " +
|
||||
"! videorate " +
|
||||
"! videoscale " +
|
||||
fmt.Sprintf("! video/x-raw,width=%d,height=%d ", config.WebcamWidth, config.WebcamHeight) +
|
||||
"! identity drop-allocation=true " +
|
||||
fmt.Sprintf("! v4l2sink sync=false device=%s", config.WebcamDevice),
|
||||
// TODO: Test this pipeline.
|
||||
codec.VP9().Name: "appsrc format=time is-live=true do-timestamp=true name=appsrc " +
|
||||
"! application/x-rtp " +
|
||||
"! rtpvp9depay " +
|
||||
"! decodebin " +
|
||||
"! videoconvert " +
|
||||
"! videorate " +
|
||||
"! videoscale " +
|
||||
fmt.Sprintf("! video/x-raw,width=%d,height=%d ", config.WebcamWidth, config.WebcamHeight) +
|
||||
"! identity drop-allocation=true " +
|
||||
fmt.Sprintf("! v4l2sink sync=false device=%s", config.WebcamDevice),
|
||||
// TODO: Test this pipeline.
|
||||
codec.H264().Name: "appsrc format=time is-live=true do-timestamp=true name=appsrc " +
|
||||
"! application/x-rtp " +
|
||||
"! rtph264depay " +
|
||||
"! decodebin " +
|
||||
"! videoconvert " +
|
||||
"! videorate " +
|
||||
"! videoscale " +
|
||||
fmt.Sprintf("! video/x-raw,width=%d,height=%d ", config.WebcamWidth, config.WebcamHeight) +
|
||||
"! identity drop-allocation=true " +
|
||||
fmt.Sprintf("! v4l2sink sync=false device=%s", config.WebcamDevice),
|
||||
}, "webcam"),
|
||||
microphone: streamSrcNew(config.MicrophoneEnabled, map[string]string{
|
||||
codec.Opus().Name: "appsrc format=time is-live=true do-timestamp=true name=appsrc " +
|
||||
fmt.Sprintf("! application/x-rtp, payload=%d, encoding-name=OPUS ", codec.Opus().PayloadType) +
|
||||
"! rtpopusdepay " +
|
||||
"! decodebin " +
|
||||
fmt.Sprintf("! pulsesink device=%s", config.MicrophoneDevice),
|
||||
// TODO: Test this pipeline.
|
||||
codec.G722().Name: "appsrc format=time is-live=true do-timestamp=true name=appsrc " +
|
||||
"! application/x-rtp clock-rate=8000 " +
|
||||
"! rtpg722depay " +
|
||||
"! decodebin " +
|
||||
fmt.Sprintf("! pulsesink device=%s", config.MicrophoneDevice),
|
||||
}, "microphone"),
|
||||
}
|
||||
}
|
||||
|
||||
func (manager *CaptureManagerCtx) Start() {
|
||||
if manager.broadcast.Started() {
|
||||
if err := manager.broadcast.createPipeline(); err != nil {
|
||||
manager.logger.Panic().Err(err).Msg("unable to create broadcast pipeline")
|
||||
}
|
||||
}
|
||||
|
||||
manager.desktop.OnBeforeScreenSizeChange(func() {
|
||||
manager.video.destroyPipelines()
|
||||
|
||||
if manager.broadcast.Started() {
|
||||
manager.broadcast.destroyPipeline()
|
||||
}
|
||||
|
||||
if manager.screencast.Started() {
|
||||
manager.screencast.destroyPipeline()
|
||||
}
|
||||
})
|
||||
|
||||
manager.desktop.OnAfterScreenSizeChange(func() {
|
||||
err := manager.video.recreatePipelines()
|
||||
if err != nil {
|
||||
manager.logger.Panic().Err(err).Msg("unable to recreate video pipelines")
|
||||
}
|
||||
|
||||
if manager.broadcast.Started() {
|
||||
err := manager.broadcast.createPipeline()
|
||||
if err != nil && !errors.Is(err, types.ErrCapturePipelineAlreadyExists) {
|
||||
manager.logger.Panic().Err(err).Msg("unable to recreate broadcast pipeline")
|
||||
}
|
||||
}
|
||||
|
||||
if manager.screencast.Started() {
|
||||
err := manager.screencast.createPipeline()
|
||||
if err != nil && !errors.Is(err, types.ErrCapturePipelineAlreadyExists) {
|
||||
manager.logger.Panic().Err(err).Msg("unable to recreate screencast pipeline")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (manager *CaptureManagerCtx) Shutdown() error {
|
||||
manager.logger.Info().Msgf("shutdown")
|
||||
|
||||
manager.broadcast.shutdown()
|
||||
manager.screencast.shutdown()
|
||||
|
||||
manager.audio.shutdown()
|
||||
manager.video.shutdown()
|
||||
|
||||
manager.webcam.shutdown()
|
||||
manager.microphone.shutdown()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (manager *CaptureManagerCtx) Broadcast() types.BroadcastManager {
|
||||
return manager.broadcast
|
||||
}
|
||||
|
||||
func (manager *CaptureManagerCtx) Screencast() types.ScreencastManager {
|
||||
return manager.screencast
|
||||
}
|
||||
|
||||
func (manager *CaptureManagerCtx) Audio() types.StreamSinkManager {
|
||||
return manager.audio
|
||||
}
|
||||
|
||||
func (manager *CaptureManagerCtx) Video() types.StreamSelectorManager {
|
||||
return manager.video
|
||||
}
|
||||
|
||||
func (manager *CaptureManagerCtx) Webcam() types.StreamSrcManager {
|
||||
return manager.webcam
|
||||
}
|
||||
|
||||
func (manager *CaptureManagerCtx) Microphone() types.StreamSrcManager {
|
||||
return manager.microphone
|
||||
}
|
|
@ -1,414 +0,0 @@
|
|||
package capture
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/demodesk/neko/pkg/gst"
|
||||
"github.com/demodesk/neko/pkg/types"
|
||||
"github.com/demodesk/neko/pkg/types/codec"
|
||||
)
|
||||
|
||||
var moveSinkListenerMu = sync.Mutex{}
|
||||
|
||||
type StreamSinkManagerCtx struct {
|
||||
id string
|
||||
|
||||
// wait for a keyframe before sending samples
|
||||
waitForKf bool
|
||||
|
||||
bitrate uint64 // atomic
|
||||
brBuckets map[int]float64
|
||||
|
||||
logger zerolog.Logger
|
||||
mu sync.Mutex
|
||||
wg sync.WaitGroup
|
||||
|
||||
codec codec.RTPCodec
|
||||
pipeline gst.Pipeline
|
||||
pipelineMu sync.Mutex
|
||||
pipelineFn func() (string, error)
|
||||
|
||||
listeners map[uintptr]types.SampleListener
|
||||
listenersKf map[uintptr]types.SampleListener // keyframe lobby
|
||||
listenersMu sync.Mutex
|
||||
|
||||
// metrics
|
||||
currentListeners prometheus.Gauge
|
||||
totalBytes prometheus.Counter
|
||||
pipelinesCounter prometheus.Counter
|
||||
pipelinesActive prometheus.Gauge
|
||||
}
|
||||
|
||||
func streamSinkNew(codec codec.RTPCodec, pipelineFn func() (string, error), id string) *StreamSinkManagerCtx {
|
||||
logger := log.With().
|
||||
Str("module", "capture").
|
||||
Str("submodule", "stream-sink").
|
||||
Str("id", id).Logger()
|
||||
|
||||
manager := &StreamSinkManagerCtx{
|
||||
id: id,
|
||||
|
||||
// only wait for keyframes if the codec is video
|
||||
waitForKf: codec.IsVideo(),
|
||||
|
||||
bitrate: 0,
|
||||
brBuckets: map[int]float64{},
|
||||
|
||||
logger: logger,
|
||||
codec: codec,
|
||||
pipelineFn: pipelineFn,
|
||||
|
||||
listeners: map[uintptr]types.SampleListener{},
|
||||
listenersKf: map[uintptr]types.SampleListener{},
|
||||
|
||||
// metrics
|
||||
currentListeners: promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "streamsink_listeners",
|
||||
Namespace: "neko",
|
||||
Subsystem: "capture",
|
||||
Help: "Current number of listeners for a pipeline.",
|
||||
ConstLabels: map[string]string{
|
||||
"video_id": id,
|
||||
"codec_name": codec.Name,
|
||||
"codec_type": codec.Type.String(),
|
||||
},
|
||||
}),
|
||||
totalBytes: promauto.NewCounter(prometheus.CounterOpts{
|
||||
Name: "streamsink_bytes",
|
||||
Namespace: "neko",
|
||||
Subsystem: "capture",
|
||||
Help: "Total number of bytes created by the pipeline.",
|
||||
ConstLabels: map[string]string{
|
||||
"video_id": id,
|
||||
"codec_name": codec.Name,
|
||||
"codec_type": codec.Type.String(),
|
||||
},
|
||||
}),
|
||||
pipelinesCounter: promauto.NewCounter(prometheus.CounterOpts{
|
||||
Name: "pipelines_total",
|
||||
Namespace: "neko",
|
||||
Subsystem: "capture",
|
||||
Help: "Total number of created pipelines.",
|
||||
ConstLabels: map[string]string{
|
||||
"submodule": "streamsink",
|
||||
"video_id": id,
|
||||
"codec_name": codec.Name,
|
||||
"codec_type": codec.Type.String(),
|
||||
},
|
||||
}),
|
||||
pipelinesActive: promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "pipelines_active",
|
||||
Namespace: "neko",
|
||||
Subsystem: "capture",
|
||||
Help: "Total number of active pipelines.",
|
||||
ConstLabels: map[string]string{
|
||||
"submodule": "streamsink",
|
||||
"video_id": id,
|
||||
"codec_name": codec.Name,
|
||||
"codec_type": codec.Type.String(),
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
return manager
|
||||
}
|
||||
|
||||
func (manager *StreamSinkManagerCtx) shutdown() {
|
||||
manager.logger.Info().Msgf("shutdown")
|
||||
|
||||
manager.listenersMu.Lock()
|
||||
for key := range manager.listeners {
|
||||
delete(manager.listeners, key)
|
||||
}
|
||||
for key := range manager.listenersKf {
|
||||
delete(manager.listenersKf, key)
|
||||
}
|
||||
manager.listenersMu.Unlock()
|
||||
|
||||
manager.DestroyPipeline()
|
||||
manager.wg.Wait()
|
||||
}
|
||||
|
||||
func (manager *StreamSinkManagerCtx) ID() string {
|
||||
return manager.id
|
||||
}
|
||||
|
||||
func (manager *StreamSinkManagerCtx) Bitrate() uint64 {
|
||||
return atomic.LoadUint64(&manager.bitrate)
|
||||
}
|
||||
|
||||
func (manager *StreamSinkManagerCtx) Codec() codec.RTPCodec {
|
||||
return manager.codec
|
||||
}
|
||||
|
||||
func (manager *StreamSinkManagerCtx) start() error {
|
||||
if len(manager.listeners)+len(manager.listenersKf) == 0 {
|
||||
err := manager.CreatePipeline()
|
||||
if err != nil && !errors.Is(err, types.ErrCapturePipelineAlreadyExists) {
|
||||
return err
|
||||
}
|
||||
|
||||
manager.logger.Info().Msgf("first listener, starting")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (manager *StreamSinkManagerCtx) stop() {
|
||||
if len(manager.listeners)+len(manager.listenersKf) == 0 {
|
||||
manager.DestroyPipeline()
|
||||
manager.logger.Info().Msgf("last listener, stopping")
|
||||
}
|
||||
}
|
||||
|
||||
func (manager *StreamSinkManagerCtx) addListener(listener types.SampleListener) {
|
||||
ptr := reflect.ValueOf(listener).Pointer()
|
||||
emitKeyframe := false
|
||||
|
||||
manager.listenersMu.Lock()
|
||||
if manager.waitForKf {
|
||||
// if this is the first listener, we need to emit a keyframe
|
||||
emitKeyframe = len(manager.listenersKf) == 0
|
||||
// if we're waiting for a keyframe, add it to the keyframe lobby
|
||||
manager.listenersKf[ptr] = listener
|
||||
} else {
|
||||
// otherwise, add it as a regular listener
|
||||
manager.listeners[ptr] = listener
|
||||
}
|
||||
manager.listenersMu.Unlock()
|
||||
|
||||
manager.logger.Debug().Interface("ptr", ptr).Msgf("adding listener")
|
||||
manager.currentListeners.Set(float64(manager.ListenersCount()))
|
||||
|
||||
// if we will be waiting for a keyframe, emit one now
|
||||
if manager.pipeline != nil && emitKeyframe {
|
||||
manager.pipeline.EmitVideoKeyframe()
|
||||
}
|
||||
}
|
||||
|
||||
func (manager *StreamSinkManagerCtx) removeListener(listener types.SampleListener) {
|
||||
ptr := reflect.ValueOf(listener).Pointer()
|
||||
|
||||
manager.listenersMu.Lock()
|
||||
delete(manager.listeners, ptr)
|
||||
delete(manager.listenersKf, ptr) // if it's a keyframe listener, remove it too
|
||||
manager.listenersMu.Unlock()
|
||||
|
||||
manager.logger.Debug().Interface("ptr", ptr).Msgf("removing listener")
|
||||
manager.currentListeners.Set(float64(manager.ListenersCount()))
|
||||
}
|
||||
|
||||
func (manager *StreamSinkManagerCtx) AddListener(listener types.SampleListener) error {
|
||||
manager.mu.Lock()
|
||||
defer manager.mu.Unlock()
|
||||
|
||||
if listener == nil {
|
||||
return errors.New("listener cannot be nil")
|
||||
}
|
||||
|
||||
// start if stopped
|
||||
if err := manager.start(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// add listener
|
||||
manager.addListener(listener)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (manager *StreamSinkManagerCtx) RemoveListener(listener types.SampleListener) error {
|
||||
manager.mu.Lock()
|
||||
defer manager.mu.Unlock()
|
||||
|
||||
if listener == nil {
|
||||
return errors.New("listener cannot be nil")
|
||||
}
|
||||
|
||||
// remove listener
|
||||
manager.removeListener(listener)
|
||||
|
||||
// stop if started
|
||||
manager.stop()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// moving listeners between streams ensures, that target pipeline is running
|
||||
// before listener is added, and stops source pipeline if there are 0 listeners
|
||||
func (manager *StreamSinkManagerCtx) MoveListenerTo(listener types.SampleListener, stream types.StreamSinkManager) error {
|
||||
if listener == nil {
|
||||
return errors.New("listener cannot be nil")
|
||||
}
|
||||
|
||||
targetStream, ok := stream.(*StreamSinkManagerCtx)
|
||||
if !ok {
|
||||
return errors.New("target stream manager does not support moving listeners")
|
||||
}
|
||||
|
||||
// we need to acquire both mutextes, from source stream and from target stream
|
||||
// in order to do that safely (without possibility of deadlock) we need third
|
||||
// global mutex, that ensures atomic locking
|
||||
|
||||
// lock global mutex
|
||||
moveSinkListenerMu.Lock()
|
||||
|
||||
// lock source stream
|
||||
manager.mu.Lock()
|
||||
defer manager.mu.Unlock()
|
||||
|
||||
// lock target stream
|
||||
targetStream.mu.Lock()
|
||||
defer targetStream.mu.Unlock()
|
||||
|
||||
// unlock global mutex
|
||||
moveSinkListenerMu.Unlock()
|
||||
|
||||
// start if stopped
|
||||
if err := targetStream.start(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// swap listeners
|
||||
manager.removeListener(listener)
|
||||
targetStream.addListener(listener)
|
||||
|
||||
// stop if started
|
||||
manager.stop()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (manager *StreamSinkManagerCtx) ListenersCount() int {
|
||||
manager.listenersMu.Lock()
|
||||
defer manager.listenersMu.Unlock()
|
||||
|
||||
return len(manager.listeners) + len(manager.listenersKf)
|
||||
}
|
||||
|
||||
func (manager *StreamSinkManagerCtx) Started() bool {
|
||||
return manager.ListenersCount() > 0
|
||||
}
|
||||
|
||||
func (manager *StreamSinkManagerCtx) CreatePipeline() error {
|
||||
manager.pipelineMu.Lock()
|
||||
defer manager.pipelineMu.Unlock()
|
||||
|
||||
if manager.pipeline != nil {
|
||||
return types.ErrCapturePipelineAlreadyExists
|
||||
}
|
||||
|
||||
pipelineStr, err := manager.pipelineFn()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
manager.logger.Info().
|
||||
Str("codec", manager.codec.Name).
|
||||
Str("src", pipelineStr).
|
||||
Msgf("creating pipeline")
|
||||
|
||||
manager.pipeline, err = gst.CreatePipeline(pipelineStr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
manager.pipeline.AttachAppsink("appsink")
|
||||
manager.pipeline.Play()
|
||||
|
||||
manager.wg.Add(1)
|
||||
pipeline := manager.pipeline
|
||||
|
||||
go func() {
|
||||
manager.logger.Debug().Msg("started emitting samples")
|
||||
defer manager.wg.Done()
|
||||
|
||||
for {
|
||||
sample, ok := <-pipeline.Sample()
|
||||
if !ok {
|
||||
manager.logger.Debug().Msg("stopped emitting samples")
|
||||
return
|
||||
}
|
||||
|
||||
manager.onSample(sample)
|
||||
}
|
||||
}()
|
||||
|
||||
manager.pipelinesCounter.Inc()
|
||||
manager.pipelinesActive.Set(1)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (manager *StreamSinkManagerCtx) saveSampleBitrate(timestamp time.Time, delta float64) {
|
||||
// get unix timestamp in seconds
|
||||
sec := timestamp.Unix()
|
||||
// last bucket is timestamp rounded to 3 seconds - 1 second
|
||||
last := int((sec - 1) % 3)
|
||||
// current bucket is timestamp rounded to 3 seconds
|
||||
curr := int(sec % 3)
|
||||
// next bucket is timestamp rounded to 3 seconds + 1 second
|
||||
next := int((sec + 1) % 3)
|
||||
|
||||
if manager.brBuckets[next] != 0 {
|
||||
// atomic update bitrate
|
||||
atomic.StoreUint64(&manager.bitrate, uint64(manager.brBuckets[last]))
|
||||
// empty next bucket
|
||||
manager.brBuckets[next] = 0
|
||||
}
|
||||
|
||||
// add rate to current bucket
|
||||
manager.brBuckets[curr] += delta
|
||||
}
|
||||
|
||||
func (manager *StreamSinkManagerCtx) onSample(sample types.Sample) {
|
||||
manager.listenersMu.Lock()
|
||||
defer manager.listenersMu.Unlock()
|
||||
|
||||
// save to metrics
|
||||
length := float64(sample.Length)
|
||||
manager.totalBytes.Add(length)
|
||||
manager.saveSampleBitrate(sample.Timestamp, length)
|
||||
|
||||
// if is not delta unit -> it can be decoded independently -> it is a keyframe
|
||||
if manager.waitForKf && !sample.DeltaUnit && len(manager.listenersKf) > 0 {
|
||||
// if current sample is a keyframe, move listeners from
|
||||
// keyframe lobby to actual listeners map and clear lobby
|
||||
for k, v := range manager.listenersKf {
|
||||
manager.listeners[k] = v
|
||||
}
|
||||
manager.listenersKf = make(map[uintptr]types.SampleListener)
|
||||
}
|
||||
|
||||
for _, l := range manager.listeners {
|
||||
l.WriteSample(sample)
|
||||
}
|
||||
}
|
||||
|
||||
func (manager *StreamSinkManagerCtx) DestroyPipeline() {
|
||||
manager.pipelineMu.Lock()
|
||||
defer manager.pipelineMu.Unlock()
|
||||
|
||||
if manager.pipeline == nil {
|
||||
return
|
||||
}
|
||||
|
||||
manager.pipeline.Destroy()
|
||||
manager.logger.Info().Msgf("destroying pipeline")
|
||||
manager.pipeline = nil
|
||||
|
||||
manager.pipelinesActive.Set(0)
|
||||
|
||||
manager.brBuckets = make(map[int]float64)
|
||||
atomic.StoreUint64(&manager.bitrate, 0)
|
||||
}
|
|
@ -1,246 +0,0 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/demodesk/neko/pkg/types"
|
||||
"github.com/demodesk/neko/pkg/types/codec"
|
||||
"github.com/demodesk/neko/pkg/utils"
|
||||
)
|
||||
|
||||
type Capture struct {
|
||||
Display string
|
||||
|
||||
VideoCodec codec.RTPCodec
|
||||
VideoIDs []string
|
||||
VideoPipelines map[string]types.VideoConfig
|
||||
|
||||
AudioDevice string
|
||||
AudioCodec codec.RTPCodec
|
||||
AudioPipeline string
|
||||
|
||||
BroadcastAudioBitrate int
|
||||
BroadcastVideoBitrate int
|
||||
BroadcastPreset string
|
||||
BroadcastPipeline string
|
||||
BroadcastUrl string
|
||||
|
||||
ScreencastEnabled bool
|
||||
ScreencastRate string
|
||||
ScreencastQuality string
|
||||
ScreencastPipeline string
|
||||
|
||||
WebcamEnabled bool
|
||||
WebcamDevice string
|
||||
WebcamWidth int
|
||||
WebcamHeight int
|
||||
|
||||
MicrophoneEnabled bool
|
||||
MicrophoneDevice string
|
||||
}
|
||||
|
||||
func (Capture) Init(cmd *cobra.Command) error {
|
||||
// audio
|
||||
cmd.PersistentFlags().String("capture.audio.device", "audio_output.monitor", "pulseaudio device to capture")
|
||||
if err := viper.BindPFlag("capture.audio.device", cmd.PersistentFlags().Lookup("capture.audio.device")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("capture.audio.codec", "opus", "audio codec to be used")
|
||||
if err := viper.BindPFlag("capture.audio.codec", cmd.PersistentFlags().Lookup("capture.audio.codec")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("capture.audio.pipeline", "", "gstreamer pipeline used for audio streaming")
|
||||
if err := viper.BindPFlag("capture.audio.pipeline", cmd.PersistentFlags().Lookup("capture.audio.pipeline")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// videos
|
||||
cmd.PersistentFlags().String("capture.video.codec", "vp8", "video codec to be used")
|
||||
if err := viper.BindPFlag("capture.video.codec", cmd.PersistentFlags().Lookup("capture.video.codec")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().StringSlice("capture.video.ids", []string{}, "ordered list of video ids")
|
||||
if err := viper.BindPFlag("capture.video.ids", cmd.PersistentFlags().Lookup("capture.video.ids")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("capture.video.pipelines", "[]", "pipelines config in JSON used for video streaming")
|
||||
if err := viper.BindPFlag("capture.video.pipelines", cmd.PersistentFlags().Lookup("capture.video.pipelines")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// broadcast
|
||||
cmd.PersistentFlags().Int("capture.broadcast.audio_bitrate", 128, "broadcast audio bitrate in KB/s")
|
||||
if err := viper.BindPFlag("capture.broadcast.audio_bitrate", cmd.PersistentFlags().Lookup("capture.broadcast.audio_bitrate")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Int("capture.broadcast.video_bitrate", 4096, "broadcast video bitrate in KB/s")
|
||||
if err := viper.BindPFlag("capture.broadcast.video_bitrate", cmd.PersistentFlags().Lookup("capture.broadcast.video_bitrate")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("capture.broadcast.preset", "veryfast", "broadcast speed preset for h264 encoding")
|
||||
if err := viper.BindPFlag("capture.broadcast.preset", cmd.PersistentFlags().Lookup("capture.broadcast.preset")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("capture.broadcast.pipeline", "", "gstreamer pipeline used for broadcasting")
|
||||
if err := viper.BindPFlag("capture.broadcast.pipeline", cmd.PersistentFlags().Lookup("capture.broadcast.pipeline")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("capture.broadcast.url", "", "initial URL for broadcasting, setting this value will automatically start broadcasting")
|
||||
if err := viper.BindPFlag("capture.broadcast.url", cmd.PersistentFlags().Lookup("capture.broadcast.url")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// screencast
|
||||
cmd.PersistentFlags().Bool("capture.screencast.enabled", false, "enable screencast")
|
||||
if err := viper.BindPFlag("capture.screencast.enabled", cmd.PersistentFlags().Lookup("capture.screencast.enabled")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("capture.screencast.rate", "10/1", "screencast frame rate")
|
||||
if err := viper.BindPFlag("capture.screencast.rate", cmd.PersistentFlags().Lookup("capture.screencast.rate")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("capture.screencast.quality", "60", "screencast JPEG quality")
|
||||
if err := viper.BindPFlag("capture.screencast.quality", cmd.PersistentFlags().Lookup("capture.screencast.quality")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("capture.screencast.pipeline", "", "gstreamer pipeline used for screencasting")
|
||||
if err := viper.BindPFlag("capture.screencast.pipeline", cmd.PersistentFlags().Lookup("capture.screencast.pipeline")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// webcam
|
||||
cmd.PersistentFlags().Bool("capture.webcam.enabled", false, "enable webcam stream")
|
||||
if err := viper.BindPFlag("capture.webcam.enabled", cmd.PersistentFlags().Lookup("capture.webcam.enabled")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// sudo apt install v4l2loopback-dkms v4l2loopback-utils
|
||||
// sudo apt-get install linux-headers-`uname -r` linux-modules-extra-`uname -r`
|
||||
// sudo modprobe v4l2loopback exclusive_caps=1
|
||||
cmd.PersistentFlags().String("capture.webcam.device", "/dev/video0", "v4l2sink device used for webcam")
|
||||
if err := viper.BindPFlag("capture.webcam.device", cmd.PersistentFlags().Lookup("capture.webcam.device")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Int("capture.webcam.width", 1280, "webcam stream width")
|
||||
if err := viper.BindPFlag("capture.webcam.width", cmd.PersistentFlags().Lookup("capture.webcam.width")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Int("capture.webcam.height", 720, "webcam stream height")
|
||||
if err := viper.BindPFlag("capture.webcam.height", cmd.PersistentFlags().Lookup("capture.webcam.height")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// microphone
|
||||
cmd.PersistentFlags().Bool("capture.microphone.enabled", true, "enable microphone stream")
|
||||
if err := viper.BindPFlag("capture.microphone.enabled", cmd.PersistentFlags().Lookup("capture.microphone.enabled")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("capture.microphone.device", "audio_input", "pulseaudio device used for microphone")
|
||||
if err := viper.BindPFlag("capture.microphone.device", cmd.PersistentFlags().Lookup("capture.microphone.device")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Capture) Set() {
|
||||
var ok bool
|
||||
|
||||
// Display is provided by env variable
|
||||
s.Display = os.Getenv("DISPLAY")
|
||||
|
||||
// video
|
||||
videoCodec := viper.GetString("capture.video.codec")
|
||||
s.VideoCodec, ok = codec.ParseStr(videoCodec)
|
||||
if !ok || !s.VideoCodec.IsVideo() {
|
||||
log.Warn().Str("codec", videoCodec).Msgf("unknown video codec, using Vp8")
|
||||
s.VideoCodec = codec.VP8()
|
||||
}
|
||||
|
||||
s.VideoIDs = viper.GetStringSlice("capture.video.ids")
|
||||
if err := viper.UnmarshalKey("capture.video.pipelines", &s.VideoPipelines, viper.DecodeHook(
|
||||
utils.JsonStringAutoDecode(s.VideoPipelines),
|
||||
)); err != nil {
|
||||
log.Warn().Err(err).Msgf("unable to parse video pipelines")
|
||||
}
|
||||
|
||||
// default video
|
||||
if len(s.VideoPipelines) == 0 {
|
||||
log.Warn().Msgf("no video pipelines specified, using defaults")
|
||||
|
||||
s.VideoCodec = codec.VP8()
|
||||
s.VideoPipelines = map[string]types.VideoConfig{
|
||||
"main": {
|
||||
Fps: "25",
|
||||
GstEncoder: "vp8enc",
|
||||
GstParams: map[string]string{
|
||||
"target-bitrate": "round(3072 * 650)",
|
||||
"cpu-used": "4",
|
||||
"end-usage": "cbr",
|
||||
"threads": "4",
|
||||
"deadline": "1",
|
||||
"undershoot": "95",
|
||||
"buffer-size": "(3072 * 4)",
|
||||
"buffer-initial-size": "(3072 * 2)",
|
||||
"buffer-optimal-size": "(3072 * 3)",
|
||||
"keyframe-max-dist": "25",
|
||||
"min-quantizer": "4",
|
||||
"max-quantizer": "20",
|
||||
},
|
||||
},
|
||||
}
|
||||
s.VideoIDs = []string{"main"}
|
||||
}
|
||||
|
||||
// audio
|
||||
s.AudioDevice = viper.GetString("capture.audio.device")
|
||||
s.AudioPipeline = viper.GetString("capture.audio.pipeline")
|
||||
|
||||
audioCodec := viper.GetString("capture.audio.codec")
|
||||
s.AudioCodec, ok = codec.ParseStr(audioCodec)
|
||||
if !ok || !s.AudioCodec.IsAudio() {
|
||||
log.Warn().Str("codec", audioCodec).Msgf("unknown audio codec, using Opus")
|
||||
s.AudioCodec = codec.Opus()
|
||||
}
|
||||
|
||||
// broadcast
|
||||
s.BroadcastAudioBitrate = viper.GetInt("capture.broadcast.audio_bitrate")
|
||||
s.BroadcastVideoBitrate = viper.GetInt("capture.broadcast.video_bitrate")
|
||||
s.BroadcastPreset = viper.GetString("capture.broadcast.preset")
|
||||
s.BroadcastPipeline = viper.GetString("capture.broadcast.pipeline")
|
||||
s.BroadcastUrl = viper.GetString("capture.broadcast.url")
|
||||
|
||||
// screencast
|
||||
s.ScreencastEnabled = viper.GetBool("capture.screencast.enabled")
|
||||
s.ScreencastRate = viper.GetString("capture.screencast.rate")
|
||||
s.ScreencastQuality = viper.GetString("capture.screencast.quality")
|
||||
s.ScreencastPipeline = viper.GetString("capture.screencast.pipeline")
|
||||
|
||||
// webcam
|
||||
s.WebcamEnabled = viper.GetBool("capture.webcam.enabled")
|
||||
s.WebcamDevice = viper.GetString("capture.webcam.device")
|
||||
s.WebcamWidth = viper.GetInt("capture.webcam.width")
|
||||
s.WebcamHeight = viper.GetInt("capture.webcam.height")
|
||||
|
||||
// microphone
|
||||
s.MicrophoneEnabled = viper.GetBool("capture.microphone.enabled")
|
||||
s.MicrophoneDevice = viper.GetString("capture.microphone.device")
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
package config
|
||||
|
||||
import "github.com/spf13/cobra"
|
||||
|
||||
type Config interface {
|
||||
Init(cmd *cobra.Command) error
|
||||
Set()
|
||||
}
|
|
@ -1,91 +0,0 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/demodesk/neko/pkg/types"
|
||||
)
|
||||
|
||||
type Desktop struct {
|
||||
Display string
|
||||
|
||||
ScreenSize types.ScreenSize
|
||||
|
||||
UseInputDriver bool
|
||||
InputSocket string
|
||||
|
||||
Unminimize bool
|
||||
UploadDrop bool
|
||||
FileChooserDialog bool
|
||||
}
|
||||
|
||||
func (Desktop) Init(cmd *cobra.Command) error {
|
||||
cmd.PersistentFlags().String("desktop.screen", "1280x720@30", "default screen size and framerate")
|
||||
if err := viper.BindPFlag("desktop.screen", cmd.PersistentFlags().Lookup("desktop.screen")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Bool("desktop.input.enabled", true, "whether custom xf86 input driver should be used to handle touchscreen")
|
||||
if err := viper.BindPFlag("desktop.input.enabled", cmd.PersistentFlags().Lookup("desktop.input.enabled")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("desktop.input.socket", "/tmp/xf86-input-neko.sock", "socket path for custom xf86 input driver connection")
|
||||
if err := viper.BindPFlag("desktop.input.socket", cmd.PersistentFlags().Lookup("desktop.input.socket")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Bool("desktop.unminimize", true, "automatically unminimize window when it is minimized")
|
||||
if err := viper.BindPFlag("desktop.unminimize", cmd.PersistentFlags().Lookup("desktop.unminimize")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Bool("desktop.upload_drop", true, "whether drop upload is enabled")
|
||||
if err := viper.BindPFlag("desktop.upload_drop", cmd.PersistentFlags().Lookup("desktop.upload_drop")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Bool("desktop.file_chooser_dialog", false, "whether to handle file chooser dialog externally")
|
||||
if err := viper.BindPFlag("desktop.file_chooser_dialog", cmd.PersistentFlags().Lookup("desktop.file_chooser_dialog")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Desktop) Set() {
|
||||
// Display is provided by env variable
|
||||
s.Display = os.Getenv("DISPLAY")
|
||||
|
||||
s.ScreenSize = types.ScreenSize{
|
||||
Width: 1280,
|
||||
Height: 720,
|
||||
Rate: 30,
|
||||
}
|
||||
|
||||
r := regexp.MustCompile(`([0-9]{1,4})x([0-9]{1,4})@([0-9]{1,3})`)
|
||||
res := r.FindStringSubmatch(viper.GetString("desktop.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.ScreenSize.Width = int(width)
|
||||
s.ScreenSize.Height = int(height)
|
||||
s.ScreenSize.Rate = int16(rate)
|
||||
}
|
||||
}
|
||||
|
||||
s.UseInputDriver = viper.GetBool("desktop.input.enabled")
|
||||
s.InputSocket = viper.GetString("desktop.input.socket")
|
||||
s.Unminimize = viper.GetBool("desktop.unminimize")
|
||||
s.UploadDrop = viper.GetBool("desktop.upload_drop")
|
||||
s.FileChooserDialog = viper.GetBool("desktop.file_chooser_dialog")
|
||||
}
|
|
@ -1,97 +0,0 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type Root struct {
|
||||
Config string
|
||||
|
||||
LogLevel zerolog.Level
|
||||
LogTime string
|
||||
LogJson bool
|
||||
LogNocolor bool
|
||||
LogDir string
|
||||
}
|
||||
|
||||
func (Root) Init(cmd *cobra.Command) error {
|
||||
cmd.PersistentFlags().StringP("config", "c", "", "configuration file path")
|
||||
if err := viper.BindPFlag("config", cmd.PersistentFlags().Lookup("config")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// just a shortcut
|
||||
cmd.PersistentFlags().BoolP("debug", "d", false, "enable debug mode")
|
||||
if err := viper.BindPFlag("debug", cmd.PersistentFlags().Lookup("debug")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("log.level", "info", "set log level (trace, debug, info, warn, error, fatal, panic, disabled)")
|
||||
if err := viper.BindPFlag("log.level", cmd.PersistentFlags().Lookup("log.level")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("log.time", "unix", "time format used in logs (unix, unixms, unixmicro)")
|
||||
if err := viper.BindPFlag("log.time", cmd.PersistentFlags().Lookup("log.time")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Bool("log.json", false, "logs in JSON format")
|
||||
if err := viper.BindPFlag("log.json", cmd.PersistentFlags().Lookup("log.json")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Bool("log.nocolor", false, "no ANSI colors in non-JSON output")
|
||||
if err := viper.BindPFlag("log.nocolor", cmd.PersistentFlags().Lookup("log.nocolor")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("log.dir", "", "logging directory to store logs")
|
||||
if err := viper.BindPFlag("log.dir", cmd.PersistentFlags().Lookup("log.dir")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Root) Set() {
|
||||
s.Config = viper.GetString("config")
|
||||
|
||||
logLevel := viper.GetString("log.level")
|
||||
level, err := zerolog.ParseLevel(logLevel)
|
||||
if err != nil {
|
||||
log.Warn().Msgf("unknown log level %s", logLevel)
|
||||
} else {
|
||||
s.LogLevel = level
|
||||
}
|
||||
|
||||
logTime := viper.GetString("log.time")
|
||||
switch logTime {
|
||||
case "unix":
|
||||
s.LogTime = zerolog.TimeFormatUnix
|
||||
case "unixms":
|
||||
s.LogTime = zerolog.TimeFormatUnixMs
|
||||
case "unixmicro":
|
||||
s.LogTime = zerolog.TimeFormatUnixMicro
|
||||
default:
|
||||
log.Warn().Msgf("unknown log time %s", logTime)
|
||||
}
|
||||
|
||||
s.LogJson = viper.GetBool("log.json")
|
||||
s.LogNocolor = viper.GetBool("log.nocolor")
|
||||
s.LogDir = viper.GetString("log.dir")
|
||||
|
||||
if viper.GetBool("debug") && s.LogLevel != zerolog.TraceLevel {
|
||||
s.LogLevel = zerolog.DebugLevel
|
||||
}
|
||||
|
||||
// support for NO_COLOR env variable: https://no-color.org/
|
||||
if os.Getenv("NO_COLOR") != "" {
|
||||
s.LogNocolor = true
|
||||
}
|
||||
}
|
|
@ -1,103 +0,0 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"path"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/demodesk/neko/pkg/utils"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
Cert string
|
||||
Key string
|
||||
Bind string
|
||||
Proxy bool
|
||||
Static string
|
||||
PathPrefix string
|
||||
PProf bool
|
||||
Metrics bool
|
||||
CORS []string
|
||||
}
|
||||
|
||||
func (Server) Init(cmd *cobra.Command) error {
|
||||
cmd.PersistentFlags().String("server.bind", "127.0.0.1:8080", "address/port/socket to serve neko")
|
||||
if err := viper.BindPFlag("server.bind", cmd.PersistentFlags().Lookup("server.bind")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("server.cert", "", "path to the SSL cert used to secure the neko server")
|
||||
if err := viper.BindPFlag("server.cert", cmd.PersistentFlags().Lookup("server.cert")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("server.key", "", "path to the SSL key used to secure the neko server")
|
||||
if err := viper.BindPFlag("server.key", cmd.PersistentFlags().Lookup("server.key")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Bool("server.proxy", false, "trust reverse proxy headers")
|
||||
if err := viper.BindPFlag("server.proxy", cmd.PersistentFlags().Lookup("server.proxy")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("server.static", "", "path to neko client files to serve")
|
||||
if err := viper.BindPFlag("server.static", cmd.PersistentFlags().Lookup("server.static")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("server.path_prefix", "/", "path prefix for HTTP requests")
|
||||
if err := viper.BindPFlag("server.path_prefix", cmd.PersistentFlags().Lookup("server.path_prefix")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Bool("server.pprof", false, "enable pprof endpoint available at /debug/pprof")
|
||||
if err := viper.BindPFlag("server.pprof", cmd.PersistentFlags().Lookup("server.pprof")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Bool("server.metrics", true, "enable prometheus metrics available at /metrics")
|
||||
if err := viper.BindPFlag("server.metrics", cmd.PersistentFlags().Lookup("server.metrics")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().StringSlice("server.cors", []string{}, "list of allowed origins for CORS, if empty CORS is disabled, if '*' is present all origins are allowed")
|
||||
if err := viper.BindPFlag("server.cors", cmd.PersistentFlags().Lookup("server.cors")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) Set() {
|
||||
s.Cert = viper.GetString("server.cert")
|
||||
s.Key = viper.GetString("server.key")
|
||||
s.Bind = viper.GetString("server.bind")
|
||||
s.Proxy = viper.GetBool("server.proxy")
|
||||
s.Static = viper.GetString("server.static")
|
||||
s.PathPrefix = path.Join("/", path.Clean(viper.GetString("server.path_prefix")))
|
||||
s.PProf = viper.GetBool("server.pprof")
|
||||
s.Metrics = viper.GetBool("server.metrics")
|
||||
|
||||
s.CORS = viper.GetStringSlice("server.cors")
|
||||
in, _ := utils.ArrayIn("*", s.CORS)
|
||||
if len(s.CORS) == 0 || in {
|
||||
s.CORS = []string{"*"}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) HasCors() bool {
|
||||
return len(s.CORS) > 0
|
||||
}
|
||||
|
||||
func (s *Server) AllowOrigin(origin string) bool {
|
||||
// if CORS is disabled, allow all origins
|
||||
if len(s.CORS) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
// if CORS is enabled, allow only origins in the list
|
||||
in, _ := utils.ArrayIn(origin, s.CORS)
|
||||
return in || s.CORS[0] == "*"
|
||||
}
|
|
@ -1,273 +0,0 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/demodesk/neko/pkg/types"
|
||||
"github.com/demodesk/neko/pkg/utils"
|
||||
)
|
||||
|
||||
// default stun server
|
||||
const defStunSrv = "stun:stun.l.google.com:19302"
|
||||
|
||||
type WebRTCEstimator struct {
|
||||
Enabled bool
|
||||
Passive bool
|
||||
Debug bool
|
||||
InitialBitrate int
|
||||
|
||||
// how often to read and process bandwidth estimation reports
|
||||
ReadInterval time.Duration
|
||||
// how long to wait for stable connection (only neutral or upward trend) before upgrading
|
||||
StableDuration time.Duration
|
||||
// how long to wait for unstable connection (downward trend) before downgrading
|
||||
UnstableDuration time.Duration
|
||||
// how long to wait for stalled connection (neutral trend with low bandwidth) before downgrading
|
||||
StalledDuration time.Duration
|
||||
// how long to wait before downgrading again after previous downgrade
|
||||
DowngradeBackoff time.Duration
|
||||
// how long to wait before upgrading again after previous upgrade
|
||||
UpgradeBackoff time.Duration
|
||||
// how bigger the difference between estimated and stream bitrate must be to trigger upgrade/downgrade
|
||||
DiffThreshold float64
|
||||
}
|
||||
|
||||
type WebRTC struct {
|
||||
ICELite bool
|
||||
ICETrickle bool
|
||||
ICEServersFrontend []types.ICEServer
|
||||
ICEServersBackend []types.ICEServer
|
||||
EphemeralMin uint16
|
||||
EphemeralMax uint16
|
||||
TCPMux int
|
||||
UDPMux int
|
||||
|
||||
NAT1To1IPs []string
|
||||
IpRetrievalUrl string
|
||||
|
||||
Estimator WebRTCEstimator
|
||||
}
|
||||
|
||||
func (WebRTC) Init(cmd *cobra.Command) error {
|
||||
cmd.PersistentFlags().Bool("webrtc.icelite", false, "configures whether or not the ICE agent should be a lite agent")
|
||||
if err := viper.BindPFlag("webrtc.icelite", cmd.PersistentFlags().Lookup("webrtc.icelite")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Bool("webrtc.icetrickle", true, "configures whether cadidates should be sent asynchronously using Trickle ICE")
|
||||
if err := viper.BindPFlag("webrtc.icetrickle", cmd.PersistentFlags().Lookup("webrtc.icetrickle")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Looks like this is conflicting with the frontend and backend ICE servers since latest versions
|
||||
//cmd.PersistentFlags().String("webrtc.iceservers", "[]", "Global STUN and TURN servers in JSON format with `urls`, `username` and `credential` keys")
|
||||
//if err := viper.BindPFlag("webrtc.iceservers", cmd.PersistentFlags().Lookup("webrtc.iceservers")); err != nil {
|
||||
// return err
|
||||
//}
|
||||
|
||||
cmd.PersistentFlags().String("webrtc.iceservers.frontend", "[]", "Frontend only STUN and TURN servers in JSON format with `urls`, `username` and `credential` keys")
|
||||
if err := viper.BindPFlag("webrtc.iceservers.frontend", cmd.PersistentFlags().Lookup("webrtc.iceservers.frontend")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("webrtc.iceservers.backend", "[]", "Backend only STUN and TURN servers in JSON format with `urls`, `username` and `credential` keys")
|
||||
if err := viper.BindPFlag("webrtc.iceservers.backend", cmd.PersistentFlags().Lookup("webrtc.iceservers.backend")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("webrtc.epr", "", "limits the pool of ephemeral ports that ICE UDP connections can allocate from")
|
||||
if err := viper.BindPFlag("webrtc.epr", cmd.PersistentFlags().Lookup("webrtc.epr")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Int("webrtc.tcpmux", 0, "single TCP mux port for all peers")
|
||||
if err := viper.BindPFlag("webrtc.tcpmux", cmd.PersistentFlags().Lookup("webrtc.tcpmux")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Int("webrtc.udpmux", 0, "single UDP mux port for all peers, replaces EPR")
|
||||
if err := viper.BindPFlag("webrtc.udpmux", cmd.PersistentFlags().Lookup("webrtc.udpmux")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().StringSlice("webrtc.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("webrtc.nat1to1", cmd.PersistentFlags().Lookup("webrtc.nat1to1")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("webrtc.ip_retrieval_url", "https://checkip.amazonaws.com", "URL address used for retrieval of the external IP address")
|
||||
if err := viper.BindPFlag("webrtc.ip_retrieval_url", cmd.PersistentFlags().Lookup("webrtc.ip_retrieval_url")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// bandwidth estimator
|
||||
|
||||
cmd.PersistentFlags().Bool("webrtc.estimator.enabled", false, "enables the bandwidth estimator")
|
||||
if err := viper.BindPFlag("webrtc.estimator.enabled", cmd.PersistentFlags().Lookup("webrtc.estimator.enabled")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Bool("webrtc.estimator.passive", false, "passive estimator mode, when it does not switch pipelines, only estimates")
|
||||
if err := viper.BindPFlag("webrtc.estimator.passive", cmd.PersistentFlags().Lookup("webrtc.estimator.passive")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Bool("webrtc.estimator.debug", false, "enables debug logging for the bandwidth estimator")
|
||||
if err := viper.BindPFlag("webrtc.estimator.debug", cmd.PersistentFlags().Lookup("webrtc.estimator.debug")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Int("webrtc.estimator.initial_bitrate", 1_000_000, "initial bitrate for the bandwidth estimator")
|
||||
if err := viper.BindPFlag("webrtc.estimator.initial_bitrate", cmd.PersistentFlags().Lookup("webrtc.estimator.initial_bitrate")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Duration("webrtc.estimator.read_interval", 2*time.Second, "how often to read and process bandwidth estimation reports")
|
||||
if err := viper.BindPFlag("webrtc.estimator.read_interval", cmd.PersistentFlags().Lookup("webrtc.estimator.read_interval")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Duration("webrtc.estimator.stable_duration", 12*time.Second, "how long to wait for stable connection (upward or neutral trend) before upgrading")
|
||||
if err := viper.BindPFlag("webrtc.estimator.stable_duration", cmd.PersistentFlags().Lookup("webrtc.estimator.stable_duration")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Duration("webrtc.estimator.unstable_duration", 6*time.Second, "how long to wait for stalled connection (neutral trend with low bandwidth) before downgrading")
|
||||
if err := viper.BindPFlag("webrtc.estimator.unstable_duration", cmd.PersistentFlags().Lookup("webrtc.estimator.unstable_duration")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Duration("webrtc.estimator.stalled_duration", 24*time.Second, "how long to wait for stalled bandwidth estimation before downgrading")
|
||||
if err := viper.BindPFlag("webrtc.estimator.stalled_duration", cmd.PersistentFlags().Lookup("webrtc.estimator.stalled_duration")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Duration("webrtc.estimator.downgrade_backoff", 10*time.Second, "how long to wait before downgrading again after previous downgrade")
|
||||
if err := viper.BindPFlag("webrtc.estimator.downgrade_backoff", cmd.PersistentFlags().Lookup("webrtc.estimator.downgrade_backoff")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Duration("webrtc.estimator.upgrade_backoff", 5*time.Second, "how long to wait before upgrading again after previous upgrade")
|
||||
if err := viper.BindPFlag("webrtc.estimator.upgrade_backoff", cmd.PersistentFlags().Lookup("webrtc.estimator.upgrade_backoff")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Float64("webrtc.estimator.diff_threshold", 0.15, "how bigger the difference between estimated and stream bitrate must be to trigger upgrade/downgrade")
|
||||
if err := viper.BindPFlag("webrtc.estimator.diff_threshold", cmd.PersistentFlags().Lookup("webrtc.estimator.diff_threshold")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *WebRTC) Set() {
|
||||
s.ICELite = viper.GetBool("webrtc.icelite")
|
||||
s.ICETrickle = viper.GetBool("webrtc.icetrickle")
|
||||
|
||||
// parse frontend ice servers
|
||||
if err := viper.UnmarshalKey("webrtc.iceservers.frontend", &s.ICEServersFrontend, viper.DecodeHook(
|
||||
utils.JsonStringAutoDecode([]types.ICEServer{}),
|
||||
)); err != nil {
|
||||
log.Warn().Err(err).Msgf("unable to parse frontend ICE servers")
|
||||
}
|
||||
|
||||
// parse backend ice servers
|
||||
if err := viper.UnmarshalKey("webrtc.iceservers.backend", &s.ICEServersBackend, viper.DecodeHook(
|
||||
utils.JsonStringAutoDecode([]types.ICEServer{}),
|
||||
)); err != nil {
|
||||
log.Warn().Err(err).Msgf("unable to parse backend ICE servers")
|
||||
}
|
||||
|
||||
if s.ICELite && len(s.ICEServersBackend) > 0 {
|
||||
log.Warn().Msgf("ICE Lite is enabled, but backend ICE servers are configured. Backend ICE servers will be ignored.")
|
||||
}
|
||||
|
||||
// if no frontend or backend ice servers are configured
|
||||
if len(s.ICEServersFrontend) == 0 && len(s.ICEServersBackend) == 0 {
|
||||
// parse global ice servers
|
||||
var iceServers []types.ICEServer
|
||||
if err := viper.UnmarshalKey("webrtc.iceservers", &iceServers, viper.DecodeHook(
|
||||
utils.JsonStringAutoDecode([]types.ICEServer{}),
|
||||
)); err != nil {
|
||||
log.Warn().Err(err).Msgf("unable to parse global ICE servers")
|
||||
}
|
||||
|
||||
// add default stun server if none are configured
|
||||
if len(iceServers) == 0 {
|
||||
iceServers = append(iceServers, types.ICEServer{
|
||||
URLs: []string{defStunSrv},
|
||||
})
|
||||
}
|
||||
|
||||
s.ICEServersFrontend = append(s.ICEServersFrontend, iceServers...)
|
||||
s.ICEServersBackend = append(s.ICEServersBackend, iceServers...)
|
||||
}
|
||||
|
||||
s.TCPMux = viper.GetInt("webrtc.tcpmux")
|
||||
s.UDPMux = viper.GetInt("webrtc.udpmux")
|
||||
|
||||
epr := viper.GetString("webrtc.epr")
|
||||
if epr != "" {
|
||||
ports := strings.SplitN(epr, "-", -1)
|
||||
if len(ports) > 1 {
|
||||
min, err := strconv.ParseUint(ports[0], 10, 16)
|
||||
if err != nil {
|
||||
log.Panic().Err(err).Msgf("unable to parse ephemeral min port")
|
||||
}
|
||||
|
||||
max, err := strconv.ParseUint(ports[1], 10, 16)
|
||||
if err != nil {
|
||||
log.Panic().Err(err).Msgf("unable to parse ephemeral max port")
|
||||
}
|
||||
|
||||
s.EphemeralMin = uint16(min)
|
||||
s.EphemeralMax = uint16(max)
|
||||
}
|
||||
|
||||
if s.EphemeralMin > s.EphemeralMax {
|
||||
log.Panic().Msgf("ephemeral min port cannot be bigger than max")
|
||||
}
|
||||
}
|
||||
|
||||
if epr == "" && s.TCPMux == 0 && s.UDPMux == 0 {
|
||||
// using default epr range
|
||||
s.EphemeralMin = 59000
|
||||
s.EphemeralMax = 59100
|
||||
|
||||
log.Warn().
|
||||
Uint16("min", s.EphemeralMin).
|
||||
Uint16("max", s.EphemeralMax).
|
||||
Msgf("no TCP, UDP mux or epr specified, using default epr range")
|
||||
}
|
||||
|
||||
s.NAT1To1IPs = viper.GetStringSlice("webrtc.nat1to1")
|
||||
s.IpRetrievalUrl = viper.GetString("webrtc.ip_retrieval_url")
|
||||
if s.IpRetrievalUrl != "" && len(s.NAT1To1IPs) == 0 {
|
||||
ip, err := utils.HttpRequestGET(s.IpRetrievalUrl)
|
||||
if err == nil {
|
||||
s.NAT1To1IPs = append(s.NAT1To1IPs, ip)
|
||||
} else {
|
||||
log.Warn().Err(err).Msgf("IP retrieval failed")
|
||||
}
|
||||
}
|
||||
|
||||
// bandwidth estimator
|
||||
|
||||
s.Estimator.Enabled = viper.GetBool("webrtc.estimator.enabled")
|
||||
s.Estimator.Passive = viper.GetBool("webrtc.estimator.passive")
|
||||
s.Estimator.Debug = viper.GetBool("webrtc.estimator.debug")
|
||||
s.Estimator.InitialBitrate = viper.GetInt("webrtc.estimator.initial_bitrate")
|
||||
s.Estimator.ReadInterval = viper.GetDuration("webrtc.estimator.read_interval")
|
||||
s.Estimator.StableDuration = viper.GetDuration("webrtc.estimator.stable_duration")
|
||||
s.Estimator.UnstableDuration = viper.GetDuration("webrtc.estimator.unstable_duration")
|
||||
s.Estimator.StalledDuration = viper.GetDuration("webrtc.estimator.stalled_duration")
|
||||
s.Estimator.DowngradeBackoff = viper.GetDuration("webrtc.estimator.downgrade_backoff")
|
||||
s.Estimator.UpgradeBackoff = viper.GetDuration("webrtc.estimator.upgrade_backoff")
|
||||
s.Estimator.DiffThreshold = viper.GetFloat64("webrtc.estimator.diff_threshold")
|
||||
}
|
|
@ -1,122 +0,0 @@
|
|||
package desktop
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/demodesk/neko/pkg/types"
|
||||
"github.com/demodesk/neko/pkg/xevent"
|
||||
)
|
||||
|
||||
func (manager *DesktopManagerCtx) ClipboardGetText() (*types.ClipboardText, error) {
|
||||
text, err := manager.ClipboardGetBinary("STRING")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Rich text must not always be available, can fail silently.
|
||||
html, _ := manager.ClipboardGetBinary("text/html")
|
||||
|
||||
return &types.ClipboardText{
|
||||
Text: string(text),
|
||||
HTML: string(html),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (manager *DesktopManagerCtx) ClipboardSetText(data types.ClipboardText) error {
|
||||
// TODO: Refactor.
|
||||
// Current implementation is unable to set multiple targets. HTML
|
||||
// is set, if available. Otherwise plain text.
|
||||
|
||||
if data.HTML != "" {
|
||||
return manager.ClipboardSetBinary("text/html", []byte(data.HTML))
|
||||
}
|
||||
|
||||
return manager.ClipboardSetBinary("STRING", []byte(data.Text))
|
||||
}
|
||||
|
||||
func (manager *DesktopManagerCtx) ClipboardGetBinary(mime string) ([]byte, error) {
|
||||
cmd := exec.Command("xclip", "-selection", "clipboard", "-out", "-target", mime)
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
msg := strings.TrimSpace(stderr.String())
|
||||
return nil, fmt.Errorf("%s", msg)
|
||||
}
|
||||
|
||||
return stdout.Bytes(), nil
|
||||
}
|
||||
|
||||
func (manager *DesktopManagerCtx) ClipboardSetBinary(mime string, data []byte) error {
|
||||
cmd := exec.Command("xclip", "-selection", "clipboard", "-in", "-target", mime)
|
||||
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO: Refactor.
|
||||
// We need to wait until the data came to the clipboard.
|
||||
wait := make(chan struct{})
|
||||
xevent.Emmiter.Once("clipboard-updated", func(payload ...any) {
|
||||
wait <- struct{}{}
|
||||
})
|
||||
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
msg := strings.TrimSpace(stderr.String())
|
||||
return fmt.Errorf("%s", msg)
|
||||
}
|
||||
|
||||
_, err = stdin.Write(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
stdin.Close()
|
||||
|
||||
// TODO: Refactor.
|
||||
// cmd.Wait()
|
||||
<-wait
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (manager *DesktopManagerCtx) ClipboardGetTargets() ([]string, error) {
|
||||
cmd := exec.Command("xclip", "-selection", "clipboard", "-out", "-target", "TARGETS")
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
msg := strings.TrimSpace(stderr.String())
|
||||
return nil, fmt.Errorf("%s", msg)
|
||||
}
|
||||
|
||||
var response []string
|
||||
targets := strings.Split(stdout.String(), "\n")
|
||||
for _, target := range targets {
|
||||
if target == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if !strings.Contains(target, "/") {
|
||||
continue
|
||||
}
|
||||
|
||||
response = append(response, target)
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
|
@ -1,138 +0,0 @@
|
|||
package desktop
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/kataras/go-events"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/demodesk/neko/internal/config"
|
||||
"github.com/demodesk/neko/pkg/types"
|
||||
"github.com/demodesk/neko/pkg/xevent"
|
||||
"github.com/demodesk/neko/pkg/xinput"
|
||||
"github.com/demodesk/neko/pkg/xorg"
|
||||
)
|
||||
|
||||
var mu = sync.Mutex{}
|
||||
|
||||
type DesktopManagerCtx struct {
|
||||
logger zerolog.Logger
|
||||
wg sync.WaitGroup
|
||||
shutdown chan struct{}
|
||||
emmiter events.EventEmmiter
|
||||
config *config.Desktop
|
||||
screenSize types.ScreenSize // cached screen size
|
||||
input xinput.Driver
|
||||
}
|
||||
|
||||
func New(config *config.Desktop) *DesktopManagerCtx {
|
||||
var input xinput.Driver
|
||||
if config.UseInputDriver {
|
||||
input = xinput.NewDriver(config.InputSocket)
|
||||
} else {
|
||||
input = xinput.NewDummy()
|
||||
}
|
||||
|
||||
return &DesktopManagerCtx{
|
||||
logger: log.With().Str("module", "desktop").Logger(),
|
||||
shutdown: make(chan struct{}),
|
||||
emmiter: events.New(),
|
||||
config: config,
|
||||
screenSize: config.ScreenSize,
|
||||
input: input,
|
||||
}
|
||||
}
|
||||
|
||||
func (manager *DesktopManagerCtx) Start() {
|
||||
if xorg.DisplayOpen(manager.config.Display) {
|
||||
manager.logger.Panic().Str("display", manager.config.Display).Msg("unable to open display")
|
||||
}
|
||||
|
||||
// X11 can throw errors below, and the default error handler exits
|
||||
xevent.SetupErrorHandler()
|
||||
|
||||
xorg.GetScreenConfigurations()
|
||||
|
||||
screenSize, err := xorg.ChangeScreenSize(manager.config.ScreenSize)
|
||||
if err != nil {
|
||||
manager.logger.Err(err).
|
||||
Str("screen_size", screenSize.String()).
|
||||
Msgf("unable to set initial screen size")
|
||||
} else {
|
||||
// cache screen size
|
||||
manager.screenSize = screenSize
|
||||
manager.logger.Info().
|
||||
Str("screen_size", screenSize.String()).
|
||||
Msgf("setting initial screen size")
|
||||
}
|
||||
|
||||
err = manager.input.Connect()
|
||||
if err != nil {
|
||||
// TODO: fail silently to dummy driver?
|
||||
manager.logger.Panic().Err(err).Msg("unable to connect to input driver")
|
||||
}
|
||||
|
||||
// set up event listeners
|
||||
xevent.Unminimize = manager.config.Unminimize
|
||||
xevent.FileChooserDialog = manager.config.FileChooserDialog
|
||||
go xevent.EventLoop(manager.config.Display)
|
||||
|
||||
// in case it was opened
|
||||
if manager.config.FileChooserDialog {
|
||||
go manager.CloseFileChooserDialog()
|
||||
}
|
||||
|
||||
manager.OnEventError(func(error_code uint8, message string, request_code uint8, minor_code uint8) {
|
||||
manager.logger.Warn().
|
||||
Uint8("error_code", error_code).
|
||||
Str("message", message).
|
||||
Uint8("request_code", request_code).
|
||||
Uint8("minor_code", minor_code).
|
||||
Msg("X event error occured")
|
||||
})
|
||||
|
||||
manager.wg.Add(1)
|
||||
|
||||
go func() {
|
||||
defer manager.wg.Done()
|
||||
|
||||
ticker := time.NewTicker(1 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
const debounceDuration = 10 * time.Second
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-manager.shutdown:
|
||||
return
|
||||
case <-ticker.C:
|
||||
xorg.CheckKeys(debounceDuration)
|
||||
manager.input.Debounce(debounceDuration)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (manager *DesktopManagerCtx) OnBeforeScreenSizeChange(listener func()) {
|
||||
manager.emmiter.On("before_screen_size_change", func(payload ...any) {
|
||||
listener()
|
||||
})
|
||||
}
|
||||
|
||||
func (manager *DesktopManagerCtx) OnAfterScreenSizeChange(listener func()) {
|
||||
manager.emmiter.On("after_screen_size_change", func(payload ...any) {
|
||||
listener()
|
||||
})
|
||||
}
|
||||
|
||||
func (manager *DesktopManagerCtx) Shutdown() error {
|
||||
manager.logger.Info().Msgf("shutdown")
|
||||
|
||||
close(manager.shutdown)
|
||||
manager.wg.Wait()
|
||||
|
||||
xorg.DisplayClose()
|
||||
return nil
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
package desktop
|
||||
|
||||
import (
|
||||
"github.com/demodesk/neko/pkg/xevent"
|
||||
)
|
||||
|
||||
func (manager *DesktopManagerCtx) OnCursorChanged(listener func(serial uint64)) {
|
||||
xevent.Emmiter.On("cursor-changed", func(payload ...any) {
|
||||
listener(payload[0].(uint64))
|
||||
})
|
||||
}
|
||||
|
||||
func (manager *DesktopManagerCtx) OnClipboardUpdated(listener func()) {
|
||||
xevent.Emmiter.On("clipboard-updated", func(payload ...any) {
|
||||
listener()
|
||||
})
|
||||
}
|
||||
|
||||
func (manager *DesktopManagerCtx) OnFileChooserDialogOpened(listener func()) {
|
||||
xevent.Emmiter.On("file-chooser-dialog-opened", func(payload ...any) {
|
||||
listener()
|
||||
})
|
||||
}
|
||||
|
||||
func (manager *DesktopManagerCtx) OnFileChooserDialogClosed(listener func()) {
|
||||
xevent.Emmiter.On("file-chooser-dialog-closed", func(payload ...any) {
|
||||
listener()
|
||||
})
|
||||
}
|
||||
|
||||
func (manager *DesktopManagerCtx) OnEventError(listener func(error_code uint8, message string, request_code uint8, minor_code uint8)) {
|
||||
xevent.Emmiter.On("event-error", func(payload ...any) {
|
||||
listener(payload[0].(uint8), payload[1].(string), payload[2].(uint8), payload[3].(uint8))
|
||||
})
|
||||
}
|
|
@ -1,202 +0,0 @@
|
|||
package desktop
|
||||
|
||||
import (
|
||||
"image"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"github.com/demodesk/neko/pkg/types"
|
||||
"github.com/demodesk/neko/pkg/xorg"
|
||||
)
|
||||
|
||||
func (manager *DesktopManagerCtx) Move(x, y int) {
|
||||
xorg.Move(x, y)
|
||||
}
|
||||
|
||||
func (manager *DesktopManagerCtx) GetCursorPosition() (int, int) {
|
||||
return xorg.GetCursorPosition()
|
||||
}
|
||||
|
||||
func (manager *DesktopManagerCtx) Scroll(deltaX, deltaY int, controlKey bool) {
|
||||
xorg.Scroll(deltaX, deltaY, controlKey)
|
||||
}
|
||||
|
||||
func (manager *DesktopManagerCtx) ButtonDown(code uint32) error {
|
||||
return xorg.ButtonDown(code)
|
||||
}
|
||||
|
||||
func (manager *DesktopManagerCtx) KeyDown(code uint32) error {
|
||||
return xorg.KeyDown(code)
|
||||
}
|
||||
|
||||
func (manager *DesktopManagerCtx) ButtonUp(code uint32) error {
|
||||
return xorg.ButtonUp(code)
|
||||
}
|
||||
|
||||
func (manager *DesktopManagerCtx) KeyUp(code uint32) error {
|
||||
return xorg.KeyUp(code)
|
||||
}
|
||||
|
||||
func (manager *DesktopManagerCtx) ButtonPress(code uint32) error {
|
||||
xorg.ResetKeys()
|
||||
defer xorg.ResetKeys()
|
||||
|
||||
return xorg.ButtonDown(code)
|
||||
}
|
||||
|
||||
func (manager *DesktopManagerCtx) KeyPress(codes ...uint32) error {
|
||||
xorg.ResetKeys()
|
||||
defer xorg.ResetKeys()
|
||||
|
||||
for _, code := range codes {
|
||||
if err := xorg.KeyDown(code); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(codes) > 1 {
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (manager *DesktopManagerCtx) ResetKeys() {
|
||||
xorg.ResetKeys()
|
||||
}
|
||||
|
||||
func (manager *DesktopManagerCtx) ScreenConfigurations() []types.ScreenSize {
|
||||
var configs []types.ScreenSize
|
||||
for _, size := range xorg.ScreenConfigurations {
|
||||
for _, fps := range size.Rates {
|
||||
// filter out all irrelevant rates
|
||||
if fps > 60 || (fps > 30 && fps%10 != 0) {
|
||||
continue
|
||||
}
|
||||
|
||||
configs = append(configs, types.ScreenSize{
|
||||
Width: size.Width,
|
||||
Height: size.Height,
|
||||
Rate: fps,
|
||||
})
|
||||
}
|
||||
}
|
||||
return configs
|
||||
}
|
||||
|
||||
func (manager *DesktopManagerCtx) SetScreenSize(screenSize types.ScreenSize) (types.ScreenSize, error) {
|
||||
mu.Lock()
|
||||
manager.emmiter.Emit("before_screen_size_change")
|
||||
|
||||
defer func() {
|
||||
manager.emmiter.Emit("after_screen_size_change")
|
||||
mu.Unlock()
|
||||
}()
|
||||
|
||||
screenSize, err := xorg.ChangeScreenSize(screenSize)
|
||||
if err == nil {
|
||||
// cache the new screen size
|
||||
manager.screenSize = screenSize
|
||||
}
|
||||
|
||||
return screenSize, err
|
||||
}
|
||||
|
||||
func (manager *DesktopManagerCtx) GetScreenSize() types.ScreenSize {
|
||||
return xorg.GetScreenSize()
|
||||
}
|
||||
|
||||
func (manager *DesktopManagerCtx) SetKeyboardMap(kbd types.KeyboardMap) error {
|
||||
// TOOD: Use native API.
|
||||
cmd := exec.Command("setxkbmap", "-layout", kbd.Layout, "-variant", kbd.Variant)
|
||||
_, err := cmd.Output()
|
||||
return err
|
||||
}
|
||||
|
||||
func (manager *DesktopManagerCtx) GetKeyboardMap() (*types.KeyboardMap, error) {
|
||||
// TOOD: Use native API.
|
||||
cmd := exec.Command("setxkbmap", "-query")
|
||||
res, err := cmd.Output()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
kbd := types.KeyboardMap{}
|
||||
|
||||
re := regexp.MustCompile(`layout:\s+(.*)\n`)
|
||||
arr := re.FindStringSubmatch(string(res))
|
||||
if len(arr) > 1 {
|
||||
kbd.Layout = arr[1]
|
||||
}
|
||||
|
||||
re = regexp.MustCompile(`variant:\s+(.*)\n`)
|
||||
arr = re.FindStringSubmatch(string(res))
|
||||
if len(arr) > 1 {
|
||||
kbd.Variant = arr[1]
|
||||
}
|
||||
|
||||
return &kbd, nil
|
||||
}
|
||||
|
||||
func (manager *DesktopManagerCtx) SetKeyboardModifiers(mod types.KeyboardModifiers) {
|
||||
if mod.Shift != nil {
|
||||
xorg.SetKeyboardModifier(xorg.KbdModShift, *mod.Shift)
|
||||
}
|
||||
|
||||
if mod.CapsLock != nil {
|
||||
xorg.SetKeyboardModifier(xorg.KbdModCapsLock, *mod.CapsLock)
|
||||
}
|
||||
|
||||
if mod.Control != nil {
|
||||
xorg.SetKeyboardModifier(xorg.KbdModControl, *mod.Control)
|
||||
}
|
||||
|
||||
if mod.Alt != nil {
|
||||
xorg.SetKeyboardModifier(xorg.KbdModAlt, *mod.Alt)
|
||||
}
|
||||
|
||||
if mod.NumLock != nil {
|
||||
xorg.SetKeyboardModifier(xorg.KbdModNumLock, *mod.NumLock)
|
||||
}
|
||||
|
||||
if mod.Meta != nil {
|
||||
xorg.SetKeyboardModifier(xorg.KbdModMeta, *mod.Meta)
|
||||
}
|
||||
|
||||
if mod.Super != nil {
|
||||
xorg.SetKeyboardModifier(xorg.KbdModSuper, *mod.Super)
|
||||
}
|
||||
|
||||
if mod.AltGr != nil {
|
||||
xorg.SetKeyboardModifier(xorg.KbdModAltGr, *mod.AltGr)
|
||||
}
|
||||
}
|
||||
|
||||
func (manager *DesktopManagerCtx) GetKeyboardModifiers() types.KeyboardModifiers {
|
||||
modifiers := xorg.GetKeyboardModifiers()
|
||||
|
||||
isset := func(mod xorg.KbdMod) *bool {
|
||||
x := modifiers&mod != 0
|
||||
return &x
|
||||
}
|
||||
|
||||
return types.KeyboardModifiers{
|
||||
Shift: isset(xorg.KbdModShift),
|
||||
CapsLock: isset(xorg.KbdModCapsLock),
|
||||
Control: isset(xorg.KbdModControl),
|
||||
Alt: isset(xorg.KbdModAlt),
|
||||
NumLock: isset(xorg.KbdModNumLock),
|
||||
Meta: isset(xorg.KbdModMeta),
|
||||
Super: isset(xorg.KbdModSuper),
|
||||
AltGr: isset(xorg.KbdModAltGr),
|
||||
}
|
||||
}
|
||||
|
||||
func (manager *DesktopManagerCtx) GetCursorImage() *types.CursorImage {
|
||||
return xorg.GetCursorImage()
|
||||
}
|
||||
|
||||
func (manager *DesktopManagerCtx) GetScreenshotImage() *image.RGBA {
|
||||
return xorg.GetScreenshotImage()
|
||||
}
|
|
@ -1,135 +0,0 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/middleware"
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"github.com/demodesk/neko/pkg/types"
|
||||
"github.com/demodesk/neko/pkg/utils"
|
||||
)
|
||||
|
||||
type logFormatter struct {
|
||||
logger zerolog.Logger
|
||||
}
|
||||
|
||||
func (l *logFormatter) NewLogEntry(r *http.Request) middleware.LogEntry {
|
||||
// exclude health & metrics from logs
|
||||
if r.RequestURI == "/health" || r.RequestURI == "/metrics" {
|
||||
return &nulllog{}
|
||||
}
|
||||
|
||||
req := map[string]any{}
|
||||
|
||||
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)
|
||||
|
||||
return &logEntry{
|
||||
logger: l.logger.With().Interface("req", req).Logger(),
|
||||
}
|
||||
}
|
||||
|
||||
type logEntry struct {
|
||||
logger zerolog.Logger
|
||||
err error
|
||||
panic *logPanic
|
||||
session types.Session
|
||||
}
|
||||
|
||||
type logPanic struct {
|
||||
message string
|
||||
stack string
|
||||
}
|
||||
|
||||
func (e *logEntry) Panic(v any, stack []byte) {
|
||||
e.panic = &logPanic{
|
||||
message: fmt.Sprintf("%+v", v),
|
||||
stack: string(stack),
|
||||
}
|
||||
}
|
||||
|
||||
func (e *logEntry) Error(err error) {
|
||||
e.err = err
|
||||
}
|
||||
|
||||
func (e *logEntry) SetSession(session types.Session) {
|
||||
e.session = session
|
||||
}
|
||||
|
||||
func (e *logEntry) Write(status, bytes int, header http.Header, elapsed time.Duration, extra any) {
|
||||
res := map[string]any{}
|
||||
res["time"] = time.Now().UTC().Format(time.RFC1123)
|
||||
res["status"] = status
|
||||
res["bytes"] = bytes
|
||||
res["elapsed"] = float64(elapsed.Nanoseconds()) / 1000000.0
|
||||
|
||||
logger := e.logger.With().Interface("res", res).Logger()
|
||||
|
||||
// add session ID to logs (if exists)
|
||||
if e.session != nil {
|
||||
logger = logger.With().Str("session_id", e.session.ID()).Logger()
|
||||
}
|
||||
|
||||
// handle panic error message
|
||||
if e.panic != nil {
|
||||
logger.WithLevel(zerolog.PanicLevel).
|
||||
Err(e.err).
|
||||
Str("stack", e.panic.stack).
|
||||
Msgf("request failed (%d): %s", status, e.panic.message)
|
||||
return
|
||||
}
|
||||
|
||||
// handle panic error message
|
||||
if e.err != nil {
|
||||
httpErr, ok := e.err.(*utils.HTTPError)
|
||||
if !ok {
|
||||
logger.Err(e.err).Msgf("request failed (%d)", status)
|
||||
return
|
||||
}
|
||||
|
||||
if httpErr.Message == "" {
|
||||
httpErr.Message = http.StatusText(httpErr.Code)
|
||||
}
|
||||
|
||||
var logLevel zerolog.Level
|
||||
if httpErr.Code < 500 {
|
||||
logLevel = zerolog.WarnLevel
|
||||
} else {
|
||||
logLevel = zerolog.ErrorLevel
|
||||
}
|
||||
|
||||
message := httpErr.Message
|
||||
if httpErr.InternalMsg != "" {
|
||||
message = httpErr.InternalMsg
|
||||
}
|
||||
|
||||
logger.WithLevel(logLevel).Err(httpErr.InternalErr).Msgf("request failed (%d): %s", status, message)
|
||||
return
|
||||
}
|
||||
|
||||
logger.Debug().Msgf("request complete (%d)", status)
|
||||
}
|
||||
|
||||
type nulllog struct{}
|
||||
|
||||
func (e *nulllog) Panic(v any, stack []byte) {}
|
||||
func (e *nulllog) Error(err error) {}
|
||||
func (e *nulllog) SetSession(session types.Session) {}
|
||||
func (e *nulllog) Write(status, bytes int, header http.Header, elapsed time.Duration, extra any) {
|
||||
}
|
|
@ -1,412 +0,0 @@
|
|||
package session
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/kataras/go-events"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/demodesk/neko/internal/config"
|
||||
"github.com/demodesk/neko/pkg/types"
|
||||
"github.com/demodesk/neko/pkg/utils"
|
||||
)
|
||||
|
||||
func New(config *config.Session) *SessionManagerCtx {
|
||||
manager := &SessionManagerCtx{
|
||||
logger: log.With().Str("module", "session").Logger(),
|
||||
config: config,
|
||||
settings: types.Settings{
|
||||
PrivateMode: config.PrivateMode,
|
||||
LockedControls: config.LockedControls,
|
||||
ImplicitHosting: config.ImplicitHosting,
|
||||
InactiveCursors: config.InactiveCursors,
|
||||
MercifulReconnect: config.MercifulReconnect,
|
||||
},
|
||||
tokens: make(map[string]string),
|
||||
sessions: make(map[string]*SessionCtx),
|
||||
cursors: make(map[types.Session][]types.Cursor),
|
||||
emmiter: events.New(),
|
||||
}
|
||||
|
||||
// create API session
|
||||
if config.APIToken != "" {
|
||||
manager.apiSession = &SessionCtx{
|
||||
id: "API",
|
||||
token: config.APIToken,
|
||||
manager: manager,
|
||||
logger: manager.logger.With().Str("session_id", "API").Logger(),
|
||||
profile: types.MemberProfile{
|
||||
Name: "API Session",
|
||||
IsAdmin: true,
|
||||
CanLogin: true,
|
||||
CanConnect: false,
|
||||
CanWatch: true,
|
||||
CanHost: true,
|
||||
CanAccessClipboard: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// try to load sessions from file
|
||||
manager.load()
|
||||
|
||||
return manager
|
||||
}
|
||||
|
||||
type SessionManagerCtx struct {
|
||||
logger zerolog.Logger
|
||||
config *config.Session
|
||||
|
||||
settings types.Settings
|
||||
settingsMu sync.Mutex
|
||||
|
||||
tokens map[string]string
|
||||
sessions map[string]*SessionCtx
|
||||
sessionsMu sync.Mutex
|
||||
|
||||
hostId atomic.Value
|
||||
|
||||
cursors map[types.Session][]types.Cursor
|
||||
cursorsMu sync.Mutex
|
||||
|
||||
emmiter events.EventEmmiter
|
||||
apiSession *SessionCtx
|
||||
}
|
||||
|
||||
func (manager *SessionManagerCtx) Create(id string, profile types.MemberProfile) (types.Session, string, error) {
|
||||
token, err := utils.NewUID(64)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
manager.sessionsMu.Lock()
|
||||
if _, ok := manager.sessions[id]; ok {
|
||||
manager.sessionsMu.Unlock()
|
||||
return nil, "", types.ErrSessionAlreadyExists
|
||||
}
|
||||
|
||||
if _, ok := manager.tokens[token]; ok {
|
||||
manager.sessionsMu.Unlock()
|
||||
return nil, "", errors.New("session token already exists")
|
||||
}
|
||||
|
||||
session := &SessionCtx{
|
||||
id: id,
|
||||
token: token,
|
||||
manager: manager,
|
||||
logger: manager.logger.With().Str("session_id", id).Logger(),
|
||||
profile: profile,
|
||||
}
|
||||
|
||||
manager.tokens[token] = id
|
||||
manager.sessions[id] = session
|
||||
manager.sessionsMu.Unlock()
|
||||
|
||||
manager.emmiter.Emit("created", session)
|
||||
manager.save()
|
||||
|
||||
return session, token, nil
|
||||
}
|
||||
|
||||
func (manager *SessionManagerCtx) Update(id string, profile types.MemberProfile) error {
|
||||
manager.sessionsMu.Lock()
|
||||
|
||||
session, ok := manager.sessions[id]
|
||||
if !ok {
|
||||
manager.sessionsMu.Unlock()
|
||||
return types.ErrSessionNotFound
|
||||
}
|
||||
|
||||
session.profile = profile
|
||||
manager.sessionsMu.Unlock()
|
||||
|
||||
manager.emmiter.Emit("profile_changed", session)
|
||||
manager.save()
|
||||
|
||||
session.profileChanged()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (manager *SessionManagerCtx) Delete(id string) error {
|
||||
manager.sessionsMu.Lock()
|
||||
session, ok := manager.sessions[id]
|
||||
if !ok {
|
||||
manager.sessionsMu.Unlock()
|
||||
return types.ErrSessionNotFound
|
||||
}
|
||||
|
||||
delete(manager.tokens, session.token)
|
||||
delete(manager.sessions, id)
|
||||
manager.sessionsMu.Unlock()
|
||||
|
||||
if session.State().IsConnected {
|
||||
session.DestroyWebSocketPeer("session deleted")
|
||||
}
|
||||
|
||||
if session.State().IsWatching {
|
||||
session.GetWebRTCPeer().Destroy()
|
||||
}
|
||||
|
||||
manager.emmiter.Emit("deleted", session)
|
||||
manager.save()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (manager *SessionManagerCtx) Get(id string) (types.Session, bool) {
|
||||
manager.sessionsMu.Lock()
|
||||
defer manager.sessionsMu.Unlock()
|
||||
|
||||
session, ok := manager.sessions[id]
|
||||
return session, ok
|
||||
}
|
||||
|
||||
func (manager *SessionManagerCtx) GetByToken(token string) (types.Session, bool) {
|
||||
manager.sessionsMu.Lock()
|
||||
id, ok := manager.tokens[token]
|
||||
manager.sessionsMu.Unlock()
|
||||
|
||||
if ok {
|
||||
return manager.Get(id)
|
||||
}
|
||||
|
||||
// is API session
|
||||
if manager.apiSession != nil && manager.apiSession.token == token {
|
||||
return manager.apiSession, true
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (manager *SessionManagerCtx) List() []types.Session {
|
||||
manager.sessionsMu.Lock()
|
||||
defer manager.sessionsMu.Unlock()
|
||||
|
||||
var sessions []types.Session
|
||||
for _, session := range manager.sessions {
|
||||
sessions = append(sessions, session)
|
||||
}
|
||||
|
||||
return sessions
|
||||
}
|
||||
|
||||
// ---
|
||||
// host
|
||||
// ---
|
||||
|
||||
func (manager *SessionManagerCtx) SetHost(host types.Session) {
|
||||
var hostId string
|
||||
if host != nil {
|
||||
hostId = host.ID()
|
||||
}
|
||||
|
||||
manager.hostId.Store(hostId)
|
||||
manager.emmiter.Emit("host_changed", host)
|
||||
}
|
||||
|
||||
func (manager *SessionManagerCtx) GetHost() (types.Session, bool) {
|
||||
hostId, ok := manager.hostId.Load().(string)
|
||||
if !ok || hostId == "" {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return manager.Get(hostId)
|
||||
}
|
||||
|
||||
func (manager *SessionManagerCtx) ClearHost() {
|
||||
manager.SetHost(nil)
|
||||
}
|
||||
|
||||
func (manager *SessionManagerCtx) isHost(host types.Session) bool {
|
||||
hostId, ok := manager.hostId.Load().(string)
|
||||
return ok && hostId == host.ID()
|
||||
}
|
||||
|
||||
// ---
|
||||
// cursors
|
||||
// ---
|
||||
|
||||
func (manager *SessionManagerCtx) SetCursor(cursor types.Cursor, session types.Session) {
|
||||
manager.cursorsMu.Lock()
|
||||
defer manager.cursorsMu.Unlock()
|
||||
|
||||
list, ok := manager.cursors[session]
|
||||
if !ok {
|
||||
list = []types.Cursor{}
|
||||
}
|
||||
|
||||
list = append(list, cursor)
|
||||
manager.cursors[session] = list
|
||||
}
|
||||
|
||||
func (manager *SessionManagerCtx) PopCursors() map[types.Session][]types.Cursor {
|
||||
manager.cursorsMu.Lock()
|
||||
defer manager.cursorsMu.Unlock()
|
||||
|
||||
cursors := manager.cursors
|
||||
manager.cursors = make(map[types.Session][]types.Cursor)
|
||||
|
||||
return cursors
|
||||
}
|
||||
|
||||
// ---
|
||||
// broadcasts
|
||||
// ---
|
||||
|
||||
func (manager *SessionManagerCtx) Broadcast(event string, payload any, exclude ...string) {
|
||||
for _, session := range manager.List() {
|
||||
if !session.State().IsConnected {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(exclude) > 0 {
|
||||
if in, _ := utils.ArrayIn(session.ID(), exclude); in {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
session.Send(event, payload)
|
||||
}
|
||||
}
|
||||
|
||||
func (manager *SessionManagerCtx) AdminBroadcast(event string, payload any, exclude ...string) {
|
||||
for _, session := range manager.List() {
|
||||
if !session.State().IsConnected || !session.Profile().IsAdmin {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(exclude) > 0 {
|
||||
if in, _ := utils.ArrayIn(session.ID(), exclude); in {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
session.Send(event, payload)
|
||||
}
|
||||
}
|
||||
|
||||
func (manager *SessionManagerCtx) InactiveCursorsBroadcast(event string, payload any, exclude ...string) {
|
||||
for _, session := range manager.List() {
|
||||
if !session.State().IsConnected || !session.Profile().CanSeeInactiveCursors {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(exclude) > 0 {
|
||||
if in, _ := utils.ArrayIn(session.ID(), exclude); in {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
session.Send(event, payload)
|
||||
}
|
||||
}
|
||||
|
||||
// ---
|
||||
// events
|
||||
// ---
|
||||
|
||||
func (manager *SessionManagerCtx) OnCreated(listener func(session types.Session)) {
|
||||
manager.emmiter.On("created", func(payload ...any) {
|
||||
listener(payload[0].(*SessionCtx))
|
||||
})
|
||||
}
|
||||
|
||||
func (manager *SessionManagerCtx) OnDeleted(listener func(session types.Session)) {
|
||||
manager.emmiter.On("deleted", func(payload ...any) {
|
||||
listener(payload[0].(*SessionCtx))
|
||||
})
|
||||
}
|
||||
|
||||
func (manager *SessionManagerCtx) OnConnected(listener func(session types.Session)) {
|
||||
manager.emmiter.On("connected", func(payload ...any) {
|
||||
listener(payload[0].(*SessionCtx))
|
||||
})
|
||||
}
|
||||
|
||||
func (manager *SessionManagerCtx) OnDisconnected(listener func(session types.Session)) {
|
||||
manager.emmiter.On("disconnected", func(payload ...any) {
|
||||
listener(payload[0].(*SessionCtx))
|
||||
})
|
||||
}
|
||||
|
||||
func (manager *SessionManagerCtx) OnProfileChanged(listener func(session types.Session)) {
|
||||
manager.emmiter.On("profile_changed", func(payload ...any) {
|
||||
listener(payload[0].(*SessionCtx))
|
||||
})
|
||||
}
|
||||
|
||||
func (manager *SessionManagerCtx) OnStateChanged(listener func(session types.Session)) {
|
||||
manager.emmiter.On("state_changed", func(payload ...any) {
|
||||
listener(payload[0].(*SessionCtx))
|
||||
})
|
||||
}
|
||||
|
||||
func (manager *SessionManagerCtx) OnHostChanged(listener func(session types.Session)) {
|
||||
manager.emmiter.On("host_changed", func(payload ...any) {
|
||||
if payload[0] == nil {
|
||||
listener(nil)
|
||||
} else {
|
||||
listener(payload[0].(*SessionCtx))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (manager *SessionManagerCtx) OnSettingsChanged(listener func(new types.Settings, old types.Settings)) {
|
||||
manager.emmiter.On("settings_changed", func(payload ...any) {
|
||||
listener(payload[0].(types.Settings), payload[1].(types.Settings))
|
||||
})
|
||||
}
|
||||
|
||||
// ---
|
||||
// settings
|
||||
// ---
|
||||
|
||||
func (manager *SessionManagerCtx) UpdateSettings(new types.Settings) {
|
||||
manager.settingsMu.Lock()
|
||||
old := manager.settings
|
||||
manager.settings = new
|
||||
manager.settingsMu.Unlock()
|
||||
|
||||
// if private mode changed
|
||||
if old.PrivateMode != new.PrivateMode {
|
||||
// update webrtc paused state for all sessions
|
||||
for _, session := range manager.List() {
|
||||
enabled := session.PrivateModeEnabled()
|
||||
|
||||
// if session had control, it must release it
|
||||
if enabled && session.IsHost() {
|
||||
manager.ClearHost()
|
||||
}
|
||||
|
||||
// its webrtc connection will be paused or unpaused
|
||||
if webrtcPeer := session.GetWebRTCPeer(); webrtcPeer != nil {
|
||||
webrtcPeer.SetPaused(enabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if contols have been locked
|
||||
if old.LockedControls != new.LockedControls && new.LockedControls {
|
||||
// if the host is not admin, it must release controls
|
||||
host, hasHost := manager.GetHost()
|
||||
if hasHost && !host.Profile().IsAdmin {
|
||||
manager.ClearHost()
|
||||
}
|
||||
}
|
||||
|
||||
manager.emmiter.Emit("settings_changed", new, old)
|
||||
}
|
||||
|
||||
func (manager *SessionManagerCtx) Settings() types.Settings {
|
||||
manager.settingsMu.Lock()
|
||||
defer manager.settingsMu.Unlock()
|
||||
|
||||
return manager.settings
|
||||
}
|
||||
|
||||
func (manager *SessionManagerCtx) CookieEnabled() bool {
|
||||
return manager.config.CookieEnabled
|
||||
}
|
|
@ -1,285 +0,0 @@
|
|||
package session
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"github.com/demodesk/neko/pkg/types"
|
||||
"github.com/demodesk/neko/pkg/types/event"
|
||||
)
|
||||
|
||||
// client is expected to reconnect within 5 second
|
||||
// if some unexpected websocket disconnect happens
|
||||
const WS_DELAYED_DURATION = 5 * time.Second
|
||||
|
||||
type SessionCtx struct {
|
||||
id string
|
||||
token string
|
||||
logger zerolog.Logger
|
||||
manager *SessionManagerCtx
|
||||
profile types.MemberProfile
|
||||
state types.SessionState
|
||||
|
||||
websocketPeer types.WebSocketPeer
|
||||
websocketMu sync.Mutex
|
||||
|
||||
// websocket delayed set connected events
|
||||
wsDelayedMu sync.Mutex
|
||||
wsDelayedTimer *time.Timer
|
||||
|
||||
webrtcPeer types.WebRTCPeer
|
||||
webrtcMu sync.Mutex
|
||||
}
|
||||
|
||||
func (session *SessionCtx) ID() string {
|
||||
return session.id
|
||||
}
|
||||
|
||||
func (session *SessionCtx) Profile() types.MemberProfile {
|
||||
return session.profile
|
||||
}
|
||||
|
||||
func (session *SessionCtx) profileChanged() {
|
||||
if !session.profile.CanHost && session.IsHost() {
|
||||
session.manager.ClearHost()
|
||||
}
|
||||
|
||||
if (!session.profile.CanConnect || !session.profile.CanLogin || !session.profile.CanWatch) && session.state.IsWatching {
|
||||
session.GetWebRTCPeer().Destroy()
|
||||
}
|
||||
|
||||
if (!session.profile.CanConnect || !session.profile.CanLogin) && session.state.IsConnected {
|
||||
session.DestroyWebSocketPeer("profile changed")
|
||||
}
|
||||
|
||||
// update webrtc paused state
|
||||
if webrtcPeer := session.GetWebRTCPeer(); webrtcPeer != nil {
|
||||
webrtcPeer.SetPaused(session.PrivateModeEnabled())
|
||||
}
|
||||
}
|
||||
|
||||
func (session *SessionCtx) State() types.SessionState {
|
||||
return session.state
|
||||
}
|
||||
|
||||
func (session *SessionCtx) IsHost() bool {
|
||||
return session.manager.isHost(session)
|
||||
}
|
||||
|
||||
func (session *SessionCtx) PrivateModeEnabled() bool {
|
||||
return session.manager.Settings().PrivateMode && !session.profile.IsAdmin
|
||||
}
|
||||
|
||||
func (session *SessionCtx) SetCursor(cursor types.Cursor) {
|
||||
if session.manager.Settings().InactiveCursors && session.profile.SendsInactiveCursor {
|
||||
session.manager.SetCursor(cursor, session)
|
||||
}
|
||||
}
|
||||
|
||||
// ---
|
||||
// websocket
|
||||
// ---
|
||||
|
||||
//
|
||||
// Connect WebSocket peer sets current peer and emits connected event. It also destroys the
|
||||
// previous peer, if there was one. If the peer is already set, it will be ignored.
|
||||
//
|
||||
func (session *SessionCtx) ConnectWebSocketPeer(websocketPeer types.WebSocketPeer) {
|
||||
session.websocketMu.Lock()
|
||||
isCurrentPeer := websocketPeer == session.websocketPeer
|
||||
session.websocketPeer, websocketPeer = websocketPeer, session.websocketPeer
|
||||
session.websocketMu.Unlock()
|
||||
|
||||
// ignore if already set
|
||||
if isCurrentPeer {
|
||||
return
|
||||
}
|
||||
|
||||
session.logger.Info().Msg("set websocket connected")
|
||||
|
||||
// update state
|
||||
now := time.Now()
|
||||
session.state.IsConnected = true
|
||||
session.state.ConnectedSince = &now
|
||||
session.state.NotConnectedSince = nil
|
||||
|
||||
session.manager.emmiter.Emit("connected", session)
|
||||
|
||||
// if there is a previous peer, destroy it
|
||||
if websocketPeer != nil {
|
||||
websocketPeer.Destroy("connection replaced")
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Disconnect WebSocket peer sets current peer to nil and emits disconnected event. It also
|
||||
// allows for a delayed disconnect. That means, the peer will not be disconnected immediately,
|
||||
// but after a delay. If the peer is connected again before the delay, the disconnect will be
|
||||
// cancelled.
|
||||
//
|
||||
// If the peer is not the current peer or the peer is nil, it will be ignored.
|
||||
//
|
||||
func (session *SessionCtx) DisconnectWebSocketPeer(websocketPeer types.WebSocketPeer, delayed bool) {
|
||||
session.websocketMu.Lock()
|
||||
isCurrentPeer := websocketPeer == session.websocketPeer && websocketPeer != nil
|
||||
session.websocketMu.Unlock()
|
||||
|
||||
// ignore if not current peer
|
||||
if !isCurrentPeer {
|
||||
return
|
||||
}
|
||||
|
||||
//
|
||||
// ws delayed
|
||||
//
|
||||
|
||||
var wsDelayedTimer *time.Timer
|
||||
|
||||
if delayed {
|
||||
wsDelayedTimer = time.AfterFunc(WS_DELAYED_DURATION, func() {
|
||||
session.DisconnectWebSocketPeer(websocketPeer, false)
|
||||
})
|
||||
}
|
||||
|
||||
session.wsDelayedMu.Lock()
|
||||
if session.wsDelayedTimer != nil {
|
||||
session.wsDelayedTimer.Stop()
|
||||
}
|
||||
session.wsDelayedTimer = wsDelayedTimer
|
||||
session.wsDelayedMu.Unlock()
|
||||
|
||||
if delayed {
|
||||
session.logger.Info().Msg("delayed websocket disconnected")
|
||||
return
|
||||
}
|
||||
|
||||
//
|
||||
// not delayed
|
||||
//
|
||||
|
||||
session.logger.Info().Msg("set websocket disconnected")
|
||||
|
||||
now := time.Now()
|
||||
session.state.IsConnected = false
|
||||
session.state.ConnectedSince = nil
|
||||
session.state.NotConnectedSince = &now
|
||||
|
||||
session.manager.emmiter.Emit("disconnected", session)
|
||||
|
||||
session.websocketMu.Lock()
|
||||
if websocketPeer == session.websocketPeer {
|
||||
session.websocketPeer = nil
|
||||
}
|
||||
session.websocketMu.Unlock()
|
||||
}
|
||||
|
||||
//
|
||||
// Destroy WebSocket peer disconnects the peer and destroys it. It ensures that the peer is
|
||||
// disconnected immediately even though normal flow would be to disconnect it delayed.
|
||||
//
|
||||
func (session *SessionCtx) DestroyWebSocketPeer(reason string) {
|
||||
session.websocketMu.Lock()
|
||||
peer := session.websocketPeer
|
||||
session.websocketMu.Unlock()
|
||||
|
||||
if peer == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// disconnect peer first, so that it is not used anymore
|
||||
session.DisconnectWebSocketPeer(peer, false)
|
||||
|
||||
// destroy it afterwards
|
||||
peer.Destroy(reason)
|
||||
}
|
||||
|
||||
//
|
||||
// Send event to websocket peer.
|
||||
//
|
||||
func (session *SessionCtx) Send(event string, payload any) {
|
||||
session.websocketMu.Lock()
|
||||
peer := session.websocketPeer
|
||||
session.websocketMu.Unlock()
|
||||
|
||||
if peer != nil {
|
||||
peer.Send(event, payload)
|
||||
}
|
||||
}
|
||||
|
||||
// ---
|
||||
// webrtc
|
||||
// ---
|
||||
|
||||
//
|
||||
// Set webrtc peer and destroy the old one, if there is old one.
|
||||
//
|
||||
func (session *SessionCtx) SetWebRTCPeer(webrtcPeer types.WebRTCPeer) {
|
||||
session.webrtcMu.Lock()
|
||||
session.webrtcPeer, webrtcPeer = webrtcPeer, session.webrtcPeer
|
||||
session.webrtcMu.Unlock()
|
||||
|
||||
if webrtcPeer != nil && webrtcPeer != session.webrtcPeer {
|
||||
webrtcPeer.Destroy()
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Set if current webrtc peer is connected or not. Since there might be lefover calls from
|
||||
// webrtc peer, that are not used anymore, we need to check if the webrtc peer is still the
|
||||
// same as the one we are setting the connected state for.
|
||||
//
|
||||
// If webrtc peer is disconnected, we don't expect it to be reconnected, so we set it to nil
|
||||
// and send a signal close to the client. New connection is expected to use a new webrtc peer.
|
||||
//
|
||||
func (session *SessionCtx) SetWebRTCConnected(webrtcPeer types.WebRTCPeer, connected bool) {
|
||||
session.webrtcMu.Lock()
|
||||
isCurrentPeer := webrtcPeer == session.webrtcPeer
|
||||
session.webrtcMu.Unlock()
|
||||
|
||||
if !isCurrentPeer {
|
||||
return
|
||||
}
|
||||
|
||||
session.logger.Info().
|
||||
Bool("connected", connected).
|
||||
Msg("set webrtc connected")
|
||||
|
||||
// update state
|
||||
session.state.IsWatching = connected
|
||||
if now := time.Now(); connected {
|
||||
session.state.WatchingSince = &now
|
||||
session.state.NotWatchingSince = nil
|
||||
} else {
|
||||
session.state.WatchingSince = nil
|
||||
session.state.NotWatchingSince = &now
|
||||
}
|
||||
|
||||
session.manager.emmiter.Emit("state_changed", session)
|
||||
|
||||
if connected {
|
||||
return
|
||||
}
|
||||
|
||||
session.webrtcMu.Lock()
|
||||
isCurrentPeer = webrtcPeer == session.webrtcPeer
|
||||
if isCurrentPeer {
|
||||
session.webrtcPeer = nil
|
||||
}
|
||||
session.webrtcMu.Unlock()
|
||||
|
||||
if isCurrentPeer {
|
||||
session.Send(event.SIGNAL_CLOSE, nil)
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Get current WebRTC peer. Nil if not connected.
|
||||
//
|
||||
func (session *SessionCtx) GetWebRTCPeer() types.WebRTCPeer {
|
||||
session.webrtcMu.Lock()
|
||||
defer session.webrtcMu.Unlock()
|
||||
|
||||
return session.webrtcPeer
|
||||
}
|
|
@ -1,543 +0,0 @@
|
|||
package webrtc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/pion/interceptor/pkg/cc"
|
||||
"github.com/pion/rtcp"
|
||||
"github.com/pion/webrtc/v3"
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"github.com/demodesk/neko/internal/config"
|
||||
"github.com/demodesk/neko/internal/webrtc/payload"
|
||||
"github.com/demodesk/neko/pkg/types"
|
||||
"github.com/demodesk/neko/pkg/types/event"
|
||||
"github.com/demodesk/neko/pkg/utils"
|
||||
)
|
||||
|
||||
type WebRTCPeerCtx struct {
|
||||
mu sync.Mutex
|
||||
logger zerolog.Logger
|
||||
session types.Session
|
||||
metrics *metrics
|
||||
connection *webrtc.PeerConnection
|
||||
// bandwidth estimator
|
||||
estimator cc.BandwidthEstimator
|
||||
estimateTrend *utils.TrendDetector
|
||||
// stream selectors
|
||||
video types.StreamSelectorManager
|
||||
audio types.StreamSinkManager
|
||||
// tracks & channels
|
||||
audioTrack *Track
|
||||
videoTrack *Track
|
||||
dataChannel *webrtc.DataChannel
|
||||
rtcpChannel chan []rtcp.Packet
|
||||
// config
|
||||
iceTrickle bool
|
||||
estimatorConfig config.WebRTCEstimator
|
||||
paused bool
|
||||
videoAuto bool
|
||||
videoDisabled bool
|
||||
audioDisabled bool
|
||||
}
|
||||
|
||||
//
|
||||
// connection
|
||||
//
|
||||
|
||||
func (peer *WebRTCPeerCtx) CreateOffer(ICERestart bool) (*webrtc.SessionDescription, error) {
|
||||
peer.mu.Lock()
|
||||
defer peer.mu.Unlock()
|
||||
|
||||
offer, err := peer.connection.CreateOffer(&webrtc.OfferOptions{
|
||||
ICERestart: ICERestart,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return peer.setLocalDescription(offer)
|
||||
}
|
||||
|
||||
func (peer *WebRTCPeerCtx) CreateAnswer() (*webrtc.SessionDescription, error) {
|
||||
peer.mu.Lock()
|
||||
defer peer.mu.Unlock()
|
||||
|
||||
answer, err := peer.connection.CreateAnswer(nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return peer.setLocalDescription(answer)
|
||||
}
|
||||
|
||||
func (peer *WebRTCPeerCtx) setLocalDescription(description webrtc.SessionDescription) (*webrtc.SessionDescription, error) {
|
||||
if !peer.iceTrickle {
|
||||
// Create channel that is blocked until ICE Gathering is complete
|
||||
gatherComplete := webrtc.GatheringCompletePromise(peer.connection)
|
||||
|
||||
if err := peer.connection.SetLocalDescription(description); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
<-gatherComplete
|
||||
} else {
|
||||
if err := peer.connection.SetLocalDescription(description); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return peer.connection.LocalDescription(), nil
|
||||
}
|
||||
|
||||
func (peer *WebRTCPeerCtx) SetRemoteDescription(desc webrtc.SessionDescription) error {
|
||||
peer.mu.Lock()
|
||||
defer peer.mu.Unlock()
|
||||
|
||||
return peer.connection.SetRemoteDescription(desc)
|
||||
}
|
||||
|
||||
func (peer *WebRTCPeerCtx) SetCandidate(candidate webrtc.ICECandidateInit) error {
|
||||
peer.mu.Lock()
|
||||
defer peer.mu.Unlock()
|
||||
|
||||
return peer.connection.AddICECandidate(candidate)
|
||||
}
|
||||
|
||||
// TODO: Add shutdown function?
|
||||
func (peer *WebRTCPeerCtx) Destroy() {
|
||||
peer.mu.Lock()
|
||||
defer peer.mu.Unlock()
|
||||
|
||||
var err error
|
||||
|
||||
// if peer connection is not closed, close it
|
||||
if peer.connection.ConnectionState() != webrtc.PeerConnectionStateClosed {
|
||||
err = peer.connection.Close()
|
||||
}
|
||||
|
||||
peer.logger.Err(err).Msg("peer connection destroyed")
|
||||
}
|
||||
|
||||
func (peer *WebRTCPeerCtx) estimatorReader() {
|
||||
conf := peer.estimatorConfig
|
||||
|
||||
// if estimator is not in debug mode, use a nop logger
|
||||
var debugLogger zerolog.Logger
|
||||
if conf.Debug {
|
||||
debugLogger = peer.logger.With().Str("component", "estimator").Logger().Level(zerolog.DebugLevel)
|
||||
} else {
|
||||
debugLogger = zerolog.Nop()
|
||||
}
|
||||
|
||||
// if estimator is disabled, do nothing
|
||||
if peer.estimator == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// use a ticker to get current client target bitrate
|
||||
ticker := time.NewTicker(conf.ReadInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
// since when is the estimate stable/unstable
|
||||
stableSince := time.Now() // we asume stable at start
|
||||
unstableSince := time.Time{}
|
||||
// since when are we neutral but cannot accomodate current bitrate
|
||||
// we migt be stalled or estimator just reached zer (very bad connection)
|
||||
stalledSince := time.Time{}
|
||||
// when was the last upgrade/downgrade
|
||||
lastUpgradeTime := time.Time{}
|
||||
lastDowngradeTime := time.Time{}
|
||||
|
||||
for range ticker.C {
|
||||
targetBitrate := peer.estimator.GetTargetBitrate()
|
||||
peer.metrics.SetReceiverEstimatedTargetBitrate(float64(targetBitrate))
|
||||
|
||||
// if peer connection is closed, stop reading
|
||||
if peer.connection.ConnectionState() == webrtc.PeerConnectionStateClosed {
|
||||
break
|
||||
}
|
||||
|
||||
// if estimation or video is disabled, do nothing
|
||||
if !peer.videoAuto || peer.videoDisabled || peer.paused || conf.Passive {
|
||||
continue
|
||||
}
|
||||
|
||||
// get trend direction to decide if we should upgrade or downgrade
|
||||
peer.estimateTrend.AddValue(int64(targetBitrate))
|
||||
direction := peer.estimateTrend.GetDirection()
|
||||
|
||||
// get current stream bitrate
|
||||
stream, ok := peer.videoTrack.Stream()
|
||||
if !ok {
|
||||
debugLogger.Warn().Msg("looks like we don't have a stream yet, skipping bitrate estimation")
|
||||
continue
|
||||
}
|
||||
|
||||
// if stream bitrate is 0, we need to wait for some time until we get a valid value
|
||||
streamId, streamBitrate := stream.ID(), stream.Bitrate()
|
||||
if streamBitrate == 0 {
|
||||
debugLogger.Warn().Msg("looks like stream bitrate is 0, we need to wait for some time")
|
||||
continue
|
||||
}
|
||||
|
||||
// check whats the difference between target and stream bitrate
|
||||
diff := float64(targetBitrate) / float64(streamBitrate)
|
||||
|
||||
debugLogger.Info().
|
||||
Float64("diff", diff).
|
||||
Int("target_bitrate", targetBitrate).
|
||||
Uint64("stream_bitrate", streamBitrate).
|
||||
Str("direction", direction.String()).
|
||||
Msg("got bitrate from estimator")
|
||||
|
||||
// if we can accomodate current stream or we are not netural anymore,
|
||||
// we are not stalled so we reset the stalled time
|
||||
if direction != utils.TrendDirectionNeutral || diff > 1+conf.DiffThreshold {
|
||||
stalledSince = time.Now()
|
||||
}
|
||||
|
||||
// if we are neutral and stalled for too long, we might be congesting
|
||||
stalled := direction == utils.TrendDirectionNeutral && time.Since(stalledSince) > conf.StalledDuration
|
||||
if stalled {
|
||||
debugLogger.Warn().
|
||||
Time("stalled_since", stalledSince).
|
||||
Msgf("it looks like we are stalled")
|
||||
}
|
||||
|
||||
// if we have an downward trend or are stalled, we might be congesting
|
||||
if direction == utils.TrendDirectionDownward || stalled {
|
||||
// we reset the stable time because we are congesting
|
||||
stableSince = time.Now()
|
||||
|
||||
// if we downgraded recently, we wait for some more time
|
||||
if time.Since(lastDowngradeTime) < conf.DowngradeBackoff {
|
||||
debugLogger.Debug().
|
||||
Time("last_downgrade", lastDowngradeTime).
|
||||
Msgf("downgraded recently, waiting for at least %v", conf.DowngradeBackoff)
|
||||
continue
|
||||
}
|
||||
|
||||
// if we are not unstable but we fluctuate we should wait for some more time
|
||||
if time.Since(unstableSince) < conf.UnstableDuration {
|
||||
debugLogger.Debug().
|
||||
Time("unstable_since", unstableSince).
|
||||
Msgf("we are not unstable long enough, waiting for at least %v", conf.UnstableDuration)
|
||||
continue
|
||||
}
|
||||
|
||||
// if we still have a big difference between target and stream bitrate, we wait for some more time
|
||||
if conf.DiffThreshold >= 0 && diff > 1+conf.DiffThreshold {
|
||||
debugLogger.Debug().
|
||||
Float64("diff", diff).
|
||||
Float64("threshold", conf.DiffThreshold).
|
||||
Msgf("we still have a big difference between target and stream bitrate, " +
|
||||
"therefore we still should be able to accomodate current stream")
|
||||
continue
|
||||
}
|
||||
|
||||
err := peer.SetVideo(types.PeerVideoRequest{
|
||||
Selector: &types.StreamSelector{
|
||||
ID: streamId,
|
||||
Type: types.StreamSelectorTypeLower,
|
||||
},
|
||||
})
|
||||
if err != nil && err != types.ErrWebRTCStreamNotFound {
|
||||
peer.logger.Warn().Err(err).Msg("failed to downgrade video stream")
|
||||
}
|
||||
lastDowngradeTime = time.Now()
|
||||
|
||||
if err == types.ErrWebRTCStreamNotFound {
|
||||
debugLogger.Info().Msg("looks like we are already on the lowest stream")
|
||||
} else {
|
||||
debugLogger.Info().Msg("downgraded video stream")
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// we reset the unstable time because we are not congesting
|
||||
unstableSince = time.Now()
|
||||
|
||||
// if we have a neutral or upward trend, that means our estimate is stable
|
||||
// if we are on the highest stream, we don't need to do anything
|
||||
// but if there is a higher stream, we should try to upgrade and see if it works
|
||||
|
||||
// if we upgraded recently, we wait for some more time
|
||||
if time.Since(lastUpgradeTime) < conf.UpgradeBackoff {
|
||||
debugLogger.Debug().
|
||||
Time("last_upgrade", lastUpgradeTime).
|
||||
Msgf("upgraded recently, waiting for at least %v", conf.UpgradeBackoff)
|
||||
continue
|
||||
}
|
||||
|
||||
// if we are not stable for long enough, we wait for some more time
|
||||
// because bandwidth estimation might fluctuate
|
||||
if time.Since(stableSince) < conf.StableDuration {
|
||||
debugLogger.Debug().
|
||||
Time("stable_since", stableSince).
|
||||
Msgf("we are not stable long enough, waiting for at least %v", conf.StableDuration)
|
||||
continue
|
||||
}
|
||||
|
||||
// upgrade only if estimated bitrate passed the threshold
|
||||
if conf.DiffThreshold >= 0 && diff < 1+conf.DiffThreshold {
|
||||
debugLogger.Debug().
|
||||
Float64("diff", diff).
|
||||
Float64("threshold", conf.DiffThreshold).
|
||||
Msgf("looks like we don't have enough bitrate to accomodate higher stream, " +
|
||||
"therefore we should wait for some more time")
|
||||
continue
|
||||
}
|
||||
|
||||
err := peer.SetVideo(types.PeerVideoRequest{
|
||||
Selector: &types.StreamSelector{
|
||||
ID: streamId,
|
||||
Type: types.StreamSelectorTypeHigher,
|
||||
},
|
||||
})
|
||||
if err != nil && err != types.ErrWebRTCStreamNotFound {
|
||||
peer.logger.Warn().Err(err).Msg("failed to upgrade video stream")
|
||||
}
|
||||
lastUpgradeTime = time.Now()
|
||||
|
||||
if err == types.ErrWebRTCStreamNotFound {
|
||||
debugLogger.Info().Msg("looks like we are already on the highest stream")
|
||||
} else {
|
||||
debugLogger.Info().Msg("upgraded video stream")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (peer *WebRTCPeerCtx) SetPaused(isPaused bool) error {
|
||||
peer.mu.Lock()
|
||||
defer peer.mu.Unlock()
|
||||
|
||||
peer.videoTrack.SetPaused(isPaused || peer.videoDisabled)
|
||||
peer.audioTrack.SetPaused(isPaused || peer.audioDisabled)
|
||||
|
||||
peer.logger.Info().Bool("is_paused", isPaused).Msg("set paused")
|
||||
peer.paused = isPaused
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (peer *WebRTCPeerCtx) Paused() bool {
|
||||
peer.mu.Lock()
|
||||
defer peer.mu.Unlock()
|
||||
|
||||
return peer.paused
|
||||
}
|
||||
|
||||
//
|
||||
// video
|
||||
//
|
||||
|
||||
func (peer *WebRTCPeerCtx) SetVideo(r types.PeerVideoRequest) error {
|
||||
peer.mu.Lock()
|
||||
defer peer.mu.Unlock()
|
||||
|
||||
modified := false
|
||||
|
||||
// video disabled
|
||||
if r.Disabled != nil {
|
||||
disabled := *r.Disabled
|
||||
|
||||
// update only if changed
|
||||
if peer.videoDisabled != disabled {
|
||||
peer.videoDisabled = disabled
|
||||
peer.videoTrack.SetPaused(disabled || peer.paused)
|
||||
|
||||
peer.logger.Info().Bool("disabled", disabled).Msg("set video disabled")
|
||||
modified = true
|
||||
}
|
||||
}
|
||||
|
||||
// video selector
|
||||
if r.Selector != nil {
|
||||
selector := *r.Selector
|
||||
|
||||
// get requested video stream from selector
|
||||
stream, ok := peer.video.GetStream(selector)
|
||||
if !ok {
|
||||
return types.ErrWebRTCStreamNotFound
|
||||
}
|
||||
|
||||
// set video stream to track
|
||||
changed, err := peer.videoTrack.SetStream(stream)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update only if stream changed
|
||||
if changed {
|
||||
videoID := stream.ID()
|
||||
peer.metrics.SetVideoID(videoID)
|
||||
|
||||
peer.logger.Info().Str("video_id", videoID).Msg("set video")
|
||||
modified = true
|
||||
}
|
||||
}
|
||||
|
||||
// video auto
|
||||
if r.Auto != nil {
|
||||
videoAuto := *r.Auto
|
||||
|
||||
if peer.estimator == nil || peer.estimatorConfig.Passive {
|
||||
peer.logger.Warn().Msg("estimator is disabled or in passive mode, cannot change video auto")
|
||||
videoAuto = false // ensure video auto is disabled
|
||||
}
|
||||
|
||||
// update only if video auto changed
|
||||
if peer.videoAuto != videoAuto {
|
||||
peer.videoAuto = videoAuto
|
||||
|
||||
peer.logger.Info().Bool("video_auto", videoAuto).Msg("set video auto")
|
||||
modified = true
|
||||
}
|
||||
}
|
||||
|
||||
// send video signal if modified
|
||||
if modified {
|
||||
go func() {
|
||||
// in goroutine because of mutex and we don't want to block
|
||||
peer.session.Send(event.SIGNAL_VIDEO, peer.Video())
|
||||
}()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (peer *WebRTCPeerCtx) Video() types.PeerVideo {
|
||||
peer.mu.Lock()
|
||||
defer peer.mu.Unlock()
|
||||
|
||||
// get current video stream ID
|
||||
ID := ""
|
||||
stream, ok := peer.videoTrack.Stream()
|
||||
if ok {
|
||||
ID = stream.ID()
|
||||
}
|
||||
|
||||
return types.PeerVideo{
|
||||
Disabled: peer.videoDisabled,
|
||||
ID: ID,
|
||||
Video: ID, // TODO: Remove, used for backward compatibility
|
||||
Auto: peer.videoAuto,
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// audio
|
||||
//
|
||||
|
||||
func (peer *WebRTCPeerCtx) SetAudio(r types.PeerAudioRequest) error {
|
||||
peer.mu.Lock()
|
||||
defer peer.mu.Unlock()
|
||||
|
||||
modified := false
|
||||
|
||||
// audio disabled
|
||||
if r.Disabled != nil {
|
||||
disabled := *r.Disabled
|
||||
|
||||
// update only if changed
|
||||
if peer.audioDisabled != disabled {
|
||||
peer.audioDisabled = disabled
|
||||
peer.audioTrack.SetPaused(disabled || peer.paused)
|
||||
|
||||
peer.logger.Info().Bool("disabled", disabled).Msg("set audio disabled")
|
||||
modified = true
|
||||
}
|
||||
}
|
||||
|
||||
// send video signal if modified
|
||||
if modified {
|
||||
go func() {
|
||||
// in goroutine because of mutex and we don't want to block
|
||||
peer.session.Send(event.SIGNAL_AUDIO, peer.Audio())
|
||||
}()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (peer *WebRTCPeerCtx) Audio() types.PeerAudio {
|
||||
peer.mu.Lock()
|
||||
defer peer.mu.Unlock()
|
||||
|
||||
return types.PeerAudio{
|
||||
Disabled: peer.audioDisabled,
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// data channel
|
||||
//
|
||||
|
||||
func (peer *WebRTCPeerCtx) SendCursorPosition(x, y int) error {
|
||||
peer.mu.Lock()
|
||||
defer peer.mu.Unlock()
|
||||
|
||||
// do not send cursor position to host
|
||||
if peer.session.IsHost() {
|
||||
return nil
|
||||
}
|
||||
|
||||
header := payload.Header{
|
||||
Event: payload.OP_CURSOR_POSITION,
|
||||
Length: 7,
|
||||
}
|
||||
|
||||
data := payload.CursorPosition{
|
||||
X: uint16(x),
|
||||
Y: uint16(y),
|
||||
}
|
||||
|
||||
buffer := &bytes.Buffer{}
|
||||
|
||||
if err := binary.Write(buffer, binary.BigEndian, header); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := binary.Write(buffer, binary.BigEndian, data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return peer.dataChannel.Send(buffer.Bytes())
|
||||
}
|
||||
|
||||
func (peer *WebRTCPeerCtx) SendCursorImage(cur *types.CursorImage, img []byte) error {
|
||||
peer.mu.Lock()
|
||||
defer peer.mu.Unlock()
|
||||
|
||||
header := payload.Header{
|
||||
Event: payload.OP_CURSOR_IMAGE,
|
||||
Length: uint16(11 + len(img)),
|
||||
}
|
||||
|
||||
data := payload.CursorImage{
|
||||
Width: cur.Width,
|
||||
Height: cur.Height,
|
||||
Xhot: cur.Xhot,
|
||||
Yhot: cur.Yhot,
|
||||
}
|
||||
|
||||
buffer := &bytes.Buffer{}
|
||||
|
||||
if err := binary.Write(buffer, binary.BigEndian, header); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := binary.Write(buffer, binary.BigEndian, data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := binary.Write(buffer, binary.BigEndian, img); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return peer.dataChannel.Send(buffer.Bytes())
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
package pionlog
|
||||
|
||||
import (
|
||||
"github.com/pion/logging"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
func New(logger zerolog.Logger) Factory {
|
||||
return Factory{
|
||||
Logger: logger.With().Str("submodule", "pion").Logger(),
|
||||
}
|
||||
}
|
||||
|
||||
type Factory struct {
|
||||
Logger zerolog.Logger
|
||||
}
|
||||
|
||||
func (l Factory) NewLogger(subsystem string) logging.LeveledLogger {
|
||||
if subsystem == "sctp" {
|
||||
return nulllog{}
|
||||
}
|
||||
|
||||
return logger{
|
||||
subsystem: subsystem,
|
||||
logger: l.Logger.With().Str("subsystem", subsystem).Logger(),
|
||||
}
|
||||
}
|
|
@ -1,66 +0,0 @@
|
|||
package pionlog
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
type logger struct {
|
||||
logger zerolog.Logger
|
||||
subsystem string
|
||||
}
|
||||
|
||||
func (l logger) Trace(msg string) {
|
||||
l.logger.Trace().Msg(strings.TrimSpace(msg))
|
||||
}
|
||||
|
||||
func (l logger) Tracef(format string, args ...any) {
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
l.logger.Trace().Msg(strings.TrimSpace(msg))
|
||||
}
|
||||
|
||||
func (l logger) Debug(msg string) {
|
||||
l.logger.Debug().Msg(strings.TrimSpace(msg))
|
||||
}
|
||||
|
||||
func (l logger) Debugf(format string, args ...any) {
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
l.logger.Debug().Msg(strings.TrimSpace(msg))
|
||||
}
|
||||
|
||||
func (l logger) Info(msg string) {
|
||||
if strings.Contains(msg, "duplicated packet") {
|
||||
return
|
||||
}
|
||||
|
||||
l.logger.Info().Msg(strings.TrimSpace(msg))
|
||||
}
|
||||
|
||||
func (l logger) Infof(format string, args ...any) {
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
if strings.Contains(msg, "duplicated packet") {
|
||||
return
|
||||
}
|
||||
|
||||
l.logger.Info().Msg(strings.TrimSpace(msg))
|
||||
}
|
||||
|
||||
func (l logger) Warn(msg string) {
|
||||
l.logger.Warn().Msg(strings.TrimSpace(msg))
|
||||
}
|
||||
|
||||
func (l logger) Warnf(format string, args ...any) {
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
l.logger.Warn().Msg(strings.TrimSpace(msg))
|
||||
}
|
||||
|
||||
func (l logger) Error(msg string) {
|
||||
l.logger.Error().Msg(strings.TrimSpace(msg))
|
||||
}
|
||||
|
||||
func (l logger) Errorf(format string, args ...any) {
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
l.logger.Error().Msg(strings.TrimSpace(msg))
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
package pionlog
|
||||
|
||||
type nulllog struct{}
|
||||
|
||||
func (l nulllog) Trace(msg string) {}
|
||||
func (l nulllog) Tracef(format string, args ...any) {}
|
||||
func (l nulllog) Debug(msg string) {}
|
||||
func (l nulllog) Debugf(format string, args ...any) {}
|
||||
func (l nulllog) Info(msg string) {}
|
||||
func (l nulllog) Infof(format string, args ...any) {}
|
||||
func (l nulllog) Warn(msg string) {}
|
||||
func (l nulllog) Warnf(format string, args ...any) {}
|
||||
func (l nulllog) Error(msg string) {}
|
||||
func (l nulllog) Errorf(format string, args ...any) {}
|
|
@ -1,222 +0,0 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/demodesk/neko/pkg/types"
|
||||
"github.com/demodesk/neko/pkg/types/event"
|
||||
"github.com/demodesk/neko/pkg/types/message"
|
||||
"github.com/demodesk/neko/pkg/xorg"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrIsNotAllowedToHost = errors.New("is not allowed to host")
|
||||
ErrIsNotTheHost = errors.New("is not the host")
|
||||
ErrIsAlreadyTheHost = errors.New("is already the host")
|
||||
ErrIsAlreadyHosted = errors.New("is already hosted")
|
||||
)
|
||||
|
||||
func (h *MessageHandlerCtx) controlRelease(session types.Session) error {
|
||||
if !session.Profile().CanHost || session.PrivateModeEnabled() {
|
||||
return ErrIsNotAllowedToHost
|
||||
}
|
||||
|
||||
if !session.IsHost() {
|
||||
return ErrIsNotTheHost
|
||||
}
|
||||
|
||||
h.desktop.ResetKeys()
|
||||
h.sessions.ClearHost()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *MessageHandlerCtx) controlRequest(session types.Session) error {
|
||||
if !session.Profile().CanHost || session.PrivateModeEnabled() {
|
||||
return ErrIsNotAllowedToHost
|
||||
}
|
||||
|
||||
if session.IsHost() {
|
||||
return ErrIsAlreadyTheHost
|
||||
}
|
||||
|
||||
if h.sessions.Settings().LockedControls && !session.Profile().IsAdmin {
|
||||
return ErrIsNotAllowedToHost
|
||||
}
|
||||
|
||||
if !h.sessions.Settings().ImplicitHosting {
|
||||
// tell session if there is a host
|
||||
if host, hasHost := h.sessions.GetHost(); hasHost {
|
||||
session.Send(
|
||||
event.CONTROL_HOST,
|
||||
message.ControlHost{
|
||||
HasHost: true,
|
||||
HostID: host.ID(),
|
||||
})
|
||||
|
||||
return ErrIsAlreadyHosted
|
||||
}
|
||||
}
|
||||
|
||||
h.sessions.SetHost(session)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *MessageHandlerCtx) controlMove(session types.Session, payload *message.ControlPos) error {
|
||||
if err := h.controlRequest(session); err != nil && !errors.Is(err, ErrIsAlreadyTheHost) {
|
||||
return err
|
||||
}
|
||||
|
||||
// handle active cursor movement
|
||||
h.desktop.Move(payload.X, payload.Y)
|
||||
h.webrtc.SetCursorPosition(payload.X, payload.Y)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *MessageHandlerCtx) controlScroll(session types.Session, payload *message.ControlScroll) error {
|
||||
if err := h.controlRequest(session); err != nil && !errors.Is(err, ErrIsAlreadyTheHost) {
|
||||
return err
|
||||
}
|
||||
|
||||
// TOOD: remove this once the client is fixed
|
||||
if payload.DeltaX == 0 && payload.DeltaY == 0 {
|
||||
payload.DeltaX = payload.X
|
||||
payload.DeltaY = payload.Y
|
||||
}
|
||||
|
||||
h.desktop.Scroll(payload.DeltaX, payload.DeltaY, payload.ControlKey)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *MessageHandlerCtx) controlButtonPress(session types.Session, payload *message.ControlButton) error {
|
||||
if payload.ControlPos != nil {
|
||||
if err := h.controlMove(session, payload.ControlPos); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err := h.controlRequest(session); err != nil && !errors.Is(err, ErrIsAlreadyTheHost) {
|
||||
return err
|
||||
}
|
||||
|
||||
return h.desktop.ButtonPress(payload.Code)
|
||||
}
|
||||
|
||||
func (h *MessageHandlerCtx) controlButtonDown(session types.Session, payload *message.ControlButton) error {
|
||||
if payload.ControlPos != nil {
|
||||
if err := h.controlMove(session, payload.ControlPos); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err := h.controlRequest(session); err != nil && !errors.Is(err, ErrIsAlreadyTheHost) {
|
||||
return err
|
||||
}
|
||||
|
||||
return h.desktop.ButtonDown(payload.Code)
|
||||
}
|
||||
|
||||
func (h *MessageHandlerCtx) controlButtonUp(session types.Session, payload *message.ControlButton) error {
|
||||
if payload.ControlPos != nil {
|
||||
if err := h.controlMove(session, payload.ControlPos); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err := h.controlRequest(session); err != nil && !errors.Is(err, ErrIsAlreadyTheHost) {
|
||||
return err
|
||||
}
|
||||
|
||||
return h.desktop.ButtonUp(payload.Code)
|
||||
}
|
||||
|
||||
func (h *MessageHandlerCtx) controlKeyPress(session types.Session, payload *message.ControlKey) error {
|
||||
if payload.ControlPos != nil {
|
||||
if err := h.controlMove(session, payload.ControlPos); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err := h.controlRequest(session); err != nil && !errors.Is(err, ErrIsAlreadyTheHost) {
|
||||
return err
|
||||
}
|
||||
|
||||
return h.desktop.KeyPress(payload.Keysym)
|
||||
}
|
||||
|
||||
func (h *MessageHandlerCtx) controlKeyDown(session types.Session, payload *message.ControlKey) error {
|
||||
if payload.ControlPos != nil {
|
||||
if err := h.controlMove(session, payload.ControlPos); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err := h.controlRequest(session); err != nil && !errors.Is(err, ErrIsAlreadyTheHost) {
|
||||
return err
|
||||
}
|
||||
|
||||
return h.desktop.KeyDown(payload.Keysym)
|
||||
}
|
||||
|
||||
func (h *MessageHandlerCtx) controlKeyUp(session types.Session, payload *message.ControlKey) error {
|
||||
if payload.ControlPos != nil {
|
||||
if err := h.controlMove(session, payload.ControlPos); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err := h.controlRequest(session); err != nil && !errors.Is(err, ErrIsAlreadyTheHost) {
|
||||
return err
|
||||
}
|
||||
|
||||
return h.desktop.KeyUp(payload.Keysym)
|
||||
}
|
||||
|
||||
func (h *MessageHandlerCtx) controlTouchBegin(session types.Session, payload *message.ControlTouch) error {
|
||||
if err := h.controlRequest(session); err != nil && !errors.Is(err, ErrIsAlreadyTheHost) {
|
||||
return err
|
||||
}
|
||||
return h.desktop.TouchBegin(payload.TouchId, payload.X, payload.Y, payload.Pressure)
|
||||
}
|
||||
|
||||
func (h *MessageHandlerCtx) controlTouchUpdate(session types.Session, payload *message.ControlTouch) error {
|
||||
if err := h.controlRequest(session); err != nil && !errors.Is(err, ErrIsAlreadyTheHost) {
|
||||
return err
|
||||
}
|
||||
return h.desktop.TouchUpdate(payload.TouchId, payload.X, payload.Y, payload.Pressure)
|
||||
}
|
||||
|
||||
func (h *MessageHandlerCtx) controlTouchEnd(session types.Session, payload *message.ControlTouch) error {
|
||||
if err := h.controlRequest(session); err != nil && !errors.Is(err, ErrIsAlreadyTheHost) {
|
||||
return err
|
||||
}
|
||||
return h.desktop.TouchEnd(payload.TouchId, payload.X, payload.Y, payload.Pressure)
|
||||
}
|
||||
|
||||
func (h *MessageHandlerCtx) controlCut(session types.Session) error {
|
||||
if err := h.controlRequest(session); err != nil && !errors.Is(err, ErrIsAlreadyTheHost) {
|
||||
return err
|
||||
}
|
||||
|
||||
return h.desktop.KeyPress(xorg.XK_Control_L, xorg.XK_x)
|
||||
}
|
||||
|
||||
func (h *MessageHandlerCtx) controlCopy(session types.Session) error {
|
||||
if err := h.controlRequest(session); err != nil && !errors.Is(err, ErrIsAlreadyTheHost) {
|
||||
return err
|
||||
}
|
||||
|
||||
return h.desktop.KeyPress(xorg.XK_Control_L, xorg.XK_c)
|
||||
}
|
||||
|
||||
func (h *MessageHandlerCtx) controlPaste(session types.Session, payload *message.ClipboardData) error {
|
||||
if err := h.controlRequest(session); err != nil && !errors.Is(err, ErrIsAlreadyTheHost) {
|
||||
return err
|
||||
}
|
||||
|
||||
// if there have been set clipboard data, set them first
|
||||
if payload != nil && payload.Text != "" {
|
||||
if err := h.clipboardSet(session, payload); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return h.desktop.KeyPress(xorg.XK_Control_L, xorg.XK_v)
|
||||
}
|
||||
|
||||
func (h *MessageHandlerCtx) controlSelectAll(session types.Session) error {
|
||||
if err := h.controlRequest(session); err != nil && !errors.Is(err, ErrIsAlreadyTheHost) {
|
||||
return err
|
||||
}
|
||||
|
||||
return h.desktop.KeyPress(xorg.XK_Control_L, xorg.XK_a)
|
||||
}
|
|
@ -1,203 +0,0 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/demodesk/neko/pkg/types"
|
||||
"github.com/demodesk/neko/pkg/types/event"
|
||||
"github.com/demodesk/neko/pkg/types/message"
|
||||
"github.com/demodesk/neko/pkg/utils"
|
||||
)
|
||||
|
||||
func New(
|
||||
sessions types.SessionManager,
|
||||
desktop types.DesktopManager,
|
||||
capture types.CaptureManager,
|
||||
webrtc types.WebRTCManager,
|
||||
) *MessageHandlerCtx {
|
||||
return &MessageHandlerCtx{
|
||||
logger: log.With().Str("module", "websocket").Str("submodule", "handler").Logger(),
|
||||
sessions: sessions,
|
||||
desktop: desktop,
|
||||
capture: capture,
|
||||
webrtc: webrtc,
|
||||
}
|
||||
}
|
||||
|
||||
type MessageHandlerCtx struct {
|
||||
logger zerolog.Logger
|
||||
sessions types.SessionManager
|
||||
webrtc types.WebRTCManager
|
||||
desktop types.DesktopManager
|
||||
capture types.CaptureManager
|
||||
}
|
||||
|
||||
func (h *MessageHandlerCtx) Message(session types.Session, data types.WebSocketMessage) bool {
|
||||
var err error
|
||||
switch data.Event {
|
||||
// System Events
|
||||
case event.SYSTEM_LOGS:
|
||||
payload := &message.SystemLogs{}
|
||||
err = utils.Unmarshal(payload, data.Payload, func() error {
|
||||
return h.systemLogs(session, payload)
|
||||
})
|
||||
|
||||
// Signal Events
|
||||
case event.SIGNAL_REQUEST:
|
||||
payload := &message.SignalRequest{}
|
||||
err = utils.Unmarshal(payload, data.Payload, func() error {
|
||||
return h.signalRequest(session, payload)
|
||||
})
|
||||
case event.SIGNAL_RESTART:
|
||||
err = h.signalRestart(session)
|
||||
case event.SIGNAL_OFFER:
|
||||
payload := &message.SignalDescription{}
|
||||
err = utils.Unmarshal(payload, data.Payload, func() error {
|
||||
return h.signalOffer(session, payload)
|
||||
})
|
||||
case event.SIGNAL_ANSWER:
|
||||
payload := &message.SignalDescription{}
|
||||
err = utils.Unmarshal(payload, data.Payload, func() error {
|
||||
return h.signalAnswer(session, payload)
|
||||
})
|
||||
case event.SIGNAL_CANDIDATE:
|
||||
payload := &message.SignalCandidate{}
|
||||
err = utils.Unmarshal(payload, data.Payload, func() error {
|
||||
return h.signalCandidate(session, payload)
|
||||
})
|
||||
case event.SIGNAL_VIDEO:
|
||||
payload := &message.SignalVideo{}
|
||||
err = utils.Unmarshal(payload, data.Payload, func() error {
|
||||
return h.signalVideo(session, payload)
|
||||
})
|
||||
case event.SIGNAL_AUDIO:
|
||||
payload := &message.SignalAudio{}
|
||||
err = utils.Unmarshal(payload, data.Payload, func() error {
|
||||
return h.signalAudio(session, payload)
|
||||
})
|
||||
|
||||
// Control Events
|
||||
case event.CONTROL_RELEASE:
|
||||
err = h.controlRelease(session)
|
||||
case event.CONTROL_REQUEST:
|
||||
err = h.controlRequest(session)
|
||||
case event.CONTROL_MOVE:
|
||||
payload := &message.ControlPos{}
|
||||
err = utils.Unmarshal(payload, data.Payload, func() error {
|
||||
return h.controlMove(session, payload)
|
||||
})
|
||||
case event.CONTROL_SCROLL:
|
||||
payload := &message.ControlScroll{}
|
||||
err = utils.Unmarshal(payload, data.Payload, func() error {
|
||||
return h.controlScroll(session, payload)
|
||||
})
|
||||
case event.CONTROL_BUTTONPRESS:
|
||||
payload := &message.ControlButton{}
|
||||
err = utils.Unmarshal(payload, data.Payload, func() error {
|
||||
return h.controlButtonPress(session, payload)
|
||||
})
|
||||
case event.CONTROL_BUTTONDOWN:
|
||||
payload := &message.ControlButton{}
|
||||
err = utils.Unmarshal(payload, data.Payload, func() error {
|
||||
return h.controlButtonDown(session, payload)
|
||||
})
|
||||
case event.CONTROL_BUTTONUP:
|
||||
payload := &message.ControlButton{}
|
||||
err = utils.Unmarshal(payload, data.Payload, func() error {
|
||||
return h.controlButtonUp(session, payload)
|
||||
})
|
||||
case event.CONTROL_KEYPRESS:
|
||||
payload := &message.ControlKey{}
|
||||
err = utils.Unmarshal(payload, data.Payload, func() error {
|
||||
return h.controlKeyPress(session, payload)
|
||||
})
|
||||
case event.CONTROL_KEYDOWN:
|
||||
payload := &message.ControlKey{}
|
||||
err = utils.Unmarshal(payload, data.Payload, func() error {
|
||||
return h.controlKeyDown(session, payload)
|
||||
})
|
||||
case event.CONTROL_KEYUP:
|
||||
payload := &message.ControlKey{}
|
||||
err = utils.Unmarshal(payload, data.Payload, func() error {
|
||||
return h.controlKeyUp(session, payload)
|
||||
})
|
||||
// touch
|
||||
case event.CONTROL_TOUCHBEGIN:
|
||||
payload := &message.ControlTouch{}
|
||||
err = utils.Unmarshal(payload, data.Payload, func() error {
|
||||
return h.controlTouchBegin(session, payload)
|
||||
})
|
||||
case event.CONTROL_TOUCHUPDATE:
|
||||
payload := &message.ControlTouch{}
|
||||
err = utils.Unmarshal(payload, data.Payload, func() error {
|
||||
return h.controlTouchUpdate(session, payload)
|
||||
})
|
||||
case event.CONTROL_TOUCHEND:
|
||||
payload := &message.ControlTouch{}
|
||||
err = utils.Unmarshal(payload, data.Payload, func() error {
|
||||
return h.controlTouchEnd(session, payload)
|
||||
})
|
||||
// actions
|
||||
case event.CONTROL_CUT:
|
||||
err = h.controlCut(session)
|
||||
case event.CONTROL_COPY:
|
||||
err = h.controlCopy(session)
|
||||
case event.CONTROL_PASTE:
|
||||
payload := &message.ClipboardData{}
|
||||
err = utils.Unmarshal(payload, data.Payload, func() error {
|
||||
return h.controlPaste(session, payload)
|
||||
})
|
||||
case event.CONTROL_SELECT_ALL:
|
||||
err = h.controlSelectAll(session)
|
||||
|
||||
// Screen Events
|
||||
case event.SCREEN_SET:
|
||||
payload := &message.ScreenSize{}
|
||||
err = utils.Unmarshal(payload, data.Payload, func() error {
|
||||
return h.screenSet(session, payload)
|
||||
})
|
||||
|
||||
// Clipboard Events
|
||||
case event.CLIPBOARD_SET:
|
||||
payload := &message.ClipboardData{}
|
||||
err = utils.Unmarshal(payload, data.Payload, func() error {
|
||||
return h.clipboardSet(session, payload)
|
||||
})
|
||||
|
||||
// Keyboard Events
|
||||
case event.KEYBOARD_MAP:
|
||||
payload := &message.KeyboardMap{}
|
||||
err = utils.Unmarshal(payload, data.Payload, func() error {
|
||||
return h.keyboardMap(session, payload)
|
||||
})
|
||||
case event.KEYBOARD_MODIFIERS:
|
||||
payload := &message.KeyboardModifiers{}
|
||||
err = utils.Unmarshal(payload, data.Payload, func() error {
|
||||
return h.keyboardModifiers(session, payload)
|
||||
})
|
||||
|
||||
// Send Events
|
||||
case event.SEND_UNICAST:
|
||||
payload := &message.SendUnicast{}
|
||||
err = utils.Unmarshal(payload, data.Payload, func() error {
|
||||
return h.sendUnicast(session, payload)
|
||||
})
|
||||
case event.SEND_BROADCAST:
|
||||
payload := &message.SendBroadcast{}
|
||||
err = utils.Unmarshal(payload, data.Payload, func() error {
|
||||
return h.sendBroadcast(session, payload)
|
||||
})
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
h.logger.Warn().Err(err).
|
||||
Str("event", data.Event).
|
||||
Str("session_id", session.ID()).
|
||||
Msg("message handler has failed")
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/demodesk/neko/pkg/types"
|
||||
"github.com/demodesk/neko/pkg/types/event"
|
||||
"github.com/demodesk/neko/pkg/types/message"
|
||||
)
|
||||
|
||||
func (h *MessageHandlerCtx) screenSet(session types.Session, payload *message.ScreenSize) error {
|
||||
if !session.Profile().IsAdmin {
|
||||
return errors.New("is not the admin")
|
||||
}
|
||||
|
||||
size, err := h.desktop.SetScreenSize(types.ScreenSize{
|
||||
Width: payload.Width,
|
||||
Height: payload.Height,
|
||||
Rate: payload.Rate,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
h.sessions.Broadcast(event.SCREEN_UPDATED, message.ScreenSize{
|
||||
Width: size.Width,
|
||||
Height: size.Height,
|
||||
Rate: size.Rate,
|
||||
})
|
||||
return nil
|
||||
}
|
|
@ -1,75 +0,0 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"github.com/demodesk/neko/pkg/types"
|
||||
"github.com/demodesk/neko/pkg/types/event"
|
||||
"github.com/demodesk/neko/pkg/types/message"
|
||||
)
|
||||
|
||||
func (h *MessageHandlerCtx) SessionCreated(session types.Session) error {
|
||||
h.sessions.Broadcast(
|
||||
event.SESSION_CREATED,
|
||||
message.SessionData{
|
||||
ID: session.ID(),
|
||||
Profile: session.Profile(),
|
||||
State: session.State(),
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *MessageHandlerCtx) SessionDeleted(session types.Session) error {
|
||||
h.sessions.Broadcast(
|
||||
event.SESSION_DELETED,
|
||||
message.SessionID{
|
||||
ID: session.ID(),
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *MessageHandlerCtx) SessionConnected(session types.Session) error {
|
||||
if err := h.systemInit(session); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if session.Profile().IsAdmin {
|
||||
if err := h.systemAdmin(session); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return h.SessionStateChanged(session)
|
||||
}
|
||||
|
||||
func (h *MessageHandlerCtx) SessionDisconnected(session types.Session) error {
|
||||
// clear host if exists
|
||||
if session.IsHost() {
|
||||
h.desktop.ResetKeys()
|
||||
h.sessions.ClearHost()
|
||||
}
|
||||
|
||||
return h.SessionStateChanged(session)
|
||||
}
|
||||
|
||||
func (h *MessageHandlerCtx) SessionProfileChanged(session types.Session) error {
|
||||
h.sessions.Broadcast(
|
||||
event.SESSION_PROFILE,
|
||||
message.MemberProfile{
|
||||
ID: session.ID(),
|
||||
MemberProfile: session.Profile(),
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *MessageHandlerCtx) SessionStateChanged(session types.Session) error {
|
||||
h.sessions.Broadcast(
|
||||
event.SESSION_STATE,
|
||||
message.SessionState{
|
||||
ID: session.ID(),
|
||||
SessionState: session.State(),
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,162 +0,0 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/demodesk/neko/pkg/types"
|
||||
"github.com/demodesk/neko/pkg/types/event"
|
||||
"github.com/demodesk/neko/pkg/types/message"
|
||||
"github.com/pion/webrtc/v3"
|
||||
)
|
||||
|
||||
func (h *MessageHandlerCtx) signalRequest(session types.Session, payload *message.SignalRequest) error {
|
||||
if !session.Profile().CanWatch {
|
||||
return errors.New("not allowed to watch")
|
||||
}
|
||||
|
||||
offer, peer, err := h.webrtc.CreatePeer(session)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// set webrtc as paused if session has private mode enabled
|
||||
if session.PrivateModeEnabled() {
|
||||
peer.SetPaused(true)
|
||||
}
|
||||
|
||||
video := payload.Video
|
||||
|
||||
// use default first video, if not provided
|
||||
if video.Selector == nil {
|
||||
videos := h.capture.Video().IDs()
|
||||
video.Selector = &types.StreamSelector{
|
||||
ID: videos[0],
|
||||
Type: types.StreamSelectorTypeExact,
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Remove, used for compatibility with old clients.
|
||||
if video.Auto == nil {
|
||||
video.Auto = &payload.Auto
|
||||
}
|
||||
|
||||
// set video stream
|
||||
err = peer.SetVideo(video)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
audio := payload.Audio
|
||||
|
||||
// enable by default if not requested otherwise
|
||||
if audio.Disabled == nil {
|
||||
disabled := false
|
||||
audio.Disabled = &disabled
|
||||
}
|
||||
|
||||
// set audio stream
|
||||
err = peer.SetAudio(audio)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
session.Send(
|
||||
event.SIGNAL_PROVIDE,
|
||||
message.SignalProvide{
|
||||
SDP: offer.SDP,
|
||||
ICEServers: h.webrtc.ICEServers(),
|
||||
|
||||
Video: peer.Video(),
|
||||
Audio: peer.Audio(),
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *MessageHandlerCtx) signalRestart(session types.Session) error {
|
||||
peer := session.GetWebRTCPeer()
|
||||
if peer == nil {
|
||||
return errors.New("webRTC peer does not exist")
|
||||
}
|
||||
|
||||
offer, err := peer.CreateOffer(true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO: Use offer event instead.
|
||||
session.Send(
|
||||
event.SIGNAL_RESTART,
|
||||
message.SignalDescription{
|
||||
SDP: offer.SDP,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *MessageHandlerCtx) signalOffer(session types.Session, payload *message.SignalDescription) error {
|
||||
peer := session.GetWebRTCPeer()
|
||||
if peer == nil {
|
||||
return errors.New("webRTC peer does not exist")
|
||||
}
|
||||
|
||||
err := peer.SetRemoteDescription(webrtc.SessionDescription{
|
||||
SDP: payload.SDP,
|
||||
Type: webrtc.SDPTypeOffer,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
answer, err := peer.CreateAnswer()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
session.Send(
|
||||
event.SIGNAL_ANSWER,
|
||||
message.SignalDescription{
|
||||
SDP: answer.SDP,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *MessageHandlerCtx) signalAnswer(session types.Session, payload *message.SignalDescription) error {
|
||||
peer := session.GetWebRTCPeer()
|
||||
if peer == nil {
|
||||
return errors.New("webRTC peer does not exist")
|
||||
}
|
||||
|
||||
return peer.SetRemoteDescription(webrtc.SessionDescription{
|
||||
SDP: payload.SDP,
|
||||
Type: webrtc.SDPTypeAnswer,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *MessageHandlerCtx) signalCandidate(session types.Session, payload *message.SignalCandidate) error {
|
||||
peer := session.GetWebRTCPeer()
|
||||
if peer == nil {
|
||||
return errors.New("webRTC peer does not exist")
|
||||
}
|
||||
|
||||
return peer.SetCandidate(payload.ICECandidateInit)
|
||||
}
|
||||
|
||||
func (h *MessageHandlerCtx) signalVideo(session types.Session, payload *message.SignalVideo) error {
|
||||
peer := session.GetWebRTCPeer()
|
||||
if peer == nil {
|
||||
return errors.New("webRTC peer does not exist")
|
||||
}
|
||||
|
||||
return peer.SetVideo(payload.PeerVideoRequest)
|
||||
}
|
||||
|
||||
func (h *MessageHandlerCtx) signalAudio(session types.Session, payload *message.SignalAudio) error {
|
||||
peer := session.GetWebRTCPeer()
|
||||
if peer == nil {
|
||||
return errors.New("webRTC peer does not exist")
|
||||
}
|
||||
|
||||
return peer.SetAudio(payload.PeerAudioRequest)
|
||||
}
|
69
neko.go
69
neko.go
|
@ -1,69 +0,0 @@
|
|||
package neko
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const Header = `&34
|
||||
_ __ __
|
||||
/ | / /__ / /______ \ /\
|
||||
/ |/ / _ \/ //_/ __ \ ) ( ')
|
||||
/ /| / __/ ,< / /_/ / ( / )
|
||||
/_/ |_/\___/_/|_|\____/ \(__)|
|
||||
&1&37 nurdism/m1k1o &33%s %s&0
|
||||
`
|
||||
|
||||
var (
|
||||
//
|
||||
buildDate = "dev"
|
||||
//
|
||||
gitCommit = "dev"
|
||||
//
|
||||
gitBranch = "dev"
|
||||
//
|
||||
gitTag = "dev"
|
||||
)
|
||||
|
||||
var Version = &version{
|
||||
GitCommit: gitCommit,
|
||||
GitBranch: gitBranch,
|
||||
GitTag: gitTag,
|
||||
BuildDate: buildDate,
|
||||
GoVersion: runtime.Version(),
|
||||
Compiler: runtime.Compiler,
|
||||
Platform: fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH),
|
||||
}
|
||||
|
||||
type version struct {
|
||||
GitCommit string
|
||||
GitBranch string
|
||||
GitTag string
|
||||
BuildDate string
|
||||
GoVersion string
|
||||
Compiler string
|
||||
Platform string
|
||||
}
|
||||
|
||||
func (i *version) String() string {
|
||||
version := i.GitTag
|
||||
if version == "" || version == "dev" {
|
||||
version = i.GitBranch
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s@%s", version, i.GitCommit)
|
||||
}
|
||||
|
||||
func (i *version) Details() string {
|
||||
return "\n" + strings.Join([]string{
|
||||
fmt.Sprintf("Version %s", i.String()),
|
||||
fmt.Sprintf("GitCommit %s", i.GitCommit),
|
||||
fmt.Sprintf("GitBranch %s", i.GitBranch),
|
||||
fmt.Sprintf("GitTag %s", i.GitTag),
|
||||
fmt.Sprintf("BuildDate %s", i.BuildDate),
|
||||
fmt.Sprintf("GoVersion %s", i.GoVersion),
|
||||
fmt.Sprintf("Compiler %s", i.Compiler),
|
||||
fmt.Sprintf("Platform %s", i.Platform),
|
||||
}, "\n") + "\n"
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
|
@ -1,2 +0,0 @@
|
|||
DISPLAY=:99.0
|
||||
PION_LOG_TRACE=all
|
8
server/.gitignore
vendored
Normal file
8
server/.gitignore
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
runtime/fonts/*
|
||||
!runtime/fonts/.gitkeep
|
||||
|
||||
runtime/icon-theme/*
|
||||
!runtime/icon-theme/.gitkeep
|
||||
|
||||
plugins/*
|
||||
!plugins/.gitkeep
|
16
server/.vscode/launch.json
vendored
16
server/.vscode/launch.json
vendored
|
@ -1,16 +0,0 @@
|
|||
{
|
||||
"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
server/.vscode/settings.json
vendored
22
server/.vscode/settings.json
vendored
|
@ -1,22 +0,0 @@
|
|||
|
||||
{
|
||||
"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
|
||||
}
|
||||
}
|
||||
}
|
68
server/build
68
server/build
|
@ -4,7 +4,12 @@
|
|||
# aborting if any command returns a non-zero value
|
||||
set -e
|
||||
|
||||
BUILD_TIME=`date -u +'%Y-%m-%dT%H:%M:%SZ'`
|
||||
#
|
||||
# do not build plugins when passing "core" as first argument
|
||||
if [ "$1" = "core" ];
|
||||
then
|
||||
skip_plugins="true"
|
||||
fi
|
||||
|
||||
#
|
||||
# set git build variables if git exists
|
||||
|
@ -13,12 +18,10 @@ then
|
|||
GIT_COMMIT=`git rev-parse --short HEAD`
|
||||
GIT_BRANCH=`git rev-parse --symbolic-full-name --abbrev-ref HEAD`
|
||||
GIT_TAG=`git tag --points-at $GIT_COMMIT | head -n 1`
|
||||
GIT_DIRTY=`git diff-index --quiet HEAD -- || echo "✗-"`
|
||||
GIT_COMMIT="${GIT_DIRTY}${GIT_COMMIT}"
|
||||
fi
|
||||
|
||||
#
|
||||
# load dependencies
|
||||
# load server dependencies
|
||||
go get -v -t -d .
|
||||
|
||||
#
|
||||
|
@ -27,9 +30,58 @@ go build \
|
|||
-o bin/neko \
|
||||
-ldflags "
|
||||
-s -w
|
||||
-X 'm1k1o/neko.buildDate=${BUILD_TIME}'
|
||||
-X 'm1k1o/neko.gitCommit=${GIT_COMMIT}'
|
||||
-X 'm1k1o/neko.gitBranch=${GIT_BRANCH}'
|
||||
-X 'm1k1o/neko.gitTag=${GIT_TAG}'
|
||||
-X 'github.com/demodesk/neko.buildDate=`date -u +'%Y-%m-%dT%H:%M:%SZ'`'
|
||||
-X 'github.com/demodesk/neko.gitCommit=${GIT_COMMIT}'
|
||||
-X 'github.com/demodesk/neko.gitBranch=${GIT_BRANCH}'
|
||||
-X 'github.com/demodesk/neko.gitTag=${GIT_TAG}'
|
||||
" \
|
||||
cmd/neko/main.go;
|
||||
|
||||
#
|
||||
# ensure plugins folder exists
|
||||
mkdir -p bin/plugins
|
||||
|
||||
#
|
||||
# if plugins are ignored
|
||||
if [ "$skip_plugins" = "true" ];
|
||||
then
|
||||
echo "Not building plugins..."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
#
|
||||
# if plugins directory does not exist
|
||||
if [ ! -d "./plugins" ];
|
||||
then
|
||||
echo "No plugins directory found, skipping..."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
#
|
||||
# remove old plugins
|
||||
rm -f bin/plugins/*
|
||||
|
||||
#
|
||||
# build plugins
|
||||
for plugPath in ./plugins/*; do
|
||||
if [ ! -d $plugPath ];
|
||||
then
|
||||
continue
|
||||
fi
|
||||
|
||||
pushd $plugPath
|
||||
|
||||
echo "Building plugin: $plugPath"
|
||||
|
||||
if [ ! -f "go.plug.mod" ];
|
||||
then
|
||||
echo "go.plug.mod not found, skipping..."
|
||||
popd
|
||||
continue
|
||||
fi
|
||||
|
||||
# build plugin
|
||||
go build -modfile=go.plug.mod -buildmode=plugin -buildvcs=false -o "../../bin/plugins/${plugPath##*/}.so"
|
||||
|
||||
popd
|
||||
done
|
||||
|
|
|
@ -5,13 +5,13 @@ import (
|
|||
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"m1k1o/neko"
|
||||
"m1k1o/neko/cmd"
|
||||
"m1k1o/neko/internal/utils"
|
||||
"github.com/demodesk/neko"
|
||||
"github.com/demodesk/neko/cmd"
|
||||
"github.com/demodesk/neko/pkg/utils"
|
||||
)
|
||||
|
||||
func main() {
|
||||
fmt.Print(utils.Colorf(neko.Header, "server", neko.Service.Version))
|
||||
fmt.Print(utils.Colorf(neko.Header, "server", neko.Version))
|
||||
if err := cmd.Execute(); err != nil {
|
||||
log.Panic().Err(err).Msg("failed to execute command")
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
|
@ -14,10 +15,19 @@ import (
|
|||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"m1k1o/neko"
|
||||
"github.com/demodesk/neko"
|
||||
"github.com/demodesk/neko/internal/config"
|
||||
)
|
||||
|
||||
func Execute() error {
|
||||
// properly log unhandled panics
|
||||
defer func() {
|
||||
panicVal := recover()
|
||||
if panicVal != nil {
|
||||
log.Panic().Msgf("%v", panicVal)
|
||||
}
|
||||
}()
|
||||
|
||||
return root.Execute()
|
||||
}
|
||||
|
||||
|
@ -25,58 +35,24 @@ var root = &cobra.Command{
|
|||
Use: "neko",
|
||||
Short: "neko streaming server",
|
||||
Long: `neko streaming server`,
|
||||
Version: neko.Service.Version.String(),
|
||||
Version: neko.Version.String(),
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootConfig := config.Root{}
|
||||
|
||||
cobra.OnInitialize(func() {
|
||||
//////
|
||||
// logs
|
||||
//////
|
||||
zerolog.TimeFieldFormat = ""
|
||||
zerolog.SetGlobalLevel(zerolog.InfoLevel)
|
||||
|
||||
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")
|
||||
|
||||
config := viper.GetString("config") // Use config file from the flag.
|
||||
if config == "" {
|
||||
config = os.Getenv("NEKO_CONFIG") // Use config file from the environment variable.
|
||||
}
|
||||
|
||||
if config != "" {
|
||||
viper.SetConfigFile(config) // Use config file from the flag.
|
||||
viper.SetConfigFile(config)
|
||||
} else {
|
||||
if runtime.GOOS == "linux" {
|
||||
viper.AddConfigPath("/etc/neko/")
|
||||
|
@ -87,41 +63,103 @@ func init() {
|
|||
}
|
||||
|
||||
viper.SetEnvPrefix("NEKO")
|
||||
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
||||
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)
|
||||
// read config values
|
||||
err := viper.ReadInConfig()
|
||||
if err != nil {
|
||||
_, notFound := err.(viper.ConfigFileNotFoundError)
|
||||
if !notFound {
|
||||
log.Fatal().Err(err).Msg("unable to read config file")
|
||||
}
|
||||
}
|
||||
|
||||
debug := viper.GetBool("debug")
|
||||
if debug {
|
||||
zerolog.SetGlobalLevel(zerolog.DebugLevel)
|
||||
// get full config file path
|
||||
config = viper.ConfigFileUsed()
|
||||
|
||||
// set root config values
|
||||
rootConfig.Set()
|
||||
|
||||
//////
|
||||
// logs
|
||||
//////
|
||||
var logWriter io.Writer
|
||||
|
||||
// log to a directory instead of stderr
|
||||
if rootConfig.LogDir != "" {
|
||||
if _, err := os.Stat(rootConfig.LogDir); os.IsNotExist(err) {
|
||||
_ = os.Mkdir(rootConfig.LogDir, os.ModePerm)
|
||||
}
|
||||
|
||||
latest := filepath.Join(rootConfig.LogDir, "neko-latest.log")
|
||||
if _, err := os.Stat(latest); err == nil {
|
||||
err = os.Rename(latest, filepath.Join(rootConfig.LogDir, "neko."+time.Now().Format("2006-01-02T15-04-05Z07-00")+".log"))
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("failed to rotate log file")
|
||||
}
|
||||
}
|
||||
|
||||
logf, err := os.OpenFile(latest, os.O_RDWR|os.O_CREATE, 0666)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("failed to open log file")
|
||||
}
|
||||
|
||||
logWriter = diode.NewWriter(logf, 1000, 10*time.Millisecond, func(missed int) {
|
||||
fmt.Printf("logger dropped %d messages", missed)
|
||||
})
|
||||
} else {
|
||||
logWriter = os.Stderr
|
||||
}
|
||||
|
||||
// log console output instead of json
|
||||
if !rootConfig.LogJson {
|
||||
logWriter = zerolog.ConsoleWriter{
|
||||
Out: logWriter,
|
||||
NoColor: rootConfig.LogNocolor,
|
||||
}
|
||||
}
|
||||
|
||||
// save new logger output
|
||||
log.Logger = log.Output(logWriter)
|
||||
|
||||
// set custom log level
|
||||
if rootConfig.LogLevel != zerolog.NoLevel {
|
||||
zerolog.SetGlobalLevel(rootConfig.LogLevel)
|
||||
}
|
||||
|
||||
// set custom log tiem format
|
||||
if rootConfig.LogTime != "" {
|
||||
zerolog.TimeFieldFormat = rootConfig.LogTime
|
||||
}
|
||||
|
||||
timeFormat := rootConfig.LogTime
|
||||
if rootConfig.LogTime == zerolog.TimeFormatUnix {
|
||||
timeFormat = "UNIX"
|
||||
}
|
||||
|
||||
file := viper.ConfigFileUsed()
|
||||
logger := log.With().
|
||||
Bool("debug", debug).
|
||||
Str("logging", viper.GetString("logs")).
|
||||
Str("config", file).
|
||||
Str("config", config).
|
||||
Str("log-level", zerolog.GlobalLevel().String()).
|
||||
Bool("log-json", rootConfig.LogJson).
|
||||
Str("log-time", timeFormat).
|
||||
Str("log-dir", rootConfig.LogDir).
|
||||
Logger()
|
||||
|
||||
if file == "" {
|
||||
if config == "" {
|
||||
logger.Warn().Msg("preflight complete without config file")
|
||||
} else {
|
||||
logger.Info().Msg("preflight complete")
|
||||
if _, err := os.Stat(config); os.IsNotExist(err) {
|
||||
logger.Err(err).Msg("preflight complete with nonexistent config file")
|
||||
} else {
|
||||
logger.Info().Msg("preflight complete with config file")
|
||||
}
|
||||
}
|
||||
|
||||
neko.Service.Root.Set()
|
||||
})
|
||||
|
||||
if err := neko.Service.Root.Init(root); err != nil {
|
||||
if err := rootConfig.Init(root); err != nil {
|
||||
log.Panic().Err(err).Msg("unable to run root command")
|
||||
}
|
||||
|
||||
root.SetVersionTemplate(neko.Service.Version.Details())
|
||||
root.SetVersionTemplate(neko.Version.Details())
|
||||
}
|
||||
|
|
|
@ -1,41 +1,240 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/signal"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"m1k1o/neko"
|
||||
"m1k1o/neko/internal/config"
|
||||
"github.com/demodesk/neko/internal/api"
|
||||
"github.com/demodesk/neko/internal/capture"
|
||||
"github.com/demodesk/neko/internal/config"
|
||||
"github.com/demodesk/neko/internal/desktop"
|
||||
"github.com/demodesk/neko/internal/http"
|
||||
"github.com/demodesk/neko/internal/member"
|
||||
"github.com/demodesk/neko/internal/plugins"
|
||||
"github.com/demodesk/neko/internal/session"
|
||||
"github.com/demodesk/neko/internal/webrtc"
|
||||
"github.com/demodesk/neko/internal/websocket"
|
||||
)
|
||||
|
||||
func init() {
|
||||
service := serve{}
|
||||
|
||||
command := &cobra.Command{
|
||||
Use: "serve",
|
||||
Short: "serve neko streaming server",
|
||||
Long: `serve neko streaming server`,
|
||||
Run: neko.Service.ServeCommand,
|
||||
Use: "serve",
|
||||
Short: "serve neko streaming server",
|
||||
Long: `serve neko streaming server`,
|
||||
PreRun: service.PreRun,
|
||||
Run: service.Run,
|
||||
}
|
||||
|
||||
configs := []config.Config{
|
||||
neko.Service.Server,
|
||||
neko.Service.WebRTC,
|
||||
neko.Service.Capture,
|
||||
neko.Service.Desktop,
|
||||
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")
|
||||
}
|
||||
if err := service.Init(command); err != nil {
|
||||
log.Panic().Err(err).Msg("unable to initialize configuration")
|
||||
}
|
||||
|
||||
root.AddCommand(command)
|
||||
}
|
||||
|
||||
type serve struct {
|
||||
logger zerolog.Logger
|
||||
|
||||
configs struct {
|
||||
Desktop config.Desktop
|
||||
Capture config.Capture
|
||||
WebRTC config.WebRTC
|
||||
Member config.Member
|
||||
Session config.Session
|
||||
Plugins config.Plugins
|
||||
Server config.Server
|
||||
}
|
||||
|
||||
managers struct {
|
||||
desktop *desktop.DesktopManagerCtx
|
||||
capture *capture.CaptureManagerCtx
|
||||
webRTC *webrtc.WebRTCManagerCtx
|
||||
member *member.MemberManagerCtx
|
||||
session *session.SessionManagerCtx
|
||||
webSocket *websocket.WebSocketManagerCtx
|
||||
plugins *plugins.ManagerCtx
|
||||
api *api.ApiManagerCtx
|
||||
http *http.HttpManagerCtx
|
||||
}
|
||||
}
|
||||
|
||||
func (c *serve) Init(cmd *cobra.Command) error {
|
||||
if err := c.configs.Desktop.Init(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.configs.Capture.Init(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.configs.WebRTC.Init(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.configs.Member.Init(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.configs.Session.Init(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.configs.Plugins.Init(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.configs.Server.Init(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// V2 configuration
|
||||
|
||||
if err := c.configs.Desktop.InitV2(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.configs.Capture.InitV2(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.configs.WebRTC.InitV2(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.configs.Member.InitV2(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.configs.Session.InitV2(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.configs.Server.InitV2(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *serve) PreRun(cmd *cobra.Command, args []string) {
|
||||
c.logger = log.With().Str("service", "neko").Logger()
|
||||
|
||||
c.configs.Desktop.Set()
|
||||
c.configs.Capture.Set()
|
||||
c.configs.WebRTC.Set()
|
||||
c.configs.Member.Set()
|
||||
c.configs.Session.Set()
|
||||
c.configs.Plugins.Set()
|
||||
c.configs.Server.Set()
|
||||
|
||||
c.configs.Desktop.SetV2()
|
||||
c.configs.Capture.SetV2()
|
||||
c.configs.WebRTC.SetV2()
|
||||
c.configs.Member.SetV2()
|
||||
c.configs.Session.SetV2()
|
||||
c.configs.Server.SetV2()
|
||||
}
|
||||
|
||||
func (c *serve) Start(cmd *cobra.Command) {
|
||||
c.managers.session = session.New(
|
||||
&c.configs.Session,
|
||||
)
|
||||
|
||||
c.managers.member = member.New(
|
||||
c.managers.session,
|
||||
&c.configs.Member,
|
||||
)
|
||||
|
||||
if err := c.managers.member.Connect(); err != nil {
|
||||
c.logger.Panic().Err(err).Msg("unable to connect to member manager")
|
||||
}
|
||||
|
||||
c.managers.desktop = desktop.New(
|
||||
&c.configs.Desktop,
|
||||
)
|
||||
c.managers.desktop.Start()
|
||||
|
||||
c.managers.capture = capture.New(
|
||||
c.managers.desktop,
|
||||
&c.configs.Capture,
|
||||
)
|
||||
c.managers.capture.Start()
|
||||
|
||||
c.managers.webRTC = webrtc.New(
|
||||
c.managers.desktop,
|
||||
c.managers.capture,
|
||||
&c.configs.WebRTC,
|
||||
)
|
||||
c.managers.webRTC.Start()
|
||||
|
||||
c.managers.webSocket = websocket.New(
|
||||
c.managers.session,
|
||||
c.managers.desktop,
|
||||
c.managers.capture,
|
||||
c.managers.webRTC,
|
||||
)
|
||||
c.managers.webSocket.Start()
|
||||
|
||||
c.managers.api = api.New(
|
||||
c.managers.session,
|
||||
c.managers.member,
|
||||
c.managers.desktop,
|
||||
c.managers.capture,
|
||||
)
|
||||
|
||||
c.managers.plugins = plugins.New(
|
||||
&c.configs.Plugins,
|
||||
)
|
||||
|
||||
// init and set configuration now
|
||||
// this means it won't be in --help
|
||||
c.managers.plugins.InitConfigs(cmd)
|
||||
c.managers.plugins.SetConfigs()
|
||||
|
||||
c.managers.plugins.Start(
|
||||
c.managers.session,
|
||||
c.managers.webSocket,
|
||||
c.managers.api,
|
||||
)
|
||||
|
||||
c.managers.http = http.New(
|
||||
c.managers.webSocket,
|
||||
c.managers.api,
|
||||
&c.configs.Server,
|
||||
)
|
||||
c.managers.http.Start()
|
||||
}
|
||||
|
||||
func (c *serve) Shutdown() {
|
||||
var err error
|
||||
|
||||
err = c.managers.http.Shutdown()
|
||||
c.logger.Err(err).Msg("http manager shutdown")
|
||||
|
||||
err = c.managers.plugins.Shutdown()
|
||||
c.logger.Err(err).Msg("plugins manager shutdown")
|
||||
|
||||
err = c.managers.webSocket.Shutdown()
|
||||
c.logger.Err(err).Msg("websocket manager shutdown")
|
||||
|
||||
err = c.managers.webRTC.Shutdown()
|
||||
c.logger.Err(err).Msg("webrtc manager shutdown")
|
||||
|
||||
err = c.managers.capture.Shutdown()
|
||||
c.logger.Err(err).Msg("capture manager shutdown")
|
||||
|
||||
err = c.managers.desktop.Shutdown()
|
||||
c.logger.Err(err).Msg("desktop manager shutdown")
|
||||
|
||||
err = c.managers.member.Disconnect()
|
||||
c.logger.Err(err).Msg("member manager disconnect")
|
||||
}
|
||||
|
||||
func (c *serve) Run(cmd *cobra.Command, args []string) {
|
||||
c.logger.Info().Msg("starting neko server")
|
||||
c.Start(cmd)
|
||||
c.logger.Info().Msg("neko ready")
|
||||
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, os.Interrupt)
|
||||
sig := <-quit
|
||||
|
||||
c.logger.Warn().Msgf("received %s, attempting graceful shutdown", sig)
|
||||
c.Shutdown()
|
||||
c.logger.Info().Msg("shutdown complete")
|
||||
}
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
cd "$(dirname "$0")"
|
||||
|
||||
if [ "$(docker images -q neko_server_build 2> /dev/null)" == "" ]; then
|
||||
echo "Image 'neko_server_build' not found. Run ./build first."
|
||||
exit 1
|
||||
echo "Image 'neko_server_build' not found. Run ./build first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
docker run -it --rm \
|
|
@ -2,8 +2,8 @@
|
|||
cd "$(dirname "$0")"
|
||||
|
||||
if [ "$(docker images -q neko_server_build 2> /dev/null)" == "" ]; then
|
||||
echo "Image 'neko_server_build' not found. Run ./build first."
|
||||
exit 1
|
||||
echo "Image 'neko_server_build' not found. Run ./build first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
docker run -it \
|
|
@ -2,8 +2,8 @@
|
|||
cd "$(dirname "$0")"
|
||||
|
||||
if [ "$(docker images -q neko_server_build 2> /dev/null)" == "" ]; then
|
||||
echo "Image 'neko_server_build' not found. Run ./build first."
|
||||
exit 1
|
||||
echo "Image 'neko_server_build' not found. Run ./build first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
#
|
|
@ -2,8 +2,8 @@
|
|||
cd "$(dirname "$0")"
|
||||
|
||||
if [ -z "$(docker images -q neko_server_app 2> /dev/null)" ]; then
|
||||
echo "Image 'neko_server_app' not found. Running ./build first."
|
||||
./build
|
||||
echo "Image 'neko_server_app' not found. Running ./build first."
|
||||
./build
|
||||
fi
|
||||
|
||||
if [ -z $NEKO_PORT ]; then
|
||||
|
@ -22,6 +22,10 @@ if [ -z $NEKO_NAT1TO1 ]; then
|
|||
fi
|
||||
done
|
||||
|
||||
if [ -z $NEKO_NAT1TO1 ]; then
|
||||
NEKO_NAT1TO1=$(hostname -I 2>/dev/null | awk '{print $1}')
|
||||
fi
|
||||
|
||||
if [ -z $NEKO_NAT1TO1 ]; then
|
||||
NEKO_NAT1TO1=$(hostname -i 2>/dev/null)
|
||||
fi
|
|
@ -1,55 +1,68 @@
|
|||
module m1k1o/neko
|
||||
module github.com/demodesk/neko
|
||||
|
||||
go 1.20
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/fsnotify/fsnotify v1.6.0
|
||||
github.com/go-chi/chi/v5 v5.0.10
|
||||
github.com/PaesslerAG/gval v1.2.2
|
||||
github.com/go-chi/chi v1.5.5
|
||||
github.com/go-chi/cors v1.2.1
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/pion/ice/v2 v2.3.0
|
||||
github.com/pion/interceptor v0.1.12
|
||||
github.com/gorilla/websocket v1.5.1
|
||||
github.com/kataras/go-events v0.0.3
|
||||
github.com/pion/ice/v2 v2.3.12
|
||||
github.com/pion/interceptor v0.1.25
|
||||
github.com/pion/logging v0.2.2
|
||||
github.com/pion/rtp v1.7.13 // indirect
|
||||
github.com/pion/srtp/v2 v2.0.12 // indirect
|
||||
github.com/pion/webrtc/v3 v3.1.55
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/rs/zerolog v1.29.0
|
||||
github.com/spf13/cast v1.5.0 // indirect
|
||||
github.com/spf13/cobra v1.6.1
|
||||
github.com/spf13/viper v1.15.0
|
||||
golang.org/x/crypto v0.6.0 // indirect
|
||||
golang.org/x/net v0.7.0 // indirect
|
||||
golang.org/x/sys v0.5.0 // indirect
|
||||
golang.org/x/text v0.7.0 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||
github.com/pion/rtcp v1.2.13
|
||||
github.com/pion/webrtc/v3 v3.2.24
|
||||
github.com/prometheus/client_golang v1.18.0
|
||||
github.com/rs/zerolog v1.31.0
|
||||
github.com/spf13/cobra v1.8.0
|
||||
github.com/spf13/viper v1.18.2
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/kr/pretty v0.3.1 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.17 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.1.1 // indirect
|
||||
github.com/pion/datachannel v1.5.5 // indirect
|
||||
github.com/pion/dtls/v2 v2.2.6 // indirect
|
||||
github.com/pion/mdns v0.0.7 // indirect
|
||||
github.com/pion/dtls/v2 v2.2.9 // indirect
|
||||
github.com/pion/mdns v0.0.9 // indirect
|
||||
github.com/pion/randutil v0.1.0 // indirect
|
||||
github.com/pion/rtcp v1.2.10 // indirect
|
||||
github.com/pion/sctp v1.8.6 // indirect
|
||||
github.com/pion/rtp v1.8.3 // indirect
|
||||
github.com/pion/sctp v1.8.9 // indirect
|
||||
github.com/pion/sdp/v3 v3.0.6 // indirect
|
||||
github.com/pion/stun v0.4.0 // indirect
|
||||
github.com/pion/transport/v2 v2.0.2 // indirect
|
||||
github.com/pion/turn/v2 v2.1.0 // indirect
|
||||
github.com/pion/udp/v2 v2.0.1 // indirect
|
||||
github.com/spf13/afero v1.9.4 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/pion/srtp/v2 v2.0.18 // indirect
|
||||
github.com/pion/stun v0.6.1 // indirect
|
||||
github.com/pion/transport/v2 v2.2.4 // indirect
|
||||
github.com/pion/turn/v2 v2.1.4 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus/client_model v0.5.0 // indirect
|
||||
github.com/prometheus/common v0.46.0 // indirect
|
||||
github.com/prometheus/procfs v0.12.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||
github.com/shopspring/decimal v1.3.1 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/afero v1.11.0 // indirect
|
||||
github.com/spf13/cast v1.6.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/subosito/gotenv v1.4.2 // indirect
|
||||
github.com/stretchr/testify v1.8.4 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/crypto v0.18.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a // indirect
|
||||
golang.org/x/net v0.20.0 // indirect
|
||||
golang.org/x/sys v0.16.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
google.golang.org/protobuf v1.32.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
|
611
server/go.sum
611
server/go.sum
|
@ -1,155 +1,57 @@
|
|||
cloud.google.com/go v0.26.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.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
|
||||
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
||||
cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
||||
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
|
||||
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
|
||||
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
|
||||
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
|
||||
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
|
||||
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
|
||||
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
|
||||
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
|
||||
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
|
||||
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
|
||||
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
|
||||
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
|
||||
cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=
|
||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
|
||||
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
|
||||
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
|
||||
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
|
||||
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
|
||||
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
||||
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
|
||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
|
||||
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
|
||||
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
|
||||
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
|
||||
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
|
||||
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
||||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||
cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/PaesslerAG/gval v1.2.2 h1:Y7iBzhgE09IGTt5QgGQ2IdaYYYOU134YGHBThD+wm9E=
|
||||
github.com/PaesslerAG/gval v1.2.2/go.mod h1:XRFLwvmkTEdYziLdaCeCa5ImcGVrfQbeNUbVR+C6xac=
|
||||
github.com/PaesslerAG/jsonpath v0.1.0 h1:gADYeifvlqK3R3i2cR5B4DGgxLXIPb3TRTH1mGi0jPI=
|
||||
github.com/PaesslerAG/jsonpath v0.1.0/go.mod h1:4BzmtoM/PI8fPO4aQGIusjGxGir2BzcV0grWtFzq1Y8=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
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/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
|
||||
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
|
||||
github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk=
|
||||
github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/go-chi/chi v1.5.5 h1:vOB/HbEMt9QqBqErz07QehcOKHaWFtuj87tTDVz2qXE=
|
||||
github.com/go-chi/chi v1.5.5/go.mod h1:C9JqLr3tIYjDOZpzn+BCuxY8z8vmca43EeMgyZt7irw=
|
||||
github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
|
||||
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
||||
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
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-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
|
||||
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
|
||||
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/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||
github.com/kataras/go-events v0.0.3 h1:o5YK53uURXtrlg7qE/vovxd/yKOJcLuFtPQbf1rYMC4=
|
||||
github.com/kataras/go-events v0.0.3/go.mod h1:bFBgtzwwzrag7kQmGuU1ZaVxhK2qseYPQomXoVEMsj4=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
|
@ -158,13 +60,12 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
|||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
|
||||
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
|
@ -176,425 +77,221 @@ github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042
|
|||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
|
||||
github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU=
|
||||
github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
|
||||
github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI=
|
||||
github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
||||
github.com/pion/datachannel v1.5.5 h1:10ef4kwdjije+M9d7Xm9im2Y3O6A6ccQb0zcqZcJew8=
|
||||
github.com/pion/datachannel v1.5.5/go.mod h1:iMz+lECmfdCMqFRhXhcA/219B0SQlbpoR2V118yimL0=
|
||||
github.com/pion/dtls/v2 v2.2.4/go.mod h1:WGKfxqhrddne4Kg3p11FUMJrynkOY4lb25zHNO49wuw=
|
||||
github.com/pion/dtls/v2 v2.2.6 h1:yXMxKr0Skd+Ub6A8UqXTRLSywskx93ooMRHsQUtd+Z4=
|
||||
github.com/pion/dtls/v2 v2.2.6/go.mod h1:t8fWJCIquY5rlQZwA2yWxUS1+OCrAdXrhVKXB5oD/wY=
|
||||
github.com/pion/ice/v2 v2.3.0 h1:G+ysriabk1p9wbySDpdsnlD+6ZspLlDLagRduRfzJPk=
|
||||
github.com/pion/ice/v2 v2.3.0/go.mod h1:+xO/cXVnnVUr6D2ZJcCT5g9LngucUkkTvfnTMqUxKRM=
|
||||
github.com/pion/interceptor v0.1.12 h1:CslaNriCFUItiXS5o+hh5lpL0t0ytQkFnUcbbCs2Zq8=
|
||||
github.com/pion/interceptor v0.1.12/go.mod h1:bDtgAD9dRkBZpWHGKaoKb42FhDHTG2rX8Ii9LRALLVA=
|
||||
github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
|
||||
github.com/pion/dtls/v2 v2.2.9 h1:K+D/aVf9/REahQvqk6G5JavdrD8W1PWDKC11UlwN7ts=
|
||||
github.com/pion/dtls/v2 v2.2.9/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
|
||||
github.com/pion/ice/v2 v2.3.11/go.mod h1:hPcLC3kxMa+JGRzMHqQzjoSj3xtE9F+eoncmXLlCL4E=
|
||||
github.com/pion/ice/v2 v2.3.12 h1:NWKW2b3+oSZS3klbQMIEWQ0i52Kuo0KBg505a5kQv4s=
|
||||
github.com/pion/ice/v2 v2.3.12/go.mod h1:hPcLC3kxMa+JGRzMHqQzjoSj3xtE9F+eoncmXLlCL4E=
|
||||
github.com/pion/interceptor v0.1.25 h1:pwY9r7P6ToQ3+IF0bajN0xmk/fNw/suTgaTdlwTDmhc=
|
||||
github.com/pion/interceptor v0.1.25/go.mod h1:wkbPYAak5zKsfpVDYMtEfWEy8D4zL+rpxCxPImLOg3Y=
|
||||
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.7 h1:P0UB4Sr6xDWEox0kTVxF0LmQihtCbSAdW0H2nEgkA3U=
|
||||
github.com/pion/mdns v0.0.7/go.mod h1:4iP2UbeFhLI/vWju/bw6ZfwjJzk0z8DNValjGxR/dD8=
|
||||
github.com/pion/mdns v0.0.8/go.mod h1:hYE72WX8WDveIhg7fmXgMKivD3Puklk0Ymzog0lSyaI=
|
||||
github.com/pion/mdns v0.0.9 h1:7Ue5KZsqq8EuqStnpPWV33vYYEH0+skdDN5L7EiEsI4=
|
||||
github.com/pion/mdns v0.0.9/go.mod h1:2JA5exfxwzXiCihmxpTKgFUpiQws2MnipoPK09vecIc=
|
||||
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
||||
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
||||
github.com/pion/rtcp v1.2.10 h1:nkr3uj+8Sp97zyItdN60tE/S6vk4al5CPRR6Gejsdjc=
|
||||
github.com/pion/rtcp v1.2.10/go.mod h1:ztfEwXZNLGyF1oQDttz/ZKIBaeeg/oWbRYqzBM9TL1I=
|
||||
github.com/pion/rtp v1.7.13 h1:qcHwlmtiI50t1XivvoawdCGTP4Uiypzfrsap+bijcoA=
|
||||
github.com/pion/rtp v1.7.13/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
|
||||
github.com/pion/rtcp v1.2.12/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
|
||||
github.com/pion/rtcp v1.2.13 h1:+EQijuisKwm/8VBs8nWllr0bIndR7Lf7cZG200mpbNo=
|
||||
github.com/pion/rtcp v1.2.13/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
|
||||
github.com/pion/rtp v1.8.2/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
|
||||
github.com/pion/rtp v1.8.3 h1:VEHxqzSVQxCkKDSHro5/4IUUG1ea+MFdqR2R3xSpNU8=
|
||||
github.com/pion/rtp v1.8.3/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
|
||||
github.com/pion/sctp v1.8.5/go.mod h1:SUFFfDpViyKejTAdwD1d/HQsCu+V/40cCs2nZIvC3s0=
|
||||
github.com/pion/sctp v1.8.6 h1:CUex11Vkt9YS++VhLf8b55O3VqKrWL6W3SDwX4jAqsI=
|
||||
github.com/pion/sctp v1.8.6/go.mod h1:SUFFfDpViyKejTAdwD1d/HQsCu+V/40cCs2nZIvC3s0=
|
||||
github.com/pion/sctp v1.8.8/go.mod h1:igF9nZBrjh5AtmKc7U30jXltsFHicFCXSmWA2GWRaWs=
|
||||
github.com/pion/sctp v1.8.9 h1:TP5ZVxV5J7rz7uZmbyvnUvsn7EJ2x/5q9uhsTtXbI3g=
|
||||
github.com/pion/sctp v1.8.9/go.mod h1:cMLT45jqw3+jiJCrtHVwfQLnfR0MGZ4rgOJwUOIqLkI=
|
||||
github.com/pion/sdp/v3 v3.0.6 h1:WuDLhtuFUUVpTfus9ILC4HRyHsW6TdugjEX/QY9OiUw=
|
||||
github.com/pion/sdp/v3 v3.0.6/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0YQw=
|
||||
github.com/pion/srtp/v2 v2.0.12 h1:WrmiVCubGMOAObBU1vwWjG0H3VSyQHawKeer2PVA5rY=
|
||||
github.com/pion/srtp/v2 v2.0.12/go.mod h1:C3Ep44hlOo2qEYaq4ddsmK5dL63eLehXFbHaZ9F5V9Y=
|
||||
github.com/pion/stun v0.4.0 h1:vgRrbBE2htWHy7l3Zsxckk7rkjnjOsSM7PHZnBwo8rk=
|
||||
github.com/pion/stun v0.4.0/go.mod h1:QPsh1/SbXASntw3zkkrIk3ZJVKz4saBY2G7S10P3wCw=
|
||||
github.com/pion/srtp/v2 v2.0.18 h1:vKpAXfawO9RtTRKZJbG4y0v1b11NZxQnxRl85kGuUlo=
|
||||
github.com/pion/srtp/v2 v2.0.18/go.mod h1:0KJQjA99A6/a0DOVTu1PhDSw0CXF2jTkqOoMg3ODqdA=
|
||||
github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4=
|
||||
github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8=
|
||||
github.com/pion/transport v0.14.1 h1:XSM6olwW+o8J4SCmOBb/BpwZypkHeyM0PGFCxNQBr40=
|
||||
github.com/pion/transport v0.14.1/go.mod h1:4tGmbk00NeYA3rUa9+n+dzCCoKkcy3YlYb99Jn2fNnI=
|
||||
github.com/pion/transport/v2 v2.0.0/go.mod h1:HS2MEBJTwD+1ZI2eSXSvHJx/HnzQqRy2/LXxt6eVMHc=
|
||||
github.com/pion/transport/v2 v2.0.1/go.mod h1:93OYg91+mrGxKW+Jrgzmqr80kgXqD7J0yybOrdr7w0Y=
|
||||
github.com/pion/transport/v2 v2.0.2 h1:St+8o+1PEzPT51O9bv+tH/KYYLMNR5Vwm5Z3Qkjsywg=
|
||||
github.com/pion/transport/v2 v2.0.2/go.mod h1:vrz6bUbFr/cjdwbnxq8OdDDzHf7JJfGsIRkxfpZoTA0=
|
||||
github.com/pion/turn/v2 v2.1.0 h1:5wGHSgGhJhP/RpabkUb/T9PdsAjkGLS6toYz5HNzoSI=
|
||||
github.com/pion/turn/v2 v2.1.0/go.mod h1:yrT5XbXSGX1VFSF31A3c1kCNB5bBZgk/uu5LET162qs=
|
||||
github.com/pion/udp v0.1.4/go.mod h1:G8LDo56HsFwC24LIcnT4YIDU5qcB6NepqqjP0keL2us=
|
||||
github.com/pion/udp/v2 v2.0.1 h1:xP0z6WNux1zWEjhC7onRA3EwwSliXqu1ElUZAQhUP54=
|
||||
github.com/pion/udp/v2 v2.0.1/go.mod h1:B7uvTMP00lzWdyMr/1PVZXtV3wpPIxBRd4Wl6AksXn8=
|
||||
github.com/pion/webrtc/v3 v3.1.55 h1:jQt98hZ8DUi/l/s/rtogthBdsKKvKekFgZCX9hMEqRo=
|
||||
github.com/pion/webrtc/v3 v3.1.55/go.mod h1:M1gU5mnvvo4e1nnLvF23esYz0nZAFOtbU/wq44MSfbc=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g=
|
||||
github.com/pion/transport/v2 v2.2.2/go.mod h1:OJg3ojoBJopjEeECq2yJdXH9YVrUJ1uQ++NjXLOUorc=
|
||||
github.com/pion/transport/v2 v2.2.3/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
|
||||
github.com/pion/transport/v2 v2.2.4 h1:41JJK6DZQYSeVLxILA2+F4ZkKb4Xd/tFJZRFZQ9QAlo=
|
||||
github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
|
||||
github.com/pion/transport/v3 v3.0.1 h1:gDTlPJwROfSfz6QfSi0ZmeCSkFcnWWiiR9ES0ouANiM=
|
||||
github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0=
|
||||
github.com/pion/turn/v2 v2.1.3/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY=
|
||||
github.com/pion/turn/v2 v2.1.4 h1:2xn8rduI5W6sCZQkEnIUDAkrBQNl2eYIBCHMZ3QMmP8=
|
||||
github.com/pion/turn/v2 v2.1.4/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY=
|
||||
github.com/pion/webrtc/v3 v3.2.24 h1:MiFL5DMo2bDaaIFWr0DDpwiV/L4EGbLZb+xoRvfEo1Y=
|
||||
github.com/pion/webrtc/v3 v3.2.24/go.mod h1:1CaT2fcZzZ6VZA+O1i9yK2DU4EOcXVvSbWG9pr5jefs=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
|
||||
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_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.29.0 h1:Zes4hju04hjbvkVkOhdl2HpZa+0PmVwigmo8XoORE5w=
|
||||
github.com/rs/zerolog v1.29.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk=
|
||||
github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA=
|
||||
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
|
||||
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
|
||||
github.com/prometheus/common v0.46.0 h1:doXzt5ybi1HBKpsZOL0sSkaNHJJqkyfEWZGGqqScV0Y=
|
||||
github.com/prometheus/common v0.46.0/go.mod h1:Tp0qkxpb9Jsg54QMe+EAmqXkSV7Evdy1BTn+g2pa/hQ=
|
||||
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
|
||||
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A=
|
||||
github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
|
||||
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
|
||||
github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
|
||||
github.com/spf13/afero v1.9.4 h1:Sd43wM1IWz/s1aVXdOBkjJvuP8UdyqioeE4AmM0QsBs=
|
||||
github.com/spf13/afero v1.9.4/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
|
||||
github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
|
||||
github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
|
||||
github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA=
|
||||
github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
|
||||
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
|
||||
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
|
||||
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
|
||||
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
|
||||
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
|
||||
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
|
||||
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
|
||||
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
|
||||
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.15.0 h1:js3yy885G8xwJa6iOISGFwd+qlUo5AvyXb7CiihdtiU=
|
||||
github.com/spf13/viper v1.15.0/go.mod h1:fFcTBJxvhhzSJiZy8n+PeW6t8l+KeT/uTARa0jHOQLA=
|
||||
github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=
|
||||
github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
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/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8=
|
||||
github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
|
||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
|
||||
golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
|
||||
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
|
||||
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
|
||||
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
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-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
|
||||
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
|
||||
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
|
||||
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
|
||||
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
|
||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a h1:Q8/wZp0KX97QFTc2ywcOE0YRjZPVIx+MXInMzdvQqcA=
|
||||
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
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/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/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-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/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-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/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/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
|
||||
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
|
||||
golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
|
||||
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
|
||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||
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/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/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-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/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/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo=
|
||||
golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
|
||||
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
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/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
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/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/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-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
|
||||
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
|
||||
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
|
||||
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
|
||||
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
|
||||
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
|
||||
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
||||
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
|
||||
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
|
||||
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
|
||||
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
|
||||
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
|
||||
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
|
||||
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
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/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
|
@ -606,13 +303,3 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
|||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
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=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||
|
|
|
@ -22,7 +22,7 @@ func (h *RoomHandler) broadcastStatus(w http.ResponseWriter, r *http.Request) er
|
|||
})
|
||||
}
|
||||
|
||||
func (h *RoomHandler) boradcastStart(w http.ResponseWriter, r *http.Request) error {
|
||||
func (h *RoomHandler) broadcastStart(w http.ResponseWriter, r *http.Request) error {
|
||||
data := &BroadcastStatusPayload{}
|
||||
if err := utils.HttpJsonRequest(w, r, data); err != nil {
|
||||
return err
|
||||
|
@ -42,7 +42,7 @@ func (h *RoomHandler) boradcastStart(w http.ResponseWriter, r *http.Request) err
|
|||
}
|
||||
|
||||
h.sessions.AdminBroadcast(
|
||||
event.BORADCAST_STATUS,
|
||||
event.BROADCAST_STATUS,
|
||||
message.BroadcastStatus{
|
||||
IsActive: broadcast.Started(),
|
||||
URL: broadcast.Url(),
|
||||
|
@ -51,7 +51,7 @@ func (h *RoomHandler) boradcastStart(w http.ResponseWriter, r *http.Request) err
|
|||
return utils.HttpSuccess(w)
|
||||
}
|
||||
|
||||
func (h *RoomHandler) boradcastStop(w http.ResponseWriter, r *http.Request) error {
|
||||
func (h *RoomHandler) broadcastStop(w http.ResponseWriter, r *http.Request) error {
|
||||
broadcast := h.capture.Broadcast()
|
||||
if !broadcast.Started() {
|
||||
return utils.HttpUnprocessableEntity("server is not broadcasting")
|
||||
|
@ -60,7 +60,7 @@ func (h *RoomHandler) boradcastStop(w http.ResponseWriter, r *http.Request) erro
|
|||
broadcast.Stop()
|
||||
|
||||
h.sessions.AdminBroadcast(
|
||||
event.BORADCAST_STATUS,
|
||||
event.BROADCAST_STATUS,
|
||||
message.BroadcastStatus{
|
||||
IsActive: broadcast.Started(),
|
||||
URL: broadcast.Url(),
|
|
@ -6,6 +6,8 @@ import (
|
|||
"github.com/go-chi/chi"
|
||||
|
||||
"github.com/demodesk/neko/pkg/auth"
|
||||
"github.com/demodesk/neko/pkg/types/event"
|
||||
"github.com/demodesk/neko/pkg/types/message"
|
||||
"github.com/demodesk/neko/pkg/utils"
|
||||
)
|
||||
|
||||
|
@ -33,17 +35,26 @@ func (h *RoomHandler) controlStatus(w http.ResponseWriter, r *http.Request) erro
|
|||
}
|
||||
|
||||
func (h *RoomHandler) controlRequest(w http.ResponseWriter, r *http.Request) error {
|
||||
_, hasHost := h.sessions.GetHost()
|
||||
session, _ := auth.GetSession(r)
|
||||
host, hasHost := h.sessions.GetHost()
|
||||
if hasHost {
|
||||
return utils.HttpUnprocessableEntity("there is already a host")
|
||||
// TODO: Some throttling mechanism to prevent spamming.
|
||||
|
||||
// let host know that someone wants to take control
|
||||
host.Send(
|
||||
event.CONTROL_REQUEST,
|
||||
message.SessionID{
|
||||
ID: session.ID(),
|
||||
})
|
||||
|
||||
return utils.HttpError(http.StatusAccepted, "control request sent")
|
||||
}
|
||||
|
||||
session, _ := auth.GetSession(r)
|
||||
if h.sessions.Settings().LockedControls && !session.Profile().IsAdmin {
|
||||
return utils.HttpForbidden("controls are locked")
|
||||
}
|
||||
|
||||
h.sessions.SetHost(session)
|
||||
session.SetAsHost()
|
||||
|
||||
return utils.HttpSuccess(w)
|
||||
}
|
||||
|
@ -55,19 +66,20 @@ func (h *RoomHandler) controlRelease(w http.ResponseWriter, r *http.Request) err
|
|||
}
|
||||
|
||||
h.desktop.ResetKeys()
|
||||
h.sessions.ClearHost()
|
||||
session.ClearHost()
|
||||
|
||||
return utils.HttpSuccess(w)
|
||||
}
|
||||
|
||||
func (h *RoomHandler) controlTake(w http.ResponseWriter, r *http.Request) error {
|
||||
session, _ := auth.GetSession(r)
|
||||
h.sessions.SetHost(session)
|
||||
session.SetAsHost()
|
||||
|
||||
return utils.HttpSuccess(w)
|
||||
}
|
||||
|
||||
func (h *RoomHandler) controlGive(w http.ResponseWriter, r *http.Request) error {
|
||||
session, _ := auth.GetSession(r)
|
||||
sessionId := chi.URLParam(r, "sessionId")
|
||||
|
||||
target, ok := h.sessions.Get(sessionId)
|
||||
|
@ -79,17 +91,18 @@ func (h *RoomHandler) controlGive(w http.ResponseWriter, r *http.Request) error
|
|||
return utils.HttpBadRequest("target session is not allowed to host")
|
||||
}
|
||||
|
||||
h.sessions.SetHost(target)
|
||||
target.SetAsHostBy(session)
|
||||
|
||||
return utils.HttpSuccess(w)
|
||||
}
|
||||
|
||||
func (h *RoomHandler) controlReset(w http.ResponseWriter, r *http.Request) error {
|
||||
session, _ := auth.GetSession(r)
|
||||
_, hasHost := h.sessions.GetHost()
|
||||
|
||||
if hasHost {
|
||||
h.desktop.ResetKeys()
|
||||
h.sessions.ClearHost()
|
||||
session.ClearHost()
|
||||
}
|
||||
|
||||
return utils.HttpSuccess(w)
|
|
@ -31,7 +31,7 @@ func New(
|
|||
}
|
||||
|
||||
// generate fallback image for private mode when needed
|
||||
sessions.OnSettingsChanged(func(new types.Settings, old types.Settings) {
|
||||
sessions.OnSettingsChanged(func(session types.Session, new, old types.Settings) {
|
||||
if old.PrivateMode && !new.PrivateMode {
|
||||
log.Debug().Msg("clearing private mode fallback image")
|
||||
h.privateModeImage = nil
|
||||
|
@ -62,8 +62,8 @@ func (h *RoomHandler) Route(r types.Router) {
|
|||
|
||||
r.With(auth.AdminsOnly).Route("/broadcast", func(r types.Router) {
|
||||
r.Get("/", h.broadcastStatus)
|
||||
r.Post("/start", h.boradcastStart)
|
||||
r.Post("/stop", h.boradcastStop)
|
||||
r.Post("/start", h.broadcastStart)
|
||||
r.Post("/stop", h.broadcastStop)
|
||||
})
|
||||
|
||||
r.With(auth.CanAccessClipboardOnly).With(auth.HostsOnly).Route("/clipboard", func(r types.Router) {
|
||||
|
@ -95,7 +95,7 @@ func (h *RoomHandler) Route(r types.Router) {
|
|||
r.Post("/release", h.controlRelease)
|
||||
|
||||
r.With(auth.AdminsOnly).Post("/take", h.controlTake)
|
||||
r.With(auth.AdminsOnly).Post("/give/{sessionId}", h.controlGive)
|
||||
r.With(auth.HostsOrAdminsOnly).Post("/give/{sessionId}", h.controlGive)
|
||||
r.With(auth.AdminsOnly).Post("/reset", h.controlReset)
|
||||
})
|
||||
|
|
@ -7,21 +7,13 @@ import (
|
|||
"github.com/demodesk/neko/pkg/utils"
|
||||
)
|
||||
|
||||
type KeyboardMapData struct {
|
||||
types.KeyboardMap
|
||||
}
|
||||
|
||||
type KeyboardModifiersData struct {
|
||||
types.KeyboardModifiers
|
||||
}
|
||||
|
||||
func (h *RoomHandler) keyboardMapSet(w http.ResponseWriter, r *http.Request) error {
|
||||
data := &KeyboardMapData{}
|
||||
if err := utils.HttpJsonRequest(w, r, data); err != nil {
|
||||
keyboardMap := types.KeyboardMap{}
|
||||
if err := utils.HttpJsonRequest(w, r, &keyboardMap); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err := h.desktop.SetKeyboardMap(data.KeyboardMap)
|
||||
err := h.desktop.SetKeyboardMap(keyboardMap)
|
||||
if err != nil {
|
||||
return utils.HttpInternalServerError().WithInternalErr(err)
|
||||
}
|
||||
|
@ -30,28 +22,26 @@ func (h *RoomHandler) keyboardMapSet(w http.ResponseWriter, r *http.Request) err
|
|||
}
|
||||
|
||||
func (h *RoomHandler) keyboardMapGet(w http.ResponseWriter, r *http.Request) error {
|
||||
data, err := h.desktop.GetKeyboardMap()
|
||||
keyboardMap, err := h.desktop.GetKeyboardMap()
|
||||
if err != nil {
|
||||
return utils.HttpInternalServerError().WithInternalErr(err)
|
||||
}
|
||||
|
||||
return utils.HttpSuccess(w, KeyboardMapData{
|
||||
KeyboardMap: *data,
|
||||
})
|
||||
return utils.HttpSuccess(w, keyboardMap)
|
||||
}
|
||||
|
||||
func (h *RoomHandler) keyboardModifiersSet(w http.ResponseWriter, r *http.Request) error {
|
||||
data := &KeyboardModifiersData{}
|
||||
if err := utils.HttpJsonRequest(w, r, data); err != nil {
|
||||
keyboardModifiers := types.KeyboardModifiers{}
|
||||
if err := utils.HttpJsonRequest(w, r, &keyboardModifiers); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
h.desktop.SetKeyboardModifiers(data.KeyboardModifiers)
|
||||
h.desktop.SetKeyboardModifiers(keyboardModifiers)
|
||||
return utils.HttpSuccess(w)
|
||||
}
|
||||
|
||||
func (h *RoomHandler) keyboardModifiersGet(w http.ResponseWriter, r *http.Request) error {
|
||||
return utils.HttpSuccess(w, KeyboardModifiersData{
|
||||
KeyboardModifiers: h.desktop.GetKeyboardModifiers(),
|
||||
})
|
||||
keyboardModifiers := h.desktop.GetKeyboardModifiers()
|
||||
|
||||
return utils.HttpSuccess(w, keyboardModifiers)
|
||||
}
|
|
@ -11,24 +11,16 @@ import (
|
|||
"github.com/demodesk/neko/pkg/utils"
|
||||
)
|
||||
|
||||
type ScreenConfigurationPayload struct {
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
Rate int16 `json:"rate"`
|
||||
}
|
||||
|
||||
func (h *RoomHandler) screenConfiguration(w http.ResponseWriter, r *http.Request) error {
|
||||
size := h.desktop.GetScreenSize()
|
||||
screenSize := h.desktop.GetScreenSize()
|
||||
|
||||
return utils.HttpSuccess(w, ScreenConfigurationPayload{
|
||||
Width: size.Width,
|
||||
Height: size.Height,
|
||||
Rate: size.Rate,
|
||||
})
|
||||
return utils.HttpSuccess(w, screenSize)
|
||||
}
|
||||
|
||||
func (h *RoomHandler) screenConfigurationChange(w http.ResponseWriter, r *http.Request) error {
|
||||
data := &ScreenConfigurationPayload{}
|
||||
auth, _ := auth.GetSession(r)
|
||||
|
||||
data := &types.ScreenSize{}
|
||||
if err := utils.HttpJsonRequest(w, r, data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -43,10 +35,9 @@ func (h *RoomHandler) screenConfigurationChange(w http.ResponseWriter, r *http.R
|
|||
return utils.HttpUnprocessableEntity("cannot set screen size").WithInternalErr(err)
|
||||
}
|
||||
|
||||
h.sessions.Broadcast(event.SCREEN_UPDATED, message.ScreenSize{
|
||||
Width: size.Width,
|
||||
Height: size.Height,
|
||||
Rate: size.Rate,
|
||||
h.sessions.Broadcast(event.SCREEN_UPDATED, message.ScreenSizeUpdate{
|
||||
ID: auth.ID(),
|
||||
ScreenSize: size,
|
||||
})
|
||||
|
||||
return utils.HttpSuccess(w, data)
|
||||
|
@ -56,16 +47,7 @@ func (h *RoomHandler) screenConfigurationChange(w http.ResponseWriter, r *http.R
|
|||
func (h *RoomHandler) screenConfigurationsList(w http.ResponseWriter, r *http.Request) error {
|
||||
configurations := h.desktop.ScreenConfigurations()
|
||||
|
||||
list := make([]ScreenConfigurationPayload, 0, len(configurations))
|
||||
for _, conf := range configurations {
|
||||
list = append(list, ScreenConfigurationPayload{
|
||||
Width: conf.Width,
|
||||
Height: conf.Height,
|
||||
Rate: conf.Rate,
|
||||
})
|
||||
}
|
||||
|
||||
return utils.HttpSuccess(w, list)
|
||||
return utils.HttpSuccess(w, configurations)
|
||||
}
|
||||
|
||||
func (h *RoomHandler) screenShotGet(w http.ResponseWriter, r *http.Request) error {
|
38
server/internal/api/room/settings.go
Normal file
38
server/internal/api/room/settings.go
Normal file
|
@ -0,0 +1,38 @@
|
|||
package room
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/demodesk/neko/pkg/auth"
|
||||
"github.com/demodesk/neko/pkg/types"
|
||||
"github.com/demodesk/neko/pkg/utils"
|
||||
)
|
||||
|
||||
func (h *RoomHandler) settingsGet(w http.ResponseWriter, r *http.Request) error {
|
||||
settings := h.sessions.Settings()
|
||||
return utils.HttpSuccess(w, settings)
|
||||
}
|
||||
|
||||
func (h *RoomHandler) settingsSet(w http.ResponseWriter, r *http.Request) error {
|
||||
session, _ := auth.GetSession(r)
|
||||
|
||||
// We read the request body first and unmashal it inside the UpdateSettingsFunc
|
||||
// to ensure atomicity of the operation.
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
return utils.HttpBadRequest("unable to read request body").WithInternalErr(err)
|
||||
}
|
||||
|
||||
h.sessions.UpdateSettingsFunc(session, func(settings *types.Settings) bool {
|
||||
err = json.Unmarshal(body, settings)
|
||||
return err == nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return utils.HttpBadRequest("unable to parse provided data").WithInternalErr(err)
|
||||
}
|
||||
|
||||
return utils.HttpSuccess(w)
|
||||
}
|
|
@ -7,6 +7,7 @@ import (
|
|||
|
||||
"github.com/demodesk/neko/internal/api/members"
|
||||
"github.com/demodesk/neko/internal/api/room"
|
||||
"github.com/demodesk/neko/internal/api/sessions"
|
||||
"github.com/demodesk/neko/pkg/auth"
|
||||
"github.com/demodesk/neko/pkg/types"
|
||||
"github.com/demodesk/neko/pkg/utils"
|
||||
|
@ -45,7 +46,10 @@ func (api *ApiManagerCtx) Route(r types.Router) {
|
|||
|
||||
r.Post("/logout", api.Logout)
|
||||
r.Get("/whoami", api.Whoami)
|
||||
r.Get("/sessions", api.Sessions)
|
||||
r.Post("/profile", api.UpdateProfile)
|
||||
|
||||
sessionsHandler := sessions.New(api.sessions)
|
||||
r.Route("/sessions", sessionsHandler.Route)
|
||||
|
||||
membersHandler := members.New(api.members)
|
||||
r.Route("/members", membersHandler.Route)
|
|
@ -33,6 +33,8 @@ func (api *ApiManagerCtx) Login(w http.ResponseWriter, r *http.Request) error {
|
|||
return utils.HttpUnprocessableEntity("session already connected")
|
||||
} else if errors.Is(err, types.ErrMemberDoesNotExist) || errors.Is(err, types.ErrMemberInvalidPassword) {
|
||||
return utils.HttpUnauthorized().WithInternalErr(err)
|
||||
} else if errors.Is(err, types.ErrSessionLoginsLocked) {
|
||||
return utils.HttpForbidden("logins are locked").WithInternalErr(err)
|
||||
} else {
|
||||
return utils.HttpInternalServerError().WithInternalErr(err)
|
||||
}
|
||||
|
@ -82,15 +84,22 @@ func (api *ApiManagerCtx) Whoami(w http.ResponseWriter, r *http.Request) error {
|
|||
})
|
||||
}
|
||||
|
||||
func (api *ApiManagerCtx) Sessions(w http.ResponseWriter, r *http.Request) error {
|
||||
sessions := []SessionDataPayload{}
|
||||
for _, session := range api.sessions.List() {
|
||||
sessions = append(sessions, SessionDataPayload{
|
||||
ID: session.ID(),
|
||||
Profile: session.Profile(),
|
||||
State: session.State(),
|
||||
})
|
||||
func (api *ApiManagerCtx) UpdateProfile(w http.ResponseWriter, r *http.Request) error {
|
||||
session, _ := auth.GetSession(r)
|
||||
|
||||
data := session.Profile()
|
||||
if err := utils.HttpJsonRequest(w, r, &data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return utils.HttpSuccess(w, sessions)
|
||||
err := api.sessions.Update(session.ID(), data)
|
||||
if err != nil {
|
||||
if errors.Is(err, types.ErrSessionNotFound) {
|
||||
return utils.HttpBadRequest("session does not exist")
|
||||
} else {
|
||||
return utils.HttpInternalServerError().WithInternalErr(err)
|
||||
}
|
||||
}
|
||||
|
||||
return utils.HttpSuccess(w, true)
|
||||
}
|
80
server/internal/api/sessions/controller.go
Normal file
80
server/internal/api/sessions/controller.go
Normal file
|
@ -0,0 +1,80 @@
|
|||
package sessions
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/demodesk/neko/pkg/auth"
|
||||
"github.com/demodesk/neko/pkg/types"
|
||||
"github.com/demodesk/neko/pkg/utils"
|
||||
"github.com/go-chi/chi"
|
||||
)
|
||||
|
||||
type SessionDataPayload struct {
|
||||
ID string `json:"id"`
|
||||
Profile types.MemberProfile `json:"profile"`
|
||||
State types.SessionState `json:"state"`
|
||||
}
|
||||
|
||||
func (h *SessionsHandler) sessionsList(w http.ResponseWriter, r *http.Request) error {
|
||||
sessions := []SessionDataPayload{}
|
||||
for _, session := range h.sessions.List() {
|
||||
sessions = append(sessions, SessionDataPayload{
|
||||
ID: session.ID(),
|
||||
Profile: session.Profile(),
|
||||
State: session.State(),
|
||||
})
|
||||
}
|
||||
|
||||
return utils.HttpSuccess(w, sessions)
|
||||
}
|
||||
|
||||
func (h *SessionsHandler) sessionsRead(w http.ResponseWriter, r *http.Request) error {
|
||||
sessionId := chi.URLParam(r, "sessionId")
|
||||
|
||||
session, ok := h.sessions.Get(sessionId)
|
||||
if !ok {
|
||||
return utils.HttpNotFound("session not found")
|
||||
}
|
||||
|
||||
return utils.HttpSuccess(w, SessionDataPayload{
|
||||
ID: session.ID(),
|
||||
Profile: session.Profile(),
|
||||
State: session.State(),
|
||||
})
|
||||
}
|
||||
|
||||
func (h *SessionsHandler) sessionsDelete(w http.ResponseWriter, r *http.Request) error {
|
||||
session, _ := auth.GetSession(r)
|
||||
|
||||
sessionId := chi.URLParam(r, "sessionId")
|
||||
if sessionId == session.ID() {
|
||||
return utils.HttpBadRequest("cannot delete own session")
|
||||
}
|
||||
|
||||
err := h.sessions.Delete(sessionId)
|
||||
if err != nil {
|
||||
if errors.Is(err, types.ErrSessionNotFound) {
|
||||
return utils.HttpBadRequest("session not found")
|
||||
} else {
|
||||
return utils.HttpInternalServerError().WithInternalErr(err)
|
||||
}
|
||||
}
|
||||
|
||||
return utils.HttpSuccess(w)
|
||||
}
|
||||
|
||||
func (h *SessionsHandler) sessionsDisconnect(w http.ResponseWriter, r *http.Request) error {
|
||||
sessionId := chi.URLParam(r, "sessionId")
|
||||
|
||||
err := h.sessions.Disconnect(sessionId)
|
||||
if err != nil {
|
||||
if errors.Is(err, types.ErrSessionNotFound) {
|
||||
return utils.HttpBadRequest("session not found")
|
||||
} else {
|
||||
return utils.HttpInternalServerError().WithInternalErr(err)
|
||||
}
|
||||
}
|
||||
|
||||
return utils.HttpSuccess(w)
|
||||
}
|
30
server/internal/api/sessions/handler.go
Normal file
30
server/internal/api/sessions/handler.go
Normal file
|
@ -0,0 +1,30 @@
|
|||
package sessions
|
||||
|
||||
import (
|
||||
"github.com/demodesk/neko/pkg/auth"
|
||||
"github.com/demodesk/neko/pkg/types"
|
||||
)
|
||||
|
||||
type SessionsHandler struct {
|
||||
sessions types.SessionManager
|
||||
}
|
||||
|
||||
func New(
|
||||
sessions types.SessionManager,
|
||||
) *SessionsHandler {
|
||||
// Init
|
||||
|
||||
return &SessionsHandler{
|
||||
sessions: sessions,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *SessionsHandler) Route(r types.Router) {
|
||||
r.Get("/", h.sessionsList)
|
||||
|
||||
r.With(auth.AdminsOnly).Route("/{sessionId}", func(r types.Router) {
|
||||
r.Get("/", h.sessionsRead)
|
||||
r.Delete("/", h.sessionsDelete)
|
||||
r.Post("/disconnect", h.sessionsDisconnect)
|
||||
})
|
||||
}
|
|
@ -3,26 +3,32 @@ package capture
|
|||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"m1k1o/neko/internal/capture/gst"
|
||||
"m1k1o/neko/internal/types"
|
||||
"github.com/demodesk/neko/pkg/gst"
|
||||
"github.com/demodesk/neko/pkg/types"
|
||||
)
|
||||
|
||||
type BroacastManagerCtx struct {
|
||||
logger zerolog.Logger
|
||||
mu sync.Mutex
|
||||
|
||||
pipeline *gst.Pipeline
|
||||
pipeline gst.Pipeline
|
||||
pipelineMu sync.Mutex
|
||||
pipelineFn func(url string) (string, error)
|
||||
|
||||
url string
|
||||
started bool
|
||||
|
||||
// metrics
|
||||
pipelinesCounter prometheus.Counter
|
||||
pipelinesActive prometheus.Gauge
|
||||
}
|
||||
|
||||
func broadcastNew(pipelineFn func(url string) (string, error), url string, started bool) *BroacastManagerCtx {
|
||||
func broadcastNew(pipelineFn func(url string) (string, error), defaultUrl string, autostart bool) *BroacastManagerCtx {
|
||||
logger := log.With().
|
||||
Str("module", "capture").
|
||||
Str("submodule", "broadcast").
|
||||
|
@ -31,8 +37,34 @@ func broadcastNew(pipelineFn func(url string) (string, error), url string, start
|
|||
return &BroacastManagerCtx{
|
||||
logger: logger,
|
||||
pipelineFn: pipelineFn,
|
||||
url: url,
|
||||
started: started && url != "",
|
||||
url: defaultUrl,
|
||||
started: defaultUrl != "" && autostart,
|
||||
|
||||
// metrics
|
||||
pipelinesCounter: promauto.NewCounter(prometheus.CounterOpts{
|
||||
Name: "pipelines_total",
|
||||
Namespace: "neko",
|
||||
Subsystem: "capture",
|
||||
Help: "Total number of created pipelines.",
|
||||
ConstLabels: map[string]string{
|
||||
"submodule": "broadcast",
|
||||
"video_id": "main",
|
||||
"codec_name": "-",
|
||||
"codec_type": "-",
|
||||
},
|
||||
}),
|
||||
pipelinesActive: promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "pipelines_active",
|
||||
Namespace: "neko",
|
||||
Subsystem: "capture",
|
||||
Help: "Total number of active pipelines.",
|
||||
ConstLabels: map[string]string{
|
||||
"submodule": "broadcast",
|
||||
"video_id": "main",
|
||||
"codec_name": "-",
|
||||
"codec_type": "-",
|
||||
},
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -86,7 +118,6 @@ func (manager *BroacastManagerCtx) createPipeline() error {
|
|||
return types.ErrCapturePipelineAlreadyExists
|
||||
}
|
||||
|
||||
var err error
|
||||
pipelineStr, err := manager.pipelineFn(manager.url)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -103,6 +134,8 @@ func (manager *BroacastManagerCtx) createPipeline() error {
|
|||
}
|
||||
|
||||
manager.pipeline.Play()
|
||||
manager.pipelinesCounter.Inc()
|
||||
manager.pipelinesActive.Set(1)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -118,4 +151,6 @@ func (manager *BroacastManagerCtx) destroyPipeline() {
|
|||
manager.pipeline.Destroy()
|
||||
manager.logger.Info().Msgf("destroying pipeline")
|
||||
manager.pipeline = nil
|
||||
|
||||
manager.pipelinesActive.Set(0)
|
||||
}
|
||||
|
|
|
@ -1,202 +0,0 @@
|
|||
#include "gst.h"
|
||||
|
||||
static void gstreamer_pipeline_log(GstPipelineCtx *ctx, char* level, const char* format, ...) {
|
||||
va_list argptr;
|
||||
va_start(argptr, format);
|
||||
char buffer[4096];
|
||||
vsnprintf(buffer, sizeof(buffer), format, argptr);
|
||||
va_end(argptr);
|
||||
goPipelineLog(level, buffer, ctx->pipelineId);
|
||||
}
|
||||
|
||||
static gboolean gstreamer_bus_call(GstBus *bus, GstMessage *msg, gpointer user_data) {
|
||||
GstPipelineCtx *ctx = (GstPipelineCtx *)user_data;
|
||||
|
||||
switch (GST_MESSAGE_TYPE(msg)) {
|
||||
case GST_MESSAGE_EOS: {
|
||||
gstreamer_pipeline_log(ctx, "fatal", "end of stream");
|
||||
break;
|
||||
}
|
||||
|
||||
case GST_MESSAGE_STATE_CHANGED: {
|
||||
GstState old_state, new_state;
|
||||
gst_message_parse_state_changed(msg, &old_state, &new_state, NULL);
|
||||
|
||||
gstreamer_pipeline_log(ctx, "debug",
|
||||
"element %s changed state from %s to %s",
|
||||
GST_OBJECT_NAME(msg->src),
|
||||
gst_element_state_get_name(old_state),
|
||||
gst_element_state_get_name(new_state));
|
||||
break;
|
||||
}
|
||||
|
||||
case GST_MESSAGE_TAG: {
|
||||
GstTagList *tags = NULL;
|
||||
gst_message_parse_tag(msg, &tags);
|
||||
|
||||
gstreamer_pipeline_log(ctx, "debug",
|
||||
"got tags from element %s",
|
||||
GST_OBJECT_NAME(msg->src));
|
||||
|
||||
gst_tag_list_unref(tags);
|
||||
break;
|
||||
}
|
||||
|
||||
case GST_MESSAGE_ERROR: {
|
||||
GError *err = NULL;
|
||||
gchar *dbg_info = NULL;
|
||||
gst_message_parse_error(msg, &err, &dbg_info);
|
||||
|
||||
gstreamer_pipeline_log(ctx, "error",
|
||||
"error from element %s: %s",
|
||||
GST_OBJECT_NAME(msg->src), err->message);
|
||||
gstreamer_pipeline_log(ctx, "warn",
|
||||
"debugging info: %s",
|
||||
(dbg_info) ? dbg_info : "none");
|
||||
|
||||
g_error_free(err);
|
||||
g_free(dbg_info);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
gstreamer_pipeline_log(ctx, "trace", "unknown message");
|
||||
break;
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
GstPipelineCtx *gstreamer_pipeline_create(char *pipelineStr, int pipelineId, GError **error) {
|
||||
GstElement *pipeline = gst_parse_launch(pipelineStr, error);
|
||||
if (pipeline == NULL) return NULL;
|
||||
|
||||
// create gstreamer pipeline context
|
||||
GstPipelineCtx *ctx = calloc(1, sizeof(GstPipelineCtx));
|
||||
ctx->pipelineId = pipelineId;
|
||||
ctx->pipeline = pipeline;
|
||||
|
||||
GstBus *bus = gst_pipeline_get_bus(GST_PIPELINE(pipeline));
|
||||
gst_bus_add_watch(bus, gstreamer_bus_call, ctx);
|
||||
gst_object_unref(bus);
|
||||
|
||||
return ctx;
|
||||
}
|
||||
|
||||
static GstFlowReturn gstreamer_send_new_sample_handler(GstElement *object, gpointer user_data) {
|
||||
GstPipelineCtx *ctx = (GstPipelineCtx *)user_data;
|
||||
GstSample *sample = NULL;
|
||||
GstBuffer *buffer = NULL;
|
||||
gpointer copy = NULL;
|
||||
gsize copy_size = 0;
|
||||
|
||||
g_signal_emit_by_name(object, "pull-sample", &sample);
|
||||
if (sample) {
|
||||
buffer = gst_sample_get_buffer(sample);
|
||||
if (buffer) {
|
||||
gst_buffer_extract_dup(buffer, 0, gst_buffer_get_size(buffer), ©, ©_size);
|
||||
goHandlePipelineBuffer(copy, copy_size, GST_BUFFER_DURATION(buffer), ctx->pipelineId);
|
||||
}
|
||||
gst_sample_unref(sample);
|
||||
}
|
||||
|
||||
return GST_FLOW_OK;
|
||||
}
|
||||
|
||||
void gstreamer_pipeline_attach_appsink(GstPipelineCtx *ctx, char *sinkName) {
|
||||
ctx->appsink = gst_bin_get_by_name(GST_BIN(ctx->pipeline), sinkName);
|
||||
g_object_set(ctx->appsink, "emit-signals", TRUE, NULL);
|
||||
g_signal_connect(ctx->appsink, "new-sample", G_CALLBACK(gstreamer_send_new_sample_handler), ctx);
|
||||
}
|
||||
|
||||
void gstreamer_pipeline_attach_appsrc(GstPipelineCtx *ctx, char *srcName) {
|
||||
ctx->appsrc = gst_bin_get_by_name(GST_BIN(ctx->pipeline), srcName);
|
||||
}
|
||||
|
||||
void gstreamer_pipeline_play(GstPipelineCtx *ctx) {
|
||||
gst_element_set_state(GST_ELEMENT(ctx->pipeline), GST_STATE_PLAYING);
|
||||
}
|
||||
|
||||
void gstreamer_pipeline_pause(GstPipelineCtx *ctx) {
|
||||
gst_element_set_state(GST_ELEMENT(ctx->pipeline), GST_STATE_PAUSED);
|
||||
}
|
||||
|
||||
void gstreamer_pipeline_destory(GstPipelineCtx *ctx) {
|
||||
// end appsrc, if exists
|
||||
if (ctx->appsrc) {
|
||||
gst_app_src_end_of_stream(GST_APP_SRC(ctx->appsrc));
|
||||
}
|
||||
|
||||
// send pipeline eos
|
||||
gst_element_send_event(GST_ELEMENT(ctx->pipeline), gst_event_new_eos());
|
||||
|
||||
// set null state
|
||||
gst_element_set_state(GST_ELEMENT(ctx->pipeline), GST_STATE_NULL);
|
||||
|
||||
if (ctx->appsink) {
|
||||
gst_object_unref(ctx->appsink);
|
||||
ctx->appsink = NULL;
|
||||
}
|
||||
|
||||
if (ctx->appsrc) {
|
||||
gst_object_unref(ctx->appsrc);
|
||||
ctx->appsrc = NULL;
|
||||
}
|
||||
|
||||
gst_object_unref(ctx->pipeline);
|
||||
}
|
||||
|
||||
void gstreamer_pipeline_push(GstPipelineCtx *ctx, void *buffer, int bufferLen) {
|
||||
if (ctx->appsrc != NULL) {
|
||||
gpointer p = g_memdup(buffer, bufferLen);
|
||||
GstBuffer *buffer = gst_buffer_new_wrapped(p, bufferLen);
|
||||
gst_app_src_push_buffer(GST_APP_SRC(ctx->appsrc), buffer);
|
||||
}
|
||||
}
|
||||
|
||||
gboolean gstreamer_pipeline_set_prop_int(GstPipelineCtx *ctx, char *binName, char *prop, gint value) {
|
||||
GstElement *el = gst_bin_get_by_name(GST_BIN(ctx->pipeline), binName);
|
||||
if (el == NULL) return FALSE;
|
||||
|
||||
g_object_set(G_OBJECT(el),
|
||||
prop, value,
|
||||
NULL);
|
||||
|
||||
gst_object_unref(el);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
gboolean gstreamer_pipeline_set_caps_framerate(GstPipelineCtx *ctx, const gchar* binName, gint numerator, gint denominator) {
|
||||
GstElement *el = gst_bin_get_by_name(GST_BIN(ctx->pipeline), binName);
|
||||
if (el == NULL) return FALSE;
|
||||
|
||||
GstCaps *caps = gst_caps_new_simple("video/x-raw",
|
||||
"framerate", GST_TYPE_FRACTION, numerator, denominator,
|
||||
NULL);
|
||||
|
||||
g_object_set(G_OBJECT(el),
|
||||
"caps", caps,
|
||||
NULL);
|
||||
|
||||
gst_caps_unref(caps);
|
||||
gst_object_unref(el);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
gboolean gstreamer_pipeline_set_caps_resolution(GstPipelineCtx *ctx, const gchar* binName, gint width, gint height) {
|
||||
GstElement *el = gst_bin_get_by_name(GST_BIN(ctx->pipeline), binName);
|
||||
if (el == NULL) return FALSE;
|
||||
|
||||
GstCaps *caps = gst_caps_new_simple("video/x-raw",
|
||||
"width", G_TYPE_INT, width,
|
||||
"height", G_TYPE_INT, height,
|
||||
NULL);
|
||||
|
||||
g_object_set(G_OBJECT(el),
|
||||
"caps", caps,
|
||||
NULL);
|
||||
|
||||
gst_caps_unref(caps);
|
||||
gst_object_unref(el);
|
||||
return TRUE;
|
||||
}
|
|
@ -1,221 +0,0 @@
|
|||
package gst
|
||||
|
||||
/*
|
||||
#cgo pkg-config: gstreamer-1.0 gstreamer-app-1.0
|
||||
|
||||
#include "gst.h"
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"m1k1o/neko/internal/types"
|
||||
)
|
||||
|
||||
type Pipeline struct {
|
||||
id int
|
||||
logger zerolog.Logger
|
||||
Src string
|
||||
Ctx *C.GstPipelineCtx
|
||||
Sample chan types.Sample
|
||||
}
|
||||
|
||||
var pSerial int32
|
||||
var pipelines = make(map[int]*Pipeline)
|
||||
var pipelinesLock sync.Mutex
|
||||
var registry *C.GstRegistry
|
||||
var gMainLoop *C.GMainLoop
|
||||
|
||||
func init() {
|
||||
C.gst_init(nil, nil)
|
||||
registry = C.gst_registry_get()
|
||||
}
|
||||
|
||||
func RunMainLoop() {
|
||||
if gMainLoop != nil {
|
||||
return
|
||||
}
|
||||
gMainLoop = C.g_main_loop_new(nil, C.int(0))
|
||||
C.g_main_loop_run(gMainLoop)
|
||||
}
|
||||
|
||||
func QuitMainLoop() {
|
||||
if gMainLoop == nil {
|
||||
return
|
||||
}
|
||||
C.g_main_loop_quit(gMainLoop)
|
||||
gMainLoop = nil
|
||||
}
|
||||
|
||||
func CreatePipeline(pipelineStr string) (*Pipeline, error) {
|
||||
id := atomic.AddInt32(&pSerial, 1)
|
||||
|
||||
pipelineStrUnsafe := C.CString(pipelineStr)
|
||||
defer C.free(unsafe.Pointer(pipelineStrUnsafe))
|
||||
|
||||
pipelinesLock.Lock()
|
||||
defer pipelinesLock.Unlock()
|
||||
|
||||
var gstError *C.GError
|
||||
ctx := C.gstreamer_pipeline_create(pipelineStrUnsafe, C.int(id), &gstError)
|
||||
|
||||
if gstError != nil {
|
||||
defer C.g_error_free(gstError)
|
||||
fmt.Printf("(pipeline error) %s", C.GoString(gstError.message))
|
||||
return nil, fmt.Errorf("(pipeline error) %s", C.GoString(gstError.message))
|
||||
}
|
||||
|
||||
p := &Pipeline{
|
||||
id: int(id),
|
||||
logger: log.With().
|
||||
Str("module", "capture").
|
||||
Str("submodule", "gstreamer").
|
||||
Int("pipeline_id", int(id)).Logger(),
|
||||
Src: pipelineStr,
|
||||
Ctx: ctx,
|
||||
}
|
||||
|
||||
pipelines[p.id] = p
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (p *Pipeline) AttachAppsink(sinkName string, sampleChannel chan types.Sample) {
|
||||
sinkNameUnsafe := C.CString(sinkName)
|
||||
defer C.free(unsafe.Pointer(sinkNameUnsafe))
|
||||
|
||||
p.Sample = sampleChannel
|
||||
|
||||
C.gstreamer_pipeline_attach_appsink(p.Ctx, sinkNameUnsafe)
|
||||
}
|
||||
|
||||
func (p *Pipeline) AttachAppsrc(srcName string) {
|
||||
srcNameUnsafe := C.CString(srcName)
|
||||
defer C.free(unsafe.Pointer(srcNameUnsafe))
|
||||
|
||||
C.gstreamer_pipeline_attach_appsrc(p.Ctx, srcNameUnsafe)
|
||||
}
|
||||
|
||||
func (p *Pipeline) Play() {
|
||||
C.gstreamer_pipeline_play(p.Ctx)
|
||||
}
|
||||
|
||||
func (p *Pipeline) Pause() {
|
||||
C.gstreamer_pipeline_pause(p.Ctx)
|
||||
}
|
||||
|
||||
func (p *Pipeline) Destroy() {
|
||||
C.gstreamer_pipeline_destory(p.Ctx)
|
||||
|
||||
pipelinesLock.Lock()
|
||||
delete(pipelines, p.id)
|
||||
pipelinesLock.Unlock()
|
||||
|
||||
C.free(unsafe.Pointer(p.Ctx))
|
||||
p = nil
|
||||
}
|
||||
|
||||
func (p *Pipeline) Push(buffer []byte) {
|
||||
bytes := C.CBytes(buffer)
|
||||
defer C.free(bytes)
|
||||
|
||||
C.gstreamer_pipeline_push(p.Ctx, bytes, C.int(len(buffer)))
|
||||
}
|
||||
|
||||
func (p *Pipeline) SetPropInt(binName string, prop string, value int) bool {
|
||||
cBinName := C.CString(binName)
|
||||
defer C.free(unsafe.Pointer(cBinName))
|
||||
|
||||
cProp := C.CString(prop)
|
||||
defer C.free(unsafe.Pointer(cProp))
|
||||
|
||||
cValue := C.int(value)
|
||||
|
||||
p.logger.Debug().Msgf("setting prop %s of %s to %d", prop, binName, value)
|
||||
|
||||
ok := C.gstreamer_pipeline_set_prop_int(p.Ctx, cBinName, cProp, cValue)
|
||||
return ok == C.TRUE
|
||||
}
|
||||
|
||||
func (p *Pipeline) SetCapsFramerate(binName string, numerator, denominator int) bool {
|
||||
cBinName := C.CString(binName)
|
||||
cNumerator := C.int(numerator)
|
||||
cDenominator := C.int(denominator)
|
||||
|
||||
defer C.free(unsafe.Pointer(cBinName))
|
||||
|
||||
p.logger.Debug().Msgf("setting caps framerate of %s to %d/%d", binName, numerator, denominator)
|
||||
|
||||
ok := C.gstreamer_pipeline_set_caps_framerate(p.Ctx, cBinName, cNumerator, cDenominator)
|
||||
return ok == C.TRUE
|
||||
}
|
||||
|
||||
func (p *Pipeline) SetCapsResolution(binName string, width, height int) bool {
|
||||
cBinName := C.CString(binName)
|
||||
cWidth := C.int(width)
|
||||
cHeight := C.int(height)
|
||||
|
||||
defer C.free(unsafe.Pointer(cBinName))
|
||||
|
||||
p.logger.Debug().Msgf("setting caps resolution of %s to %dx%d", binName, width, height)
|
||||
|
||||
ok := C.gstreamer_pipeline_set_caps_resolution(p.Ctx, cBinName, cWidth, cHeight)
|
||||
return ok == C.TRUE
|
||||
}
|
||||
|
||||
// gst-inspect-1.0
|
||||
func CheckPlugins(plugins []string) error {
|
||||
var plugin *C.GstPlugin
|
||||
for _, pluginstr := range plugins {
|
||||
plugincstr := C.CString(pluginstr)
|
||||
plugin = C.gst_registry_find_plugin(registry, plugincstr)
|
||||
C.free(unsafe.Pointer(plugincstr))
|
||||
if plugin == nil {
|
||||
return fmt.Errorf("required gstreamer plugin %s not found", pluginstr)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
//export goHandlePipelineBuffer
|
||||
func goHandlePipelineBuffer(buffer unsafe.Pointer, bufferLen C.int, duration C.int, pipelineID C.int) {
|
||||
defer C.free(buffer)
|
||||
|
||||
pipelinesLock.Lock()
|
||||
pipeline, ok := pipelines[int(pipelineID)]
|
||||
pipelinesLock.Unlock()
|
||||
|
||||
if ok {
|
||||
pipeline.Sample <- types.Sample{
|
||||
Data: C.GoBytes(buffer, bufferLen),
|
||||
Timestamp: time.Now(),
|
||||
Duration: time.Duration(duration),
|
||||
}
|
||||
} else {
|
||||
log.Warn().
|
||||
Str("module", "capture").
|
||||
Str("submodule", "gstreamer").
|
||||
Int("pipeline_id", int(pipelineID)).
|
||||
Msgf("discarding sample, pipeline not found")
|
||||
}
|
||||
}
|
||||
|
||||
//export goPipelineLog
|
||||
func goPipelineLog(levelUnsafe *C.char, msgUnsafe *C.char, pipelineID C.int) {
|
||||
levelStr := C.GoString(levelUnsafe)
|
||||
msg := C.GoString(msgUnsafe)
|
||||
|
||||
level, _ := zerolog.ParseLevel(levelStr)
|
||||
log.WithLevel(level).
|
||||
Str("module", "capture").
|
||||
Str("submodule", "gstreamer").
|
||||
Int("pipeline_id", int(pipelineID)).
|
||||
Msg(msg)
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <stdio.h>
|
||||
#include <gst/gst.h>
|
||||
#include <gst/app/gstappsrc.h>
|
||||
|
||||
typedef struct GstPipelineCtx {
|
||||
int pipelineId;
|
||||
GstElement *pipeline;
|
||||
GstElement *appsink;
|
||||
GstElement *appsrc;
|
||||
} GstPipelineCtx;
|
||||
|
||||
extern void goHandlePipelineBuffer(void *buffer, int bufferLen, int samples, int pipelineId);
|
||||
extern void goPipelineLog(char *level, char *msg, int pipelineId);
|
||||
|
||||
GstPipelineCtx *gstreamer_pipeline_create(char *pipelineStr, int pipelineId, GError **error);
|
||||
void gstreamer_pipeline_attach_appsink(GstPipelineCtx *ctx, char *sinkName);
|
||||
void gstreamer_pipeline_attach_appsrc(GstPipelineCtx *ctx, char *srcName);
|
||||
void gstreamer_pipeline_play(GstPipelineCtx *ctx);
|
||||
void gstreamer_pipeline_pause(GstPipelineCtx *ctx);
|
||||
void gstreamer_pipeline_destory(GstPipelineCtx *ctx);
|
||||
void gstreamer_pipeline_push(GstPipelineCtx *ctx, void *buffer, int bufferLen);
|
||||
|
||||
gboolean gstreamer_pipeline_set_prop_int(GstPipelineCtx *ctx, char *binName, char *prop, gint value);
|
||||
gboolean gstreamer_pipeline_set_caps_framerate(GstPipelineCtx *ctx, const gchar* binName, gint numerator, gint denominator);
|
||||
gboolean gstreamer_pipeline_set_caps_resolution(GstPipelineCtx *ctx, const gchar* binName, gint width, gint height);
|
|
@ -2,48 +2,189 @@ package capture
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"m1k1o/neko/internal/capture/gst"
|
||||
"m1k1o/neko/internal/config"
|
||||
"m1k1o/neko/internal/types"
|
||||
"github.com/demodesk/neko/internal/config"
|
||||
"github.com/demodesk/neko/pkg/types"
|
||||
"github.com/demodesk/neko/pkg/types/codec"
|
||||
)
|
||||
|
||||
type CaptureManagerCtx struct {
|
||||
logger zerolog.Logger
|
||||
desktop types.DesktopManager
|
||||
config *config.Capture
|
||||
|
||||
// sinks
|
||||
broadcast *BroacastManagerCtx
|
||||
audio *StreamSinkManagerCtx
|
||||
video *StreamSinkManagerCtx
|
||||
broadcast *BroacastManagerCtx
|
||||
screencast *ScreencastManagerCtx
|
||||
audio *StreamSinkManagerCtx
|
||||
video *StreamSelectorManagerCtx
|
||||
|
||||
// sources
|
||||
webcam *StreamSrcManagerCtx
|
||||
microphone *StreamSrcManagerCtx
|
||||
}
|
||||
|
||||
func New(desktop types.DesktopManager, config *config.Capture) *CaptureManagerCtx {
|
||||
logger := log.With().Str("module", "capture").Logger()
|
||||
|
||||
videos := map[string]types.StreamSinkManager{}
|
||||
for video_id, cnf := range config.VideoPipelines {
|
||||
pipelineConf := cnf
|
||||
|
||||
createPipeline := func() (string, error) {
|
||||
if pipelineConf.GstPipeline != "" {
|
||||
// replace {display} with valid display
|
||||
return strings.Replace(pipelineConf.GstPipeline, "{display}", config.Display, 1), nil
|
||||
}
|
||||
|
||||
screen := desktop.GetScreenSize()
|
||||
pipeline, err := pipelineConf.GetPipeline(screen)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return fmt.Sprintf(
|
||||
"ximagesrc display-name=%s show-pointer=false use-damage=false "+
|
||||
"%s ! appsink name=appsink", config.Display, pipeline,
|
||||
), nil
|
||||
}
|
||||
|
||||
// trigger function to catch evaluation errors at startup
|
||||
pipeline, err := createPipeline()
|
||||
if err != nil {
|
||||
logger.Panic().Err(err).
|
||||
Str("video_id", video_id).
|
||||
Msg("failed to create video pipeline")
|
||||
}
|
||||
|
||||
logger.Info().
|
||||
Str("video_id", video_id).
|
||||
Str("pipeline", pipeline).
|
||||
Msg("syntax check for video stream pipeline passed")
|
||||
|
||||
// append to videos
|
||||
videos[video_id] = streamSinkNew(config.VideoCodec, createPipeline, video_id)
|
||||
}
|
||||
|
||||
return &CaptureManagerCtx{
|
||||
logger: logger,
|
||||
desktop: desktop,
|
||||
config: config,
|
||||
|
||||
// sinks
|
||||
broadcast: broadcastNew(func(url string) (string, error) {
|
||||
return NewBroadcastPipeline(config.AudioDevice, config.Display, config.BroadcastPipeline, url)
|
||||
}, config.BroadcastUrl, config.BroadcastAutostart),
|
||||
audio: streamSinkNew(config.AudioCodec, func() (string, error) {
|
||||
return NewAudioPipeline(config.AudioCodec, config.AudioDevice, config.AudioPipeline, config.AudioBitrate)
|
||||
}, "audio"),
|
||||
video: streamSinkNew(config.VideoCodec, func() (string, error) {
|
||||
// use screen fps as default
|
||||
fps := desktop.GetScreenSize().Rate
|
||||
// if max fps is set, cap it to that value
|
||||
if config.VideoMaxFPS > 0 && config.VideoMaxFPS < fps {
|
||||
fps = config.VideoMaxFPS
|
||||
if config.BroadcastPipeline != "" {
|
||||
var pipeline = config.BroadcastPipeline
|
||||
// replace {display} with valid display
|
||||
pipeline = strings.Replace(pipeline, "{display}", config.Display, 1)
|
||||
// replace {device} with valid device
|
||||
pipeline = strings.Replace(pipeline, "{device}", config.AudioDevice, 1)
|
||||
// replace {url} with valid URL
|
||||
return strings.Replace(pipeline, "{url}", url, 1), nil
|
||||
}
|
||||
return NewVideoPipeline(config.VideoCodec, config.Display, config.VideoPipeline, fps, config.VideoBitrate, config.VideoHWEnc)
|
||||
}, "video"),
|
||||
|
||||
return fmt.Sprintf(
|
||||
"flvmux name=mux ! rtmpsink location='%s live=1' "+
|
||||
"pulsesrc device=%s "+
|
||||
"! audio/x-raw,channels=2 "+
|
||||
"! audioconvert "+
|
||||
"! queue "+
|
||||
"! voaacenc bitrate=%d "+
|
||||
"! mux. "+
|
||||
"ximagesrc display-name=%s show-pointer=true use-damage=false "+
|
||||
"! video/x-raw "+
|
||||
"! videoconvert "+
|
||||
"! queue "+
|
||||
"! x264enc threads=4 bitrate=%d key-int-max=15 byte-stream=true tune=zerolatency speed-preset=%s "+
|
||||
"! mux.", url, config.AudioDevice, config.BroadcastAudioBitrate*1000, config.Display, config.BroadcastVideoBitrate, config.BroadcastPreset,
|
||||
), nil
|
||||
}, config.BroadcastUrl, config.BroadcastAutostart),
|
||||
screencast: screencastNew(config.ScreencastEnabled, func() string {
|
||||
if config.ScreencastPipeline != "" {
|
||||
// replace {display} with valid display
|
||||
return strings.Replace(config.ScreencastPipeline, "{display}", config.Display, 1)
|
||||
}
|
||||
|
||||
return fmt.Sprintf(
|
||||
"ximagesrc display-name=%s show-pointer=true use-damage=false "+
|
||||
"! video/x-raw,framerate=%s "+
|
||||
"! videoconvert "+
|
||||
"! queue "+
|
||||
"! jpegenc quality=%s "+
|
||||
"! appsink name=appsink", config.Display, config.ScreencastRate, config.ScreencastQuality,
|
||||
)
|
||||
}()),
|
||||
|
||||
audio: streamSinkNew(config.AudioCodec, func() (string, error) {
|
||||
if config.AudioPipeline != "" {
|
||||
// replace {device} with valid device
|
||||
return strings.Replace(config.AudioPipeline, "{device}", config.AudioDevice, 1), nil
|
||||
}
|
||||
|
||||
return fmt.Sprintf(
|
||||
"pulsesrc device=%s "+
|
||||
"! audio/x-raw,channels=2 "+
|
||||
"! audioconvert "+
|
||||
"! queue "+
|
||||
"! %s "+
|
||||
"! appsink name=appsink", config.AudioDevice, config.AudioCodec.Pipeline,
|
||||
), nil
|
||||
}, "audio"),
|
||||
video: streamSelectorNew(config.VideoCodec, videos, config.VideoIDs),
|
||||
|
||||
// sources
|
||||
webcam: streamSrcNew(config.WebcamEnabled, map[string]string{
|
||||
codec.VP8().Name: "appsrc format=time is-live=true do-timestamp=true name=appsrc " +
|
||||
fmt.Sprintf("! application/x-rtp, payload=%d, encoding-name=VP8-DRAFT-IETF-01 ", codec.VP8().PayloadType) +
|
||||
"! rtpvp8depay " +
|
||||
"! decodebin " +
|
||||
"! videoconvert " +
|
||||
"! videorate " +
|
||||
"! videoscale " +
|
||||
fmt.Sprintf("! video/x-raw,width=%d,height=%d ", config.WebcamWidth, config.WebcamHeight) +
|
||||
"! identity drop-allocation=true " +
|
||||
fmt.Sprintf("! v4l2sink sync=false device=%s", config.WebcamDevice),
|
||||
// TODO: Test this pipeline.
|
||||
codec.VP9().Name: "appsrc format=time is-live=true do-timestamp=true name=appsrc " +
|
||||
"! application/x-rtp " +
|
||||
"! rtpvp9depay " +
|
||||
"! decodebin " +
|
||||
"! videoconvert " +
|
||||
"! videorate " +
|
||||
"! videoscale " +
|
||||
fmt.Sprintf("! video/x-raw,width=%d,height=%d ", config.WebcamWidth, config.WebcamHeight) +
|
||||
"! identity drop-allocation=true " +
|
||||
fmt.Sprintf("! v4l2sink sync=false device=%s", config.WebcamDevice),
|
||||
// TODO: Test this pipeline.
|
||||
codec.H264().Name: "appsrc format=time is-live=true do-timestamp=true name=appsrc " +
|
||||
"! application/x-rtp " +
|
||||
"! rtph264depay " +
|
||||
"! decodebin " +
|
||||
"! videoconvert " +
|
||||
"! videorate " +
|
||||
"! videoscale " +
|
||||
fmt.Sprintf("! video/x-raw,width=%d,height=%d ", config.WebcamWidth, config.WebcamHeight) +
|
||||
"! identity drop-allocation=true " +
|
||||
fmt.Sprintf("! v4l2sink sync=false device=%s", config.WebcamDevice),
|
||||
}, "webcam"),
|
||||
microphone: streamSrcNew(config.MicrophoneEnabled, map[string]string{
|
||||
codec.Opus().Name: "appsrc format=time is-live=true do-timestamp=true name=appsrc " +
|
||||
fmt.Sprintf("! application/x-rtp, payload=%d, encoding-name=OPUS ", codec.Opus().PayloadType) +
|
||||
"! rtpopusdepay " +
|
||||
"! decodebin " +
|
||||
fmt.Sprintf("! pulsesink device=%s", config.MicrophoneDevice),
|
||||
// TODO: Test this pipeline.
|
||||
codec.G722().Name: "appsrc format=time is-live=true do-timestamp=true name=appsrc " +
|
||||
"! application/x-rtp clock-rate=8000 " +
|
||||
"! rtpg722depay " +
|
||||
"! decodebin " +
|
||||
fmt.Sprintf("! pulsesink device=%s", config.MicrophoneDevice),
|
||||
}, "microphone"),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -54,55 +195,51 @@ func (manager *CaptureManagerCtx) Start() {
|
|||
}
|
||||
}
|
||||
|
||||
go gst.RunMainLoop()
|
||||
go func() {
|
||||
for {
|
||||
before, ok := <-manager.desktop.GetScreenSizeChangeChannel()
|
||||
if !ok {
|
||||
manager.logger.Info().Msg("screen size change channel was closed")
|
||||
return
|
||||
}
|
||||
manager.desktop.OnBeforeScreenSizeChange(func() {
|
||||
manager.video.destroyPipelines()
|
||||
|
||||
if before {
|
||||
// before screen size change, we need to destroy all pipelines
|
||||
if manager.broadcast.Started() {
|
||||
manager.broadcast.destroyPipeline()
|
||||
}
|
||||
|
||||
if manager.video.Started() {
|
||||
manager.video.destroyPipeline()
|
||||
}
|
||||
if manager.screencast.Started() {
|
||||
manager.screencast.destroyPipeline()
|
||||
}
|
||||
})
|
||||
|
||||
if manager.broadcast.Started() {
|
||||
manager.broadcast.destroyPipeline()
|
||||
}
|
||||
} else {
|
||||
// after screen size change, we need to recreate all pipelines
|
||||
manager.desktop.OnAfterScreenSizeChange(func() {
|
||||
err := manager.video.recreatePipelines()
|
||||
if err != nil {
|
||||
manager.logger.Panic().Err(err).Msg("unable to recreate video pipelines")
|
||||
}
|
||||
|
||||
if manager.video.Started() {
|
||||
err := manager.video.createPipeline()
|
||||
if err != nil && !errors.Is(err, types.ErrCapturePipelineAlreadyExists) {
|
||||
manager.logger.Panic().Err(err).Msg("unable to recreate video pipeline")
|
||||
}
|
||||
}
|
||||
|
||||
if manager.broadcast.Started() {
|
||||
err := manager.broadcast.createPipeline()
|
||||
if err != nil && !errors.Is(err, types.ErrCapturePipelineAlreadyExists) {
|
||||
manager.logger.Panic().Err(err).Msg("unable to recreate broadcast pipeline")
|
||||
}
|
||||
}
|
||||
if manager.broadcast.Started() {
|
||||
err := manager.broadcast.createPipeline()
|
||||
if err != nil && !errors.Is(err, types.ErrCapturePipelineAlreadyExists) {
|
||||
manager.logger.Panic().Err(err).Msg("unable to recreate broadcast pipeline")
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
if manager.screencast.Started() {
|
||||
err := manager.screencast.createPipeline()
|
||||
if err != nil && !errors.Is(err, types.ErrCapturePipelineAlreadyExists) {
|
||||
manager.logger.Panic().Err(err).Msg("unable to recreate screencast pipeline")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (manager *CaptureManagerCtx) Shutdown() error {
|
||||
manager.logger.Info().Msgf("shutdown")
|
||||
|
||||
manager.broadcast.shutdown()
|
||||
manager.screencast.shutdown()
|
||||
|
||||
manager.audio.shutdown()
|
||||
manager.video.shutdown()
|
||||
|
||||
gst.QuitMainLoop()
|
||||
manager.webcam.shutdown()
|
||||
manager.microphone.shutdown()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -111,10 +248,22 @@ func (manager *CaptureManagerCtx) Broadcast() types.BroadcastManager {
|
|||
return manager.broadcast
|
||||
}
|
||||
|
||||
func (manager *CaptureManagerCtx) Screencast() types.ScreencastManager {
|
||||
return manager.screencast
|
||||
}
|
||||
|
||||
func (manager *CaptureManagerCtx) Audio() types.StreamSinkManager {
|
||||
return manager.audio
|
||||
}
|
||||
|
||||
func (manager *CaptureManagerCtx) Video() types.StreamSinkManager {
|
||||
func (manager *CaptureManagerCtx) Video() types.StreamSelectorManager {
|
||||
return manager.video
|
||||
}
|
||||
|
||||
func (manager *CaptureManagerCtx) Webcam() types.StreamSrcManager {
|
||||
return manager.webcam
|
||||
}
|
||||
|
||||
func (manager *CaptureManagerCtx) Microphone() types.StreamSrcManager {
|
||||
return manager.microphone
|
||||
}
|
||||
|
|
|
@ -2,41 +2,122 @@ package capture
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"m1k1o/neko/internal/capture/gst"
|
||||
"m1k1o/neko/internal/types"
|
||||
"m1k1o/neko/internal/types/codec"
|
||||
"github.com/demodesk/neko/pkg/gst"
|
||||
"github.com/demodesk/neko/pkg/types"
|
||||
"github.com/demodesk/neko/pkg/types/codec"
|
||||
)
|
||||
|
||||
var moveSinkListenerMu = sync.Mutex{}
|
||||
|
||||
type StreamSinkManagerCtx struct {
|
||||
logger zerolog.Logger
|
||||
mu sync.Mutex
|
||||
sampleChannel chan types.Sample
|
||||
id string
|
||||
|
||||
// wait for a keyframe before sending samples
|
||||
waitForKf bool
|
||||
|
||||
bitrate uint64 // atomic
|
||||
brBuckets map[int]float64
|
||||
|
||||
logger zerolog.Logger
|
||||
mu sync.Mutex
|
||||
wg sync.WaitGroup
|
||||
|
||||
codec codec.RTPCodec
|
||||
pipeline *gst.Pipeline
|
||||
pipeline gst.Pipeline
|
||||
pipelineMu sync.Mutex
|
||||
pipelineFn func() (string, error)
|
||||
|
||||
listeners int
|
||||
listeners map[uintptr]types.SampleListener
|
||||
listenersKf map[uintptr]types.SampleListener // keyframe lobby
|
||||
listenersMu sync.Mutex
|
||||
|
||||
// metrics
|
||||
currentListeners prometheus.Gauge
|
||||
totalBytes prometheus.Counter
|
||||
pipelinesCounter prometheus.Counter
|
||||
pipelinesActive prometheus.Gauge
|
||||
}
|
||||
|
||||
func streamSinkNew(codec codec.RTPCodec, pipelineFn func() (string, error), video_id string) *StreamSinkManagerCtx {
|
||||
func streamSinkNew(codec codec.RTPCodec, pipelineFn func() (string, error), id string) *StreamSinkManagerCtx {
|
||||
logger := log.With().
|
||||
Str("module", "capture").
|
||||
Str("submodule", "stream-sink").
|
||||
Str("video_id", video_id).Logger()
|
||||
Str("id", id).Logger()
|
||||
|
||||
manager := &StreamSinkManagerCtx{
|
||||
logger: logger,
|
||||
codec: codec,
|
||||
pipelineFn: pipelineFn,
|
||||
sampleChannel: make(chan types.Sample),
|
||||
id: id,
|
||||
|
||||
// only wait for keyframes if the codec is video
|
||||
waitForKf: codec.IsVideo(),
|
||||
|
||||
bitrate: 0,
|
||||
brBuckets: map[int]float64{},
|
||||
|
||||
logger: logger,
|
||||
codec: codec,
|
||||
pipelineFn: pipelineFn,
|
||||
|
||||
listeners: map[uintptr]types.SampleListener{},
|
||||
listenersKf: map[uintptr]types.SampleListener{},
|
||||
|
||||
// metrics
|
||||
currentListeners: promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "streamsink_listeners",
|
||||
Namespace: "neko",
|
||||
Subsystem: "capture",
|
||||
Help: "Current number of listeners for a pipeline.",
|
||||
ConstLabels: map[string]string{
|
||||
"video_id": id,
|
||||
"codec_name": codec.Name,
|
||||
"codec_type": codec.Type.String(),
|
||||
},
|
||||
}),
|
||||
totalBytes: promauto.NewCounter(prometheus.CounterOpts{
|
||||
Name: "streamsink_bytes",
|
||||
Namespace: "neko",
|
||||
Subsystem: "capture",
|
||||
Help: "Total number of bytes created by the pipeline.",
|
||||
ConstLabels: map[string]string{
|
||||
"video_id": id,
|
||||
"codec_name": codec.Name,
|
||||
"codec_type": codec.Type.String(),
|
||||
},
|
||||
}),
|
||||
pipelinesCounter: promauto.NewCounter(prometheus.CounterOpts{
|
||||
Name: "pipelines_total",
|
||||
Namespace: "neko",
|
||||
Subsystem: "capture",
|
||||
Help: "Total number of created pipelines.",
|
||||
ConstLabels: map[string]string{
|
||||
"submodule": "streamsink",
|
||||
"video_id": id,
|
||||
"codec_name": codec.Name,
|
||||
"codec_type": codec.Type.String(),
|
||||
},
|
||||
}),
|
||||
pipelinesActive: promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "pipelines_active",
|
||||
Namespace: "neko",
|
||||
Subsystem: "capture",
|
||||
Help: "Total number of active pipelines.",
|
||||
ConstLabels: map[string]string{
|
||||
"submodule": "streamsink",
|
||||
"video_id": id,
|
||||
"codec_name": codec.Name,
|
||||
"codec_type": codec.Type.String(),
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
return manager
|
||||
|
@ -45,7 +126,25 @@ func streamSinkNew(codec codec.RTPCodec, pipelineFn func() (string, error), vide
|
|||
func (manager *StreamSinkManagerCtx) shutdown() {
|
||||
manager.logger.Info().Msgf("shutdown")
|
||||
|
||||
manager.destroyPipeline()
|
||||
manager.listenersMu.Lock()
|
||||
for key := range manager.listeners {
|
||||
delete(manager.listeners, key)
|
||||
}
|
||||
for key := range manager.listenersKf {
|
||||
delete(manager.listenersKf, key)
|
||||
}
|
||||
manager.listenersMu.Unlock()
|
||||
|
||||
manager.DestroyPipeline()
|
||||
manager.wg.Wait()
|
||||
}
|
||||
|
||||
func (manager *StreamSinkManagerCtx) ID() string {
|
||||
return manager.id
|
||||
}
|
||||
|
||||
func (manager *StreamSinkManagerCtx) Bitrate() uint64 {
|
||||
return atomic.LoadUint64(&manager.bitrate)
|
||||
}
|
||||
|
||||
func (manager *StreamSinkManagerCtx) Codec() codec.RTPCodec {
|
||||
|
@ -53,8 +152,8 @@ func (manager *StreamSinkManagerCtx) Codec() codec.RTPCodec {
|
|||
}
|
||||
|
||||
func (manager *StreamSinkManagerCtx) start() error {
|
||||
if manager.listeners == 0 {
|
||||
err := manager.createPipeline()
|
||||
if len(manager.listeners)+len(manager.listenersKf) == 0 {
|
||||
err := manager.CreatePipeline()
|
||||
if err != nil && !errors.Is(err, types.ErrCapturePipelineAlreadyExists) {
|
||||
return err
|
||||
}
|
||||
|
@ -66,45 +165,123 @@ func (manager *StreamSinkManagerCtx) start() error {
|
|||
}
|
||||
|
||||
func (manager *StreamSinkManagerCtx) stop() {
|
||||
if manager.listeners == 0 {
|
||||
manager.destroyPipeline()
|
||||
if len(manager.listeners)+len(manager.listenersKf) == 0 {
|
||||
manager.DestroyPipeline()
|
||||
manager.logger.Info().Msgf("last listener, stopping")
|
||||
}
|
||||
}
|
||||
|
||||
func (manager *StreamSinkManagerCtx) addListener() {
|
||||
func (manager *StreamSinkManagerCtx) addListener(listener types.SampleListener) {
|
||||
ptr := reflect.ValueOf(listener).Pointer()
|
||||
emitKeyframe := false
|
||||
|
||||
manager.listenersMu.Lock()
|
||||
manager.listeners++
|
||||
if manager.waitForKf {
|
||||
// if this is the first listener, we need to emit a keyframe
|
||||
emitKeyframe = len(manager.listenersKf) == 0
|
||||
// if we're waiting for a keyframe, add it to the keyframe lobby
|
||||
manager.listenersKf[ptr] = listener
|
||||
} else {
|
||||
// otherwise, add it as a regular listener
|
||||
manager.listeners[ptr] = listener
|
||||
}
|
||||
manager.listenersMu.Unlock()
|
||||
|
||||
manager.logger.Debug().Interface("ptr", ptr).Msgf("adding listener")
|
||||
manager.currentListeners.Set(float64(manager.ListenersCount()))
|
||||
|
||||
// if we will be waiting for a keyframe, emit one now
|
||||
if manager.pipeline != nil && emitKeyframe {
|
||||
manager.pipeline.EmitVideoKeyframe()
|
||||
}
|
||||
}
|
||||
|
||||
func (manager *StreamSinkManagerCtx) removeListener() {
|
||||
func (manager *StreamSinkManagerCtx) removeListener(listener types.SampleListener) {
|
||||
ptr := reflect.ValueOf(listener).Pointer()
|
||||
|
||||
manager.listenersMu.Lock()
|
||||
manager.listeners--
|
||||
delete(manager.listeners, ptr)
|
||||
delete(manager.listenersKf, ptr) // if it's a keyframe listener, remove it too
|
||||
manager.listenersMu.Unlock()
|
||||
|
||||
manager.logger.Debug().Interface("ptr", ptr).Msgf("removing listener")
|
||||
manager.currentListeners.Set(float64(manager.ListenersCount()))
|
||||
}
|
||||
|
||||
func (manager *StreamSinkManagerCtx) AddListener() error {
|
||||
func (manager *StreamSinkManagerCtx) AddListener(listener types.SampleListener) error {
|
||||
manager.mu.Lock()
|
||||
defer manager.mu.Unlock()
|
||||
|
||||
if listener == nil {
|
||||
return errors.New("listener cannot be nil")
|
||||
}
|
||||
|
||||
// start if stopped
|
||||
if err := manager.start(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// add listener
|
||||
manager.addListener()
|
||||
manager.addListener(listener)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (manager *StreamSinkManagerCtx) RemoveListener() error {
|
||||
func (manager *StreamSinkManagerCtx) RemoveListener(listener types.SampleListener) error {
|
||||
manager.mu.Lock()
|
||||
defer manager.mu.Unlock()
|
||||
|
||||
if listener == nil {
|
||||
return errors.New("listener cannot be nil")
|
||||
}
|
||||
|
||||
// remove listener
|
||||
manager.removeListener()
|
||||
manager.removeListener(listener)
|
||||
|
||||
// stop if started
|
||||
manager.stop()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// moving listeners between streams ensures, that target pipeline is running
|
||||
// before listener is added, and stops source pipeline if there are 0 listeners
|
||||
func (manager *StreamSinkManagerCtx) MoveListenerTo(listener types.SampleListener, stream types.StreamSinkManager) error {
|
||||
if listener == nil {
|
||||
return errors.New("listener cannot be nil")
|
||||
}
|
||||
|
||||
targetStream, ok := stream.(*StreamSinkManagerCtx)
|
||||
if !ok {
|
||||
return errors.New("target stream manager does not support moving listeners")
|
||||
}
|
||||
|
||||
// we need to acquire both mutextes, from source stream and from target stream
|
||||
// in order to do that safely (without possibility of deadlock) we need third
|
||||
// global mutex, that ensures atomic locking
|
||||
|
||||
// lock global mutex
|
||||
moveSinkListenerMu.Lock()
|
||||
|
||||
// lock source stream
|
||||
manager.mu.Lock()
|
||||
defer manager.mu.Unlock()
|
||||
|
||||
// lock target stream
|
||||
targetStream.mu.Lock()
|
||||
defer targetStream.mu.Unlock()
|
||||
|
||||
// unlock global mutex
|
||||
moveSinkListenerMu.Unlock()
|
||||
|
||||
// start if stopped
|
||||
if err := targetStream.start(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// swap listeners
|
||||
manager.removeListener(listener)
|
||||
targetStream.addListener(listener)
|
||||
|
||||
// stop if started
|
||||
manager.stop()
|
||||
|
@ -116,14 +293,14 @@ func (manager *StreamSinkManagerCtx) ListenersCount() int {
|
|||
manager.listenersMu.Lock()
|
||||
defer manager.listenersMu.Unlock()
|
||||
|
||||
return manager.listeners
|
||||
return len(manager.listeners) + len(manager.listenersKf)
|
||||
}
|
||||
|
||||
func (manager *StreamSinkManagerCtx) Started() bool {
|
||||
return manager.ListenersCount() > 0
|
||||
}
|
||||
|
||||
func (manager *StreamSinkManagerCtx) createPipeline() error {
|
||||
func (manager *StreamSinkManagerCtx) CreatePipeline() error {
|
||||
manager.pipelineMu.Lock()
|
||||
defer manager.pipelineMu.Unlock()
|
||||
|
||||
|
@ -146,18 +323,79 @@ func (manager *StreamSinkManagerCtx) createPipeline() error {
|
|||
return err
|
||||
}
|
||||
|
||||
appsinkSubfix := "audio"
|
||||
if manager.codec.IsVideo() {
|
||||
appsinkSubfix = "video"
|
||||
}
|
||||
|
||||
manager.pipeline.AttachAppsink("appsink"+appsinkSubfix, manager.sampleChannel)
|
||||
manager.pipeline.AttachAppsink("appsink")
|
||||
manager.pipeline.Play()
|
||||
|
||||
manager.wg.Add(1)
|
||||
pipeline := manager.pipeline
|
||||
|
||||
go func() {
|
||||
manager.logger.Debug().Msg("started emitting samples")
|
||||
defer manager.wg.Done()
|
||||
|
||||
for {
|
||||
sample, ok := <-pipeline.Sample()
|
||||
if !ok {
|
||||
manager.logger.Debug().Msg("stopped emitting samples")
|
||||
return
|
||||
}
|
||||
|
||||
manager.onSample(sample)
|
||||
}
|
||||
}()
|
||||
|
||||
manager.pipelinesCounter.Inc()
|
||||
manager.pipelinesActive.Set(1)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (manager *StreamSinkManagerCtx) destroyPipeline() {
|
||||
func (manager *StreamSinkManagerCtx) saveSampleBitrate(timestamp time.Time, delta float64) {
|
||||
// get unix timestamp in seconds
|
||||
sec := timestamp.Unix()
|
||||
// last bucket is timestamp rounded to 3 seconds - 1 second
|
||||
last := int((sec - 1) % 3)
|
||||
// current bucket is timestamp rounded to 3 seconds
|
||||
curr := int(sec % 3)
|
||||
// next bucket is timestamp rounded to 3 seconds + 1 second
|
||||
next := int((sec + 1) % 3)
|
||||
|
||||
if manager.brBuckets[next] != 0 {
|
||||
// atomic update bitrate
|
||||
atomic.StoreUint64(&manager.bitrate, uint64(manager.brBuckets[last]))
|
||||
// empty next bucket
|
||||
manager.brBuckets[next] = 0
|
||||
}
|
||||
|
||||
// add rate to current bucket
|
||||
manager.brBuckets[curr] += delta
|
||||
}
|
||||
|
||||
func (manager *StreamSinkManagerCtx) onSample(sample types.Sample) {
|
||||
manager.listenersMu.Lock()
|
||||
defer manager.listenersMu.Unlock()
|
||||
|
||||
// save to metrics
|
||||
length := float64(sample.Length)
|
||||
manager.totalBytes.Add(length)
|
||||
manager.saveSampleBitrate(sample.Timestamp, length)
|
||||
|
||||
// if is not delta unit -> it can be decoded independently -> it is a keyframe
|
||||
if manager.waitForKf && !sample.DeltaUnit && len(manager.listenersKf) > 0 {
|
||||
// if current sample is a keyframe, move listeners from
|
||||
// keyframe lobby to actual listeners map and clear lobby
|
||||
for k, v := range manager.listenersKf {
|
||||
manager.listeners[k] = v
|
||||
}
|
||||
manager.listenersKf = make(map[uintptr]types.SampleListener)
|
||||
}
|
||||
|
||||
for _, l := range manager.listeners {
|
||||
l.WriteSample(sample)
|
||||
}
|
||||
}
|
||||
|
||||
func (manager *StreamSinkManagerCtx) DestroyPipeline() {
|
||||
manager.pipelineMu.Lock()
|
||||
defer manager.pipelineMu.Unlock()
|
||||
|
||||
|
@ -168,8 +406,9 @@ func (manager *StreamSinkManagerCtx) destroyPipeline() {
|
|||
manager.pipeline.Destroy()
|
||||
manager.logger.Info().Msgf("destroying pipeline")
|
||||
manager.pipeline = nil
|
||||
}
|
||||
|
||||
func (manager *StreamSinkManagerCtx) GetSampleChannel() chan types.Sample {
|
||||
return manager.sampleChannel
|
||||
manager.pipelinesActive.Set(0)
|
||||
|
||||
manager.brBuckets = make(map[int]float64)
|
||||
atomic.StoreUint64(&manager.bitrate, 0)
|
||||
}
|
||||
|
|
|
@ -1,55 +1,197 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"m1k1o/neko/internal/types/codec"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/pion/webrtc/v3"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/demodesk/neko/pkg/types"
|
||||
"github.com/demodesk/neko/pkg/types/codec"
|
||||
"github.com/demodesk/neko/pkg/utils"
|
||||
)
|
||||
|
||||
// Legacy capture configuration
|
||||
type HwEnc int
|
||||
|
||||
// Legacy capture configuration
|
||||
const (
|
||||
HwEncNone HwEnc = iota
|
||||
HwEncUnset HwEnc = iota
|
||||
HwEncNone
|
||||
HwEncVAAPI
|
||||
HwEncNVENC
|
||||
)
|
||||
|
||||
type Capture struct {
|
||||
// video
|
||||
Display string
|
||||
VideoCodec codec.RTPCodec
|
||||
VideoHWEnc HwEnc // TODO: Pipeline builder.
|
||||
VideoBitrate uint // TODO: Pipeline builder.
|
||||
VideoMaxFPS int16 // TODO: Pipeline builder.
|
||||
VideoPipeline string
|
||||
Display string
|
||||
|
||||
VideoCodec codec.RTPCodec
|
||||
VideoIDs []string
|
||||
VideoPipelines map[string]types.VideoConfig
|
||||
|
||||
// audio
|
||||
AudioDevice string
|
||||
AudioCodec codec.RTPCodec
|
||||
AudioBitrate uint // TODO: Pipeline builder.
|
||||
AudioPipeline string
|
||||
|
||||
// broadcast
|
||||
BroadcastPipeline string
|
||||
BroadcastUrl string
|
||||
BroadcastAutostart bool
|
||||
BroadcastAudioBitrate int
|
||||
BroadcastVideoBitrate int
|
||||
BroadcastPreset string
|
||||
BroadcastPipeline string
|
||||
BroadcastUrl string
|
||||
BroadcastAutostart bool
|
||||
|
||||
ScreencastEnabled bool
|
||||
ScreencastRate string
|
||||
ScreencastQuality string
|
||||
ScreencastPipeline string
|
||||
|
||||
WebcamEnabled bool
|
||||
WebcamDevice string
|
||||
WebcamWidth int
|
||||
WebcamHeight int
|
||||
|
||||
MicrophoneEnabled bool
|
||||
MicrophoneDevice string
|
||||
}
|
||||
|
||||
func (Capture) Init(cmd *cobra.Command) error {
|
||||
//
|
||||
// video
|
||||
//
|
||||
// audio
|
||||
cmd.PersistentFlags().String("capture.audio.device", "audio_output.monitor", "pulseaudio device to capture")
|
||||
if err := viper.BindPFlag("capture.audio.device", cmd.PersistentFlags().Lookup("capture.audio.device")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("display", ":99.0", "XDisplay to capture")
|
||||
cmd.PersistentFlags().String("capture.audio.codec", "opus", "audio codec to be used")
|
||||
if err := viper.BindPFlag("capture.audio.codec", cmd.PersistentFlags().Lookup("capture.audio.codec")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("capture.audio.pipeline", "", "gstreamer pipeline used for audio streaming")
|
||||
if err := viper.BindPFlag("capture.audio.pipeline", cmd.PersistentFlags().Lookup("capture.audio.pipeline")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// videos
|
||||
cmd.PersistentFlags().String("capture.video.display", "", "X display to capture")
|
||||
if err := viper.BindPFlag("capture.video.display", cmd.PersistentFlags().Lookup("capture.video.display")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("capture.video.codec", "vp8", "video codec to be used")
|
||||
if err := viper.BindPFlag("capture.video.codec", cmd.PersistentFlags().Lookup("capture.video.codec")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().StringSlice("capture.video.ids", []string{}, "ordered list of video ids")
|
||||
if err := viper.BindPFlag("capture.video.ids", cmd.PersistentFlags().Lookup("capture.video.ids")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("capture.video.pipelines", "[]", "pipelines config in JSON used for video streaming")
|
||||
if err := viper.BindPFlag("capture.video.pipelines", cmd.PersistentFlags().Lookup("capture.video.pipelines")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// broadcast
|
||||
cmd.PersistentFlags().Int("capture.broadcast.audio_bitrate", 128, "broadcast audio bitrate in KB/s")
|
||||
if err := viper.BindPFlag("capture.broadcast.audio_bitrate", cmd.PersistentFlags().Lookup("capture.broadcast.audio_bitrate")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Int("capture.broadcast.video_bitrate", 4096, "broadcast video bitrate in KB/s")
|
||||
if err := viper.BindPFlag("capture.broadcast.video_bitrate", cmd.PersistentFlags().Lookup("capture.broadcast.video_bitrate")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("capture.broadcast.preset", "veryfast", "broadcast speed preset for h264 encoding")
|
||||
if err := viper.BindPFlag("capture.broadcast.preset", cmd.PersistentFlags().Lookup("capture.broadcast.preset")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("capture.broadcast.pipeline", "", "gstreamer pipeline used for broadcasting")
|
||||
if err := viper.BindPFlag("capture.broadcast.pipeline", cmd.PersistentFlags().Lookup("capture.broadcast.pipeline")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("capture.broadcast.url", "", "initial URL for broadcasting, setting this value will automatically start broadcasting")
|
||||
if err := viper.BindPFlag("capture.broadcast.url", cmd.PersistentFlags().Lookup("capture.broadcast.url")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Bool("capture.broadcast.autostart", true, "automatically start broadcasting when neko starts and broadcast_url is set")
|
||||
if err := viper.BindPFlag("capture.broadcast.autostart", cmd.PersistentFlags().Lookup("capture.broadcast.autostart")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// screencast
|
||||
cmd.PersistentFlags().Bool("capture.screencast.enabled", false, "enable screencast")
|
||||
if err := viper.BindPFlag("capture.screencast.enabled", cmd.PersistentFlags().Lookup("capture.screencast.enabled")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("capture.screencast.rate", "10/1", "screencast frame rate")
|
||||
if err := viper.BindPFlag("capture.screencast.rate", cmd.PersistentFlags().Lookup("capture.screencast.rate")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("capture.screencast.quality", "60", "screencast JPEG quality")
|
||||
if err := viper.BindPFlag("capture.screencast.quality", cmd.PersistentFlags().Lookup("capture.screencast.quality")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("capture.screencast.pipeline", "", "gstreamer pipeline used for screencasting")
|
||||
if err := viper.BindPFlag("capture.screencast.pipeline", cmd.PersistentFlags().Lookup("capture.screencast.pipeline")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// webcam
|
||||
cmd.PersistentFlags().Bool("capture.webcam.enabled", false, "enable webcam stream")
|
||||
if err := viper.BindPFlag("capture.webcam.enabled", cmd.PersistentFlags().Lookup("capture.webcam.enabled")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// sudo apt install v4l2loopback-dkms v4l2loopback-utils
|
||||
// sudo apt-get install linux-headers-`uname -r` linux-modules-extra-`uname -r`
|
||||
// sudo modprobe v4l2loopback exclusive_caps=1
|
||||
cmd.PersistentFlags().String("capture.webcam.device", "/dev/video0", "v4l2sink device used for webcam")
|
||||
if err := viper.BindPFlag("capture.webcam.device", cmd.PersistentFlags().Lookup("capture.webcam.device")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Int("capture.webcam.width", 1280, "webcam stream width")
|
||||
if err := viper.BindPFlag("capture.webcam.width", cmd.PersistentFlags().Lookup("capture.webcam.width")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Int("capture.webcam.height", 720, "webcam stream height")
|
||||
if err := viper.BindPFlag("capture.webcam.height", cmd.PersistentFlags().Lookup("capture.webcam.height")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// microphone
|
||||
cmd.PersistentFlags().Bool("capture.microphone.enabled", true, "enable microphone stream")
|
||||
if err := viper.BindPFlag("capture.microphone.enabled", cmd.PersistentFlags().Lookup("capture.microphone.enabled")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("capture.microphone.device", "audio_input", "pulseaudio device used for microphone")
|
||||
if err := viper.BindPFlag("capture.microphone.device", cmd.PersistentFlags().Lookup("capture.microphone.device")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (Capture) InitV2(cmd *cobra.Command) error {
|
||||
cmd.PersistentFlags().String("display", "", "V2: XDisplay to capture")
|
||||
if err := viper.BindPFlag("display", cmd.PersistentFlags().Lookup("display")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("video_codec", "vp8", "video codec to be used")
|
||||
cmd.PersistentFlags().String("video_codec", "", "V2: video codec to be used")
|
||||
if err := viper.BindPFlag("video_codec", cmd.PersistentFlags().Lookup("video_codec")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -78,22 +220,22 @@ func (Capture) Init(cmd *cobra.Command) error {
|
|||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("hwenc", "", "use hardware accelerated encoding")
|
||||
cmd.PersistentFlags().String("hwenc", "", "V2: use hardware accelerated encoding")
|
||||
if err := viper.BindPFlag("hwenc", cmd.PersistentFlags().Lookup("hwenc")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Int("video_bitrate", 3072, "video bitrate in kbit/s")
|
||||
cmd.PersistentFlags().Int("video_bitrate", 0, "V2: video bitrate in kbit/s")
|
||||
if err := viper.BindPFlag("video_bitrate", cmd.PersistentFlags().Lookup("video_bitrate")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Int("max_fps", 25, "maximum fps delivered via WebRTC, 0 is for no maximum")
|
||||
cmd.PersistentFlags().Int("max_fps", 0, "V2: maximum fps delivered via WebRTC, 0 is for no maximum")
|
||||
if err := viper.BindPFlag("max_fps", cmd.PersistentFlags().Lookup("max_fps")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("video", "", "video codec parameters to use for streaming")
|
||||
cmd.PersistentFlags().String("video", "", "V2: video codec parameters to use for streaming")
|
||||
if err := viper.BindPFlag("video", cmd.PersistentFlags().Lookup("video")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -102,12 +244,12 @@ func (Capture) Init(cmd *cobra.Command) error {
|
|||
// audio
|
||||
//
|
||||
|
||||
cmd.PersistentFlags().String("device", "audio_output.monitor", "audio device to capture")
|
||||
cmd.PersistentFlags().String("device", "", "V2: audio device to capture")
|
||||
if err := viper.BindPFlag("device", cmd.PersistentFlags().Lookup("device")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("audio_codec", "opus", "audio codec to be used")
|
||||
cmd.PersistentFlags().String("audio_codec", "", "V2: audio codec to be used")
|
||||
if err := viper.BindPFlag("audio_codec", cmd.PersistentFlags().Lookup("audio_codec")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -137,12 +279,12 @@ func (Capture) Init(cmd *cobra.Command) error {
|
|||
}
|
||||
// audio codecs
|
||||
|
||||
cmd.PersistentFlags().Int("audio_bitrate", 128, "audio bitrate in kbit/s")
|
||||
cmd.PersistentFlags().Int("audio_bitrate", 0, "V2: audio bitrate in kbit/s")
|
||||
if err := viper.BindPFlag("audio_bitrate", cmd.PersistentFlags().Lookup("audio_bitrate")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("audio", "", "audio codec parameters to use for streaming")
|
||||
cmd.PersistentFlags().String("audio", "", "V2: audio codec parameters to use for streaming")
|
||||
if err := viper.BindPFlag("audio", cmd.PersistentFlags().Lookup("audio")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -151,17 +293,17 @@ func (Capture) Init(cmd *cobra.Command) error {
|
|||
// broadcast
|
||||
//
|
||||
|
||||
cmd.PersistentFlags().String("broadcast_pipeline", "", "custom gst pipeline used for broadcasting, strings {url} {device} {display} will be replaced")
|
||||
cmd.PersistentFlags().String("broadcast_pipeline", "", "V2: custom gst pipeline used for broadcasting, strings {url} {device} {display} will be replaced")
|
||||
if err := viper.BindPFlag("broadcast_pipeline", cmd.PersistentFlags().Lookup("broadcast_pipeline")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("broadcast_url", "", "a default default URL for broadcast streams, can be disabled/changed later by admins in the GUI")
|
||||
cmd.PersistentFlags().String("broadcast_url", "", "V2: a default default URL for broadcast streams, can be disabled/changed later by admins in the GUI")
|
||||
if err := viper.BindPFlag("broadcast_url", cmd.PersistentFlags().Lookup("broadcast_url")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Bool("broadcast_autostart", true, "automatically start broadcasting when neko starts and broadcast_url is set")
|
||||
cmd.PersistentFlags().Bool("broadcast_autostart", false, "V2: automatically start broadcasting when neko starts and broadcast_url is set")
|
||||
if err := viper.BindPFlag("broadcast_autostart", cmd.PersistentFlags().Lookup("broadcast_autostart")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -172,86 +314,220 @@ func (Capture) Init(cmd *cobra.Command) error {
|
|||
func (s *Capture) Set() {
|
||||
var ok bool
|
||||
|
||||
//
|
||||
s.Display = viper.GetString("capture.video.display")
|
||||
|
||||
// Display is provided by env variable unless explicitly set
|
||||
if s.Display == "" {
|
||||
s.Display = os.Getenv("DISPLAY")
|
||||
}
|
||||
|
||||
// video
|
||||
//
|
||||
|
||||
s.Display = viper.GetString("display")
|
||||
|
||||
videoCodec := viper.GetString("video_codec")
|
||||
videoCodec := viper.GetString("capture.video.codec")
|
||||
s.VideoCodec, ok = codec.ParseStr(videoCodec)
|
||||
if !ok || s.VideoCodec.Type != webrtc.RTPCodecTypeVideo {
|
||||
if !ok || !s.VideoCodec.IsVideo() {
|
||||
log.Warn().Str("codec", videoCodec).Msgf("unknown video codec, using Vp8")
|
||||
s.VideoCodec = codec.VP8()
|
||||
}
|
||||
|
||||
s.VideoIDs = viper.GetStringSlice("capture.video.ids")
|
||||
if err := viper.UnmarshalKey("capture.video.pipelines", &s.VideoPipelines, viper.DecodeHook(
|
||||
utils.JsonStringAutoDecode(s.VideoPipelines),
|
||||
)); err != nil {
|
||||
log.Warn().Err(err).Msgf("unable to parse video pipelines")
|
||||
}
|
||||
|
||||
// default video
|
||||
if len(s.VideoPipelines) == 0 {
|
||||
log.Warn().Msgf("no video pipelines specified, using defaults")
|
||||
|
||||
s.VideoCodec = codec.VP8()
|
||||
s.VideoPipelines = map[string]types.VideoConfig{
|
||||
"main": {
|
||||
Fps: "25",
|
||||
GstEncoder: "vp8enc",
|
||||
GstParams: map[string]string{
|
||||
"target-bitrate": "round(3072 * 650)",
|
||||
"cpu-used": "4",
|
||||
"end-usage": "cbr",
|
||||
"threads": "4",
|
||||
"deadline": "1",
|
||||
"undershoot": "95",
|
||||
"buffer-size": "(3072 * 4)",
|
||||
"buffer-initial-size": "(3072 * 2)",
|
||||
"buffer-optimal-size": "(3072 * 3)",
|
||||
"keyframe-max-dist": "25",
|
||||
"min-quantizer": "4",
|
||||
"max-quantizer": "20",
|
||||
},
|
||||
},
|
||||
}
|
||||
s.VideoIDs = []string{"main"}
|
||||
}
|
||||
|
||||
// audio
|
||||
s.AudioDevice = viper.GetString("capture.audio.device")
|
||||
s.AudioPipeline = viper.GetString("capture.audio.pipeline")
|
||||
|
||||
audioCodec := viper.GetString("capture.audio.codec")
|
||||
s.AudioCodec, ok = codec.ParseStr(audioCodec)
|
||||
if !ok || !s.AudioCodec.IsAudio() {
|
||||
log.Warn().Str("codec", audioCodec).Msgf("unknown audio codec, using Opus")
|
||||
s.AudioCodec = codec.Opus()
|
||||
}
|
||||
|
||||
// broadcast
|
||||
s.BroadcastAudioBitrate = viper.GetInt("capture.broadcast.audio_bitrate")
|
||||
s.BroadcastVideoBitrate = viper.GetInt("capture.broadcast.video_bitrate")
|
||||
s.BroadcastPreset = viper.GetString("capture.broadcast.preset")
|
||||
s.BroadcastPipeline = viper.GetString("capture.broadcast.pipeline")
|
||||
s.BroadcastUrl = viper.GetString("capture.broadcast.url")
|
||||
s.BroadcastAutostart = viper.GetBool("capture.broadcast.autostart")
|
||||
|
||||
// screencast
|
||||
s.ScreencastEnabled = viper.GetBool("capture.screencast.enabled")
|
||||
s.ScreencastRate = viper.GetString("capture.screencast.rate")
|
||||
s.ScreencastQuality = viper.GetString("capture.screencast.quality")
|
||||
s.ScreencastPipeline = viper.GetString("capture.screencast.pipeline")
|
||||
|
||||
// webcam
|
||||
s.WebcamEnabled = viper.GetBool("capture.webcam.enabled")
|
||||
s.WebcamDevice = viper.GetString("capture.webcam.device")
|
||||
s.WebcamWidth = viper.GetInt("capture.webcam.width")
|
||||
s.WebcamHeight = viper.GetInt("capture.webcam.height")
|
||||
|
||||
// microphone
|
||||
s.MicrophoneEnabled = viper.GetBool("capture.microphone.enabled")
|
||||
s.MicrophoneDevice = viper.GetString("capture.microphone.device")
|
||||
}
|
||||
|
||||
func (s *Capture) SetV2() {
|
||||
var ok bool
|
||||
|
||||
//
|
||||
// video
|
||||
//
|
||||
|
||||
if display := viper.GetString("display"); display != "" {
|
||||
s.Display = display
|
||||
log.Warn().Msg("you are using v2 configuration 'NEKO_DISPLAY' which is deprecated, please use 'NEKO_CAPTURE_VIDEO_DISPLAY' and/or 'NEKO_DESKTOP_DISPLAY' instead, also consider using 'DISPLAY' env variable if both should be the same")
|
||||
}
|
||||
|
||||
if videoCodec := viper.GetString("video_codec"); videoCodec != "" {
|
||||
s.VideoCodec, ok = codec.ParseStr(videoCodec)
|
||||
if !ok || s.VideoCodec.Type != webrtc.RTPCodecTypeVideo {
|
||||
log.Warn().Str("codec", videoCodec).Msgf("unknown video codec, using Vp8")
|
||||
s.VideoCodec = codec.VP8()
|
||||
}
|
||||
log.Warn().Msg("you are using v2 configuration 'NEKO_VIDEO_CODEC' which is deprecated, please use 'NEKO_CAPTURE_VIDEO_CODEC' instead")
|
||||
}
|
||||
|
||||
if viper.GetBool("vp8") {
|
||||
s.VideoCodec = codec.VP8()
|
||||
log.Warn().Msg("you are using deprecated config setting 'NEKO_VP8=true', use 'NEKO_VIDEO_CODEC=vp8' instead")
|
||||
log.Warn().Msg("you are using deprecated config setting 'NEKO_VP8=true', use 'NEKO_CAPTURE_VIDEO_CODEC=vp8' instead")
|
||||
} else if viper.GetBool("vp9") {
|
||||
s.VideoCodec = codec.VP9()
|
||||
log.Warn().Msg("you are using deprecated config setting 'NEKO_VP9=true', use 'NEKO_VIDEO_CODEC=vp9' instead")
|
||||
log.Warn().Msg("you are using deprecated config setting 'NEKO_VP9=true', use 'NEKO_CAPTURE_VIDEO_CODEC=vp9' instead")
|
||||
} else if viper.GetBool("h264") {
|
||||
s.VideoCodec = codec.H264()
|
||||
log.Warn().Msg("you are using deprecated config setting 'NEKO_H264=true', use 'NEKO_VIDEO_CODEC=h264' instead")
|
||||
log.Warn().Msg("you are using deprecated config setting 'NEKO_H264=true', use 'NEKO_CAPTURE_VIDEO_CODEC=h264' instead")
|
||||
} else if viper.GetBool("av1") {
|
||||
s.VideoCodec = codec.AV1()
|
||||
log.Warn().Msg("you are using deprecated config setting 'NEKO_AV1=true', use 'NEKO_VIDEO_CODEC=av1' instead")
|
||||
log.Warn().Msg("you are using deprecated config setting 'NEKO_AV1=true', use 'NEKO_CAPTURE_VIDEO_CODEC=av1' instead")
|
||||
}
|
||||
|
||||
videoHWEnc := strings.ToLower(viper.GetString("hwenc"))
|
||||
switch videoHWEnc {
|
||||
case "":
|
||||
fallthrough
|
||||
case "none":
|
||||
s.VideoHWEnc = HwEncNone
|
||||
case "vaapi":
|
||||
s.VideoHWEnc = HwEncVAAPI
|
||||
case "nvenc":
|
||||
s.VideoHWEnc = HwEncNVENC
|
||||
default:
|
||||
log.Warn().Str("hwenc", videoHWEnc).Msgf("unknown video hw encoder, using CPU")
|
||||
videoHWEnc := HwEncUnset
|
||||
if hwenc := strings.ToLower(viper.GetString("hwenc")); hwenc != "" {
|
||||
switch hwenc {
|
||||
case "none":
|
||||
videoHWEnc = HwEncNone
|
||||
case "vaapi":
|
||||
videoHWEnc = HwEncVAAPI
|
||||
case "nvenc":
|
||||
videoHWEnc = HwEncNVENC
|
||||
default:
|
||||
log.Warn().Str("hwenc", hwenc).Msgf("unknown video hw encoder, using CPU")
|
||||
}
|
||||
}
|
||||
|
||||
s.VideoBitrate = viper.GetUint("video_bitrate")
|
||||
s.VideoMaxFPS = int16(viper.GetInt("max_fps"))
|
||||
s.VideoPipeline = viper.GetString("video")
|
||||
videoBitrate := viper.GetUint("video_bitrate")
|
||||
videoMaxFPS := int16(viper.GetInt("max_fps"))
|
||||
videoPipeline := viper.GetString("video")
|
||||
|
||||
// video pipeline
|
||||
if videoHWEnc != HwEncUnset || videoBitrate != 0 || videoMaxFPS != 0 || videoPipeline != "" {
|
||||
pipeline, err := NewVideoPipeline(s.VideoCodec, s.Display, videoPipeline, videoMaxFPS, videoBitrate, videoHWEnc)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("unable to create video pipeline, using default")
|
||||
} else {
|
||||
s.VideoPipelines = map[string]types.VideoConfig{
|
||||
"main": {
|
||||
GstPipeline: pipeline,
|
||||
},
|
||||
}
|
||||
// TODO: add deprecated warning and proper alternative
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// audio
|
||||
//
|
||||
|
||||
s.AudioDevice = viper.GetString("device")
|
||||
if audioDevice := viper.GetString("device"); audioDevice != "" {
|
||||
s.AudioDevice = audioDevice
|
||||
log.Warn().Msg("you are using v2 configuration 'NEKO_DEVICE' which is deprecated, please use 'NEKO_CAPTURE_AUDIO_DEVICE' instead")
|
||||
}
|
||||
|
||||
audioCodec := viper.GetString("audio_codec")
|
||||
s.AudioCodec, ok = codec.ParseStr(audioCodec)
|
||||
if !ok || s.AudioCodec.Type != webrtc.RTPCodecTypeAudio {
|
||||
log.Warn().Str("codec", audioCodec).Msgf("unknown audio codec, using Opus")
|
||||
s.AudioCodec = codec.Opus()
|
||||
if audioCodec := viper.GetString("audio_codec"); audioCodec != "" {
|
||||
s.AudioCodec, ok = codec.ParseStr(audioCodec)
|
||||
if !ok || s.AudioCodec.Type != webrtc.RTPCodecTypeAudio {
|
||||
log.Warn().Str("codec", audioCodec).Msgf("unknown audio codec, using Opus")
|
||||
s.AudioCodec = codec.Opus()
|
||||
}
|
||||
log.Warn().Msg("you are using v2 configuration 'NEKO_AUDIO_CODEC' which is deprecated, please use 'NEKO_CAPTURE_AUDIO_CODEC' instead")
|
||||
}
|
||||
|
||||
if viper.GetBool("opus") {
|
||||
s.AudioCodec = codec.Opus()
|
||||
log.Warn().Msg("you are using deprecated config setting 'NEKO_OPUS=true', use 'NEKO_VIDEO_CODEC=opus' instead")
|
||||
log.Warn().Msg("you are using deprecated config setting 'NEKO_OPUS=true', use 'NEKO_CAPTURE_AUDIO_CODEC=opus' instead")
|
||||
} else if viper.GetBool("g722") {
|
||||
s.AudioCodec = codec.G722()
|
||||
log.Warn().Msg("you are using deprecated config setting 'NEKO_G722=true', use 'NEKO_VIDEO_CODEC=g722' instead")
|
||||
log.Warn().Msg("you are using deprecated config setting 'NEKO_G722=true', use 'NEKO_CAPTURE_AUDIO_CODEC=g722' instead")
|
||||
} else if viper.GetBool("pcmu") {
|
||||
s.AudioCodec = codec.PCMU()
|
||||
log.Warn().Msg("you are using deprecated config setting 'NEKO_PCMU=true', use 'NEKO_VIDEO_CODEC=pcmu' instead")
|
||||
log.Warn().Msg("you are using deprecated config setting 'NEKO_PCMU=true', use 'NEKO_CAPTURE_AUDIO_CODEC=pcmu' instead")
|
||||
} else if viper.GetBool("pcma") {
|
||||
s.AudioCodec = codec.PCMA()
|
||||
log.Warn().Msg("you are using deprecated config setting 'NEKO_PCMA=true', use 'NEKO_VIDEO_CODEC=pcma' instead")
|
||||
log.Warn().Msg("you are using deprecated config setting 'NEKO_PCMA=true', use 'NEKO_CAPTURE_AUDIO_CODEC=pcma' instead")
|
||||
}
|
||||
|
||||
s.AudioBitrate = viper.GetUint("audio_bitrate")
|
||||
s.AudioPipeline = viper.GetString("audio")
|
||||
audioBitrate := viper.GetUint("audio_bitrate")
|
||||
audioPipeline := viper.GetString("audio")
|
||||
|
||||
// audio pipeline
|
||||
if audioBitrate != 0 || audioPipeline != "" {
|
||||
pipeline, err := NewAudioPipeline(s.AudioCodec, s.AudioDevice, audioPipeline, audioBitrate)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("unable to create audio pipeline, using default")
|
||||
} else {
|
||||
s.AudioPipeline = pipeline
|
||||
}
|
||||
// TODO: add deprecated warning and proper alternative
|
||||
}
|
||||
|
||||
//
|
||||
// broadcast
|
||||
//
|
||||
|
||||
s.BroadcastPipeline = viper.GetString("broadcast_pipeline")
|
||||
s.BroadcastUrl = viper.GetString("broadcast_url")
|
||||
s.BroadcastAutostart = viper.GetBool("broadcast_autostart")
|
||||
if viper.IsSet("broadcast_pipeline") {
|
||||
s.BroadcastPipeline = viper.GetString("broadcast_pipeline")
|
||||
log.Warn().Msg("you are using v2 configuration 'NEKO_BROADCAST_PIPELINE' which is deprecated, please use 'NEKO_CAPTURE_BROADCAST_PIPELINE' instead")
|
||||
}
|
||||
if viper.IsSet("broadcast_url") {
|
||||
s.BroadcastUrl = viper.GetString("broadcast_url")
|
||||
log.Warn().Msg("you are using v2 configuration 'NEKO_BROADCAST_URL' which is deprecated, please use 'NEKO_CAPTURE_BROADCAST_URL' instead")
|
||||
}
|
||||
if viper.IsSet("broadcast_autostart") {
|
||||
s.BroadcastAutostart = viper.GetBool("broadcast_autostart")
|
||||
log.Warn().Msg("you are using v2 configuration 'NEKO_BROADCAST_AUTOSTART' which is deprecated, please use 'NEKO_CAPTURE_BROADCAST_AUTOSTART' instead")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
package capture
|
||||
// Legacy pipeline configuration for gstreamer.
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"m1k1o/neko/internal/capture/gst"
|
||||
"m1k1o/neko/internal/config"
|
||||
"m1k1o/neko/internal/types/codec"
|
||||
"github.com/demodesk/neko/pkg/gst"
|
||||
"github.com/demodesk/neko/pkg/types/codec"
|
||||
)
|
||||
|
||||
/*
|
||||
|
@ -34,7 +34,7 @@ const (
|
|||
audioSrc = "pulsesrc device=%s ! audio/x-raw,channels=2 ! audioconvert ! "
|
||||
)
|
||||
|
||||
func NewBroadcastPipeline(device string, display string, pipelineSrc string, url string) (string, error) {
|
||||
func NewBroadcastPipeline(device string, display string, pipelineSrc string, url string) string {
|
||||
video := fmt.Sprintf(videoSrc, display, 25)
|
||||
audio := fmt.Sprintf(audioSrc, device)
|
||||
|
||||
|
@ -50,10 +50,10 @@ func NewBroadcastPipeline(device string, display string, pipelineSrc string, url
|
|||
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.", url, audio, video)
|
||||
}
|
||||
|
||||
return pipelineStr, nil
|
||||
return pipelineStr
|
||||
}
|
||||
|
||||
func NewVideoPipeline(rtpCodec codec.RTPCodec, display string, pipelineSrc string, fps int16, bitrate uint, hwenc config.HwEnc) (string, error) {
|
||||
func NewVideoPipeline(rtpCodec codec.RTPCodec, display string, pipelineSrc string, fps int16, bitrate uint, hwenc HwEnc) (string, error) {
|
||||
pipelineStr := " ! appsink name=appsinkvideo"
|
||||
|
||||
// if using custom pipeline
|
||||
|
@ -69,7 +69,7 @@ func NewVideoPipeline(rtpCodec codec.RTPCodec, display string, pipelineSrc strin
|
|||
|
||||
switch rtpCodec.Name {
|
||||
case codec.VP8().Name:
|
||||
if hwenc == config.HwEncVAAPI {
|
||||
if hwenc == HwEncVAAPI {
|
||||
if err := gst.CheckPlugins([]string{"ximagesrc", "vaapi"}); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
@ -144,13 +144,13 @@ func NewVideoPipeline(rtpCodec codec.RTPCodec, display string, pipelineSrc strin
|
|||
vbvbuf = bitrate
|
||||
}
|
||||
|
||||
if hwenc == config.HwEncVAAPI {
|
||||
if hwenc == HwEncVAAPI {
|
||||
if err := gst.CheckPlugins([]string{"vaapi"}); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
pipelineStr = fmt.Sprintf(videoSrc+"video/x-raw,format=NV12 ! vaapih264enc rate-control=vbr bitrate=%d keyframe-period=180 quality-level=7 ! video/x-h264,stream-format=byte-stream,profile=constrained-baseline"+pipelineStr, display, fps, bitrate)
|
||||
} else if hwenc == config.HwEncNVENC {
|
||||
} else if hwenc == HwEncNVENC {
|
||||
if err := gst.CheckPlugins([]string{"nvcodec"}); err != nil {
|
||||
return "", err
|
||||
}
|
|
@ -5,20 +5,67 @@ import (
|
|||
"regexp"
|
||||
"strconv"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/demodesk/neko/pkg/types"
|
||||
)
|
||||
|
||||
type Desktop struct {
|
||||
Display string
|
||||
|
||||
ScreenWidth int
|
||||
ScreenHeight int
|
||||
ScreenRate int16
|
||||
ScreenSize types.ScreenSize
|
||||
|
||||
UseInputDriver bool
|
||||
InputSocket string
|
||||
|
||||
Unminimize bool
|
||||
UploadDrop bool
|
||||
FileChooserDialog bool
|
||||
}
|
||||
|
||||
func (Desktop) Init(cmd *cobra.Command) error {
|
||||
cmd.PersistentFlags().String("screen", "1280x720@30", "default screen resolution and framerate")
|
||||
cmd.PersistentFlags().String("desktop.display", "", "X display to use for desktop sharing")
|
||||
if err := viper.BindPFlag("desktop.display", cmd.PersistentFlags().Lookup("desktop.display")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("desktop.screen", "1280x720@30", "default screen size and framerate")
|
||||
if err := viper.BindPFlag("desktop.screen", cmd.PersistentFlags().Lookup("desktop.screen")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Bool("desktop.input.enabled", true, "whether custom xf86 input driver should be used to handle touchscreen")
|
||||
if err := viper.BindPFlag("desktop.input.enabled", cmd.PersistentFlags().Lookup("desktop.input.enabled")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("desktop.input.socket", "/tmp/xf86-input-neko.sock", "socket path for custom xf86 input driver connection")
|
||||
if err := viper.BindPFlag("desktop.input.socket", cmd.PersistentFlags().Lookup("desktop.input.socket")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Bool("desktop.unminimize", true, "automatically unminimize window when it is minimized")
|
||||
if err := viper.BindPFlag("desktop.unminimize", cmd.PersistentFlags().Lookup("desktop.unminimize")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Bool("desktop.upload_drop", true, "whether drop upload is enabled")
|
||||
if err := viper.BindPFlag("desktop.upload_drop", cmd.PersistentFlags().Lookup("desktop.upload_drop")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Bool("desktop.file_chooser_dialog", false, "whether to handle file chooser dialog externally")
|
||||
if err := viper.BindPFlag("desktop.file_chooser_dialog", cmd.PersistentFlags().Lookup("desktop.file_chooser_dialog")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (Desktop) InitV2(cmd *cobra.Command) error {
|
||||
cmd.PersistentFlags().String("screen", "", "V2: default screen resolution and framerate")
|
||||
if err := viper.BindPFlag("screen", cmd.PersistentFlags().Lookup("screen")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -27,15 +74,21 @@ func (Desktop) Init(cmd *cobra.Command) error {
|
|||
}
|
||||
|
||||
func (s *Desktop) Set() {
|
||||
// Display is provided by env variable
|
||||
s.Display = os.Getenv("DISPLAY")
|
||||
s.Display = viper.GetString("desktop.display")
|
||||
|
||||
s.ScreenWidth = 1280
|
||||
s.ScreenHeight = 720
|
||||
s.ScreenRate = 30
|
||||
// Display is provided by env variable unless explicitly set
|
||||
if s.Display == "" {
|
||||
s.Display = os.Getenv("DISPLAY")
|
||||
}
|
||||
|
||||
s.ScreenSize = types.ScreenSize{
|
||||
Width: 1280,
|
||||
Height: 720,
|
||||
Rate: 30,
|
||||
}
|
||||
|
||||
r := regexp.MustCompile(`([0-9]{1,4})x([0-9]{1,4})@([0-9]{1,3})`)
|
||||
res := r.FindStringSubmatch(viper.GetString("screen"))
|
||||
res := r.FindStringSubmatch(viper.GetString("desktop.screen"))
|
||||
|
||||
if len(res) > 0 {
|
||||
width, err1 := strconv.ParseInt(res[1], 10, 64)
|
||||
|
@ -43,9 +96,35 @@ func (s *Desktop) Set() {
|
|||
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 = int16(rate)
|
||||
s.ScreenSize.Width = int(width)
|
||||
s.ScreenSize.Height = int(height)
|
||||
s.ScreenSize.Rate = int16(rate)
|
||||
}
|
||||
}
|
||||
|
||||
s.UseInputDriver = viper.GetBool("desktop.input.enabled")
|
||||
s.InputSocket = viper.GetString("desktop.input.socket")
|
||||
s.Unminimize = viper.GetBool("desktop.unminimize")
|
||||
s.UploadDrop = viper.GetBool("desktop.upload_drop")
|
||||
s.FileChooserDialog = viper.GetBool("desktop.file_chooser_dialog")
|
||||
}
|
||||
|
||||
func (s *Desktop) SetV2() {
|
||||
if viper.IsSet("screen") {
|
||||
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.ScreenSize.Width = int(width)
|
||||
s.ScreenSize.Height = int(height)
|
||||
s.ScreenSize.Rate = int16(rate)
|
||||
}
|
||||
}
|
||||
log.Warn().Msg("you are using v2 configuration 'NEKO_SCREEN' which is deprecated, please use 'NEKO_DESKTOP_SCREEN' instead")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -68,6 +68,20 @@ func (Member) Init(cmd *cobra.Command) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (Member) InitV2(cmd *cobra.Command) error {
|
||||
cmd.PersistentFlags().String("password", "", "V2: password for connecting to stream")
|
||||
if err := viper.BindPFlag("password", cmd.PersistentFlags().Lookup("password")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("password_admin", "", "V2: admin password for connecting to stream")
|
||||
if err := viper.BindPFlag("password_admin", cmd.PersistentFlags().Lookup("password_admin")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Member) Set() {
|
||||
s.Provider = viper.GetString("member.provider")
|
||||
|
||||
|
@ -126,3 +140,20 @@ func (s *Member) Set() {
|
|||
log.Warn().Err(err).Msgf("unable to parse member multiuser admin profile")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Member) SetV2() {
|
||||
if viper.IsSet("password") || viper.IsSet("password_admin") {
|
||||
s.Provider = "multiuser"
|
||||
if userPassword := viper.GetString("password"); userPassword != "" {
|
||||
s.Multiuser.UserPassword = userPassword
|
||||
} else {
|
||||
s.Multiuser.UserPassword = "neko"
|
||||
}
|
||||
if adminPassword := viper.GetString("password_admin"); adminPassword != "" {
|
||||
s.Multiuser.AdminPassword = adminPassword
|
||||
} else {
|
||||
s.Multiuser.AdminPassword = "admin"
|
||||
}
|
||||
log.Warn().Msg("you are using v2 configuration 'NEKO_PASSWORD' and 'NEKO_PASSWORD_ADMIN' which are deprecated, please use 'NEKO_MEMBER_MULTIUSER_USER_PASSWORD' and 'NEKO_MEMBER_MULTIUSER_ADMIN_PASSWORD' with 'NEKO_MEMBER_PROVIDER=multiuser' instead")
|
||||
}
|
||||
}
|
|
@ -1,29 +1,69 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type Root struct {
|
||||
Debug bool
|
||||
Logs bool
|
||||
CfgFile string
|
||||
Config string
|
||||
|
||||
LogLevel zerolog.Level
|
||||
LogTime string
|
||||
LogJson bool
|
||||
LogNocolor bool
|
||||
LogDir string
|
||||
}
|
||||
|
||||
func (Root) Init(cmd *cobra.Command) error {
|
||||
cmd.PersistentFlags().StringP("config", "c", "", "configuration file path")
|
||||
if err := viper.BindPFlag("config", cmd.PersistentFlags().Lookup("config")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// just a shortcut
|
||||
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 {
|
||||
cmd.PersistentFlags().String("log.level", "info", "set log level (trace, debug, info, warn, error, fatal, panic, disabled)")
|
||||
if err := viper.BindPFlag("log.level", cmd.PersistentFlags().Lookup("log.level")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("config", "", "configuration file path")
|
||||
if err := viper.BindPFlag("config", cmd.PersistentFlags().Lookup("config")); err != nil {
|
||||
cmd.PersistentFlags().String("log.time", "unix", "time format used in logs (unix, unixms, unixmicro)")
|
||||
if err := viper.BindPFlag("log.time", cmd.PersistentFlags().Lookup("log.time")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Bool("log.json", false, "logs in JSON format")
|
||||
if err := viper.BindPFlag("log.json", cmd.PersistentFlags().Lookup("log.json")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Bool("log.nocolor", false, "no ANSI colors in non-JSON output")
|
||||
if err := viper.BindPFlag("log.nocolor", cmd.PersistentFlags().Lookup("log.nocolor")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("log.dir", "", "logging directory to store logs")
|
||||
if err := viper.BindPFlag("log.dir", cmd.PersistentFlags().Lookup("log.dir")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (Root) InitV2(cmd *cobra.Command) error {
|
||||
cmd.PersistentFlags().BoolP("logs", "l", false, "V2: save logs to file")
|
||||
if err := viper.BindPFlag("logs", cmd.PersistentFlags().Lookup("logs")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -31,7 +71,53 @@ func (Root) Init(cmd *cobra.Command) error {
|
|||
}
|
||||
|
||||
func (s *Root) Set() {
|
||||
s.Logs = viper.GetBool("logs")
|
||||
s.Debug = viper.GetBool("debug")
|
||||
s.CfgFile = viper.GetString("config")
|
||||
s.Config = viper.GetString("config")
|
||||
|
||||
logLevel := viper.GetString("log.level")
|
||||
level, err := zerolog.ParseLevel(logLevel)
|
||||
if err != nil {
|
||||
log.Warn().Msgf("unknown log level %s", logLevel)
|
||||
} else {
|
||||
s.LogLevel = level
|
||||
}
|
||||
|
||||
logTime := viper.GetString("log.time")
|
||||
switch logTime {
|
||||
case "unix":
|
||||
s.LogTime = zerolog.TimeFormatUnix
|
||||
case "unixms":
|
||||
s.LogTime = zerolog.TimeFormatUnixMs
|
||||
case "unixmicro":
|
||||
s.LogTime = zerolog.TimeFormatUnixMicro
|
||||
default:
|
||||
log.Warn().Msgf("unknown log time %s", logTime)
|
||||
}
|
||||
|
||||
s.LogJson = viper.GetBool("log.json")
|
||||
s.LogNocolor = viper.GetBool("log.nocolor")
|
||||
s.LogDir = viper.GetString("log.dir")
|
||||
|
||||
if viper.GetBool("debug") && s.LogLevel != zerolog.TraceLevel {
|
||||
s.LogLevel = zerolog.DebugLevel
|
||||
}
|
||||
|
||||
// support for NO_COLOR env variable: https://no-color.org/
|
||||
if os.Getenv("NO_COLOR") != "" {
|
||||
s.LogNocolor = true
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Root) SetV2() {
|
||||
if viper.IsSet("logs") {
|
||||
if viper.GetBool("logs") {
|
||||
logs := filepath.Join(".", "logs")
|
||||
if runtime.GOOS == "linux" {
|
||||
logs = "/var/log/neko"
|
||||
}
|
||||
s.LogDir = logs
|
||||
} else {
|
||||
s.LogDir = ""
|
||||
}
|
||||
log.Warn().Msg("you are using v2 configuration 'NEKO_LOGS' which is deprecated, please use 'NEKO_LOG_DIR=/path/to/logs' instead")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"path"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"m1k1o/neko/internal/utils"
|
||||
"github.com/demodesk/neko/pkg/utils"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
|
@ -17,41 +17,92 @@ type Server struct {
|
|||
Proxy bool
|
||||
Static string
|
||||
PathPrefix string
|
||||
PProf bool
|
||||
Metrics bool
|
||||
CORS []string
|
||||
}
|
||||
|
||||
func (Server) Init(cmd *cobra.Command) error {
|
||||
cmd.PersistentFlags().String("bind", "127.0.0.1:8080", "address/port/socket to serve neko")
|
||||
cmd.PersistentFlags().String("server.bind", "127.0.0.1:8080", "address/port/socket to serve neko")
|
||||
if err := viper.BindPFlag("server.bind", cmd.PersistentFlags().Lookup("server.bind")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("server.cert", "", "path to the SSL cert used to secure the neko server")
|
||||
if err := viper.BindPFlag("server.cert", cmd.PersistentFlags().Lookup("server.cert")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("server.key", "", "path to the SSL key used to secure the neko server")
|
||||
if err := viper.BindPFlag("server.key", cmd.PersistentFlags().Lookup("server.key")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Bool("server.proxy", false, "trust reverse proxy headers")
|
||||
if err := viper.BindPFlag("server.proxy", cmd.PersistentFlags().Lookup("server.proxy")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("server.static", "", "path to neko client files to serve")
|
||||
if err := viper.BindPFlag("server.static", cmd.PersistentFlags().Lookup("server.static")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("server.path_prefix", "/", "path prefix for HTTP requests")
|
||||
if err := viper.BindPFlag("server.path_prefix", cmd.PersistentFlags().Lookup("server.path_prefix")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Bool("server.pprof", false, "enable pprof endpoint available at /debug/pprof")
|
||||
if err := viper.BindPFlag("server.pprof", cmd.PersistentFlags().Lookup("server.pprof")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Bool("server.metrics", true, "enable prometheus metrics available at /metrics")
|
||||
if err := viper.BindPFlag("server.metrics", cmd.PersistentFlags().Lookup("server.metrics")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().StringSlice("server.cors", []string{}, "list of allowed origins for CORS, if empty CORS is disabled, if '*' is present all origins are allowed")
|
||||
if err := viper.BindPFlag("server.cors", cmd.PersistentFlags().Lookup("server.cors")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (Server) InitV2(cmd *cobra.Command) error {
|
||||
cmd.PersistentFlags().String("bind", "", "V2: 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")
|
||||
cmd.PersistentFlags().String("cert", "", "V2: 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")
|
||||
cmd.PersistentFlags().String("key", "", "V2: 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().Bool("proxy", false, "enable reverse proxy mode")
|
||||
cmd.PersistentFlags().Bool("proxy", false, "V2: enable reverse proxy mode")
|
||||
if err := viper.BindPFlag("proxy", cmd.PersistentFlags().Lookup("proxy")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("static", "./www", "path to neko client files to serve")
|
||||
cmd.PersistentFlags().String("static", "", "V2: path to neko client files to serve")
|
||||
if err := viper.BindPFlag("static", cmd.PersistentFlags().Lookup("static")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("path_prefix", "/", "path prefix for HTTP requests")
|
||||
cmd.PersistentFlags().String("path_prefix", "", "V2: path prefix for HTTP requests")
|
||||
if err := viper.BindPFlag("path_prefix", cmd.PersistentFlags().Lookup("path_prefix")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().StringSlice("cors", []string{"*"}, "list of allowed origins for CORS")
|
||||
cmd.PersistentFlags().StringSlice("cors", []string{}, "V2: list of allowed origins for CORS")
|
||||
if err := viper.BindPFlag("cors", cmd.PersistentFlags().Lookup("cors")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -60,21 +111,68 @@ func (Server) Init(cmd *cobra.Command) error {
|
|||
}
|
||||
|
||||
func (s *Server) Set() {
|
||||
s.Cert = viper.GetString("cert")
|
||||
s.Key = viper.GetString("key")
|
||||
s.Bind = viper.GetString("bind")
|
||||
s.Proxy = viper.GetBool("proxy")
|
||||
s.Static = viper.GetString("static")
|
||||
s.PathPrefix = path.Join("/", path.Clean(viper.GetString("path_prefix")))
|
||||
s.Cert = viper.GetString("server.cert")
|
||||
s.Key = viper.GetString("server.key")
|
||||
s.Bind = viper.GetString("server.bind")
|
||||
s.Proxy = viper.GetBool("server.proxy")
|
||||
s.Static = viper.GetString("server.static")
|
||||
s.PathPrefix = path.Join("/", path.Clean(viper.GetString("server.path_prefix")))
|
||||
s.PProf = viper.GetBool("server.pprof")
|
||||
s.Metrics = viper.GetBool("server.metrics")
|
||||
|
||||
s.CORS = viper.GetStringSlice("cors")
|
||||
s.CORS = viper.GetStringSlice("server.cors")
|
||||
in, _ := utils.ArrayIn("*", s.CORS)
|
||||
if len(s.CORS) == 0 || in {
|
||||
s.CORS = []string{"*"}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) AllowOrigin(r *http.Request, origin string) bool {
|
||||
func (s *Server) SetV2() {
|
||||
if viper.IsSet("cert") {
|
||||
s.Cert = viper.GetString("cert")
|
||||
log.Warn().Msg("you are using v2 configuration 'NEKO_CERT' which is deprecated, please use 'NEKO_SERVER_CERT' instead")
|
||||
}
|
||||
if viper.IsSet("key") {
|
||||
s.Key = viper.GetString("key")
|
||||
log.Warn().Msg("you are using v2 configuration 'NEKO_KEY' which is deprecated, please use 'NEKO_SERVER_KEY' instead")
|
||||
}
|
||||
if viper.IsSet("bind") {
|
||||
s.Bind = viper.GetString("bind")
|
||||
log.Warn().Msg("you are using v2 configuration 'NEKO_BIND' which is deprecated, please use 'NEKO_SERVER_BIND' instead")
|
||||
}
|
||||
if viper.IsSet("proxy") {
|
||||
s.Proxy = viper.GetBool("proxy")
|
||||
log.Warn().Msg("you are using v2 configuration 'NEKO_PROXY' which is deprecated, please use 'NEKO_SERVER_PROXY' instead")
|
||||
}
|
||||
if viper.IsSet("static") {
|
||||
s.Static = viper.GetString("static")
|
||||
log.Warn().Msg("you are using v2 configuration 'NEKO_STATIC' which is deprecated, please use 'NEKO_SERVER_STATIC' instead")
|
||||
}
|
||||
if viper.IsSet("path_prefix") {
|
||||
s.PathPrefix = path.Join("/", path.Clean(viper.GetString("path_prefix")))
|
||||
log.Warn().Msg("you are using v2 configuration 'NEKO_PATH_PREFIX' which is deprecated, please use 'NEKO_SERVER_PATH_PREFIX' instead")
|
||||
}
|
||||
if viper.IsSet("cors") {
|
||||
s.CORS = viper.GetStringSlice("cors")
|
||||
in, _ := utils.ArrayIn("*", s.CORS)
|
||||
if len(s.CORS) == 0 || in {
|
||||
s.CORS = []string{"*"}
|
||||
}
|
||||
log.Warn().Msg("you are using v2 configuration 'NEKO_CORS' which is deprecated, please use 'NEKO_SERVER_CORS' instead")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) HasCors() bool {
|
||||
return len(s.CORS) > 0
|
||||
}
|
||||
|
||||
func (s *Server) AllowOrigin(origin string) bool {
|
||||
// if CORS is disabled, allow all origins
|
||||
if len(s.CORS) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
// if CORS is enabled, allow only origins in the list
|
||||
in, _ := utils.ArrayIn(origin, s.CORS)
|
||||
return in || s.CORS[0] == "*"
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package config
|
|||
import (
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
@ -11,7 +12,9 @@ type Session struct {
|
|||
File string
|
||||
|
||||
PrivateMode bool
|
||||
LockedLogins bool
|
||||
LockedControls bool
|
||||
ControlProtection bool
|
||||
ImplicitHosting bool
|
||||
InactiveCursors bool
|
||||
MercifulReconnect bool
|
||||
|
@ -34,11 +37,21 @@ func (Session) Init(cmd *cobra.Command) error {
|
|||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Bool("session.locked_logins", false, "whether logins should be locked for users initially")
|
||||
if err := viper.BindPFlag("session.locked_logins", cmd.PersistentFlags().Lookup("session.locked_logins")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Bool("session.locked_controls", false, "whether controls should be locked for users initially")
|
||||
if err := viper.BindPFlag("session.locked_controls", cmd.PersistentFlags().Lookup("session.locked_controls")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Bool("session.control_protection", false, "users can gain control only if at least one admin is in the room")
|
||||
if err := viper.BindPFlag("session.control_protection", cmd.PersistentFlags().Lookup("session.control_protection")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Bool("session.implicit_hosting", true, "allow implicit control switching")
|
||||
if err := viper.BindPFlag("session.implicit_hosting", cmd.PersistentFlags().Lookup("session.implicit_hosting")); err != nil {
|
||||
return err
|
||||
|
@ -83,11 +96,32 @@ func (Session) Init(cmd *cobra.Command) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (Session) InitV2(cmd *cobra.Command) error {
|
||||
cmd.PersistentFlags().StringSlice("locks", []string{}, "V2: resources, that will be locked when starting (control, login)")
|
||||
if err := viper.BindPFlag("locks", cmd.PersistentFlags().Lookup("locks")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Bool("control_protection", false, "V2: control protection means, users can gain control only if at least one admin is in the room")
|
||||
if err := viper.BindPFlag("control_protection", cmd.PersistentFlags().Lookup("control_protection")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Bool("implicit_control", false, "V2: if enabled members can gain control implicitly")
|
||||
if err := viper.BindPFlag("implicit_control", cmd.PersistentFlags().Lookup("implicit_control")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Session) Set() {
|
||||
s.File = viper.GetString("session.file")
|
||||
|
||||
s.PrivateMode = viper.GetBool("session.private_mode")
|
||||
s.LockedLogins = viper.GetBool("session.locked_logins")
|
||||
s.LockedControls = viper.GetBool("session.locked_controls")
|
||||
s.ControlProtection = viper.GetBool("session.control_protection")
|
||||
s.ImplicitHosting = viper.GetBool("session.implicit_hosting")
|
||||
s.InactiveCursors = viper.GetBool("session.inactive_cursors")
|
||||
s.MercifulReconnect = viper.GetBool("session.merciful_reconnect")
|
||||
|
@ -98,3 +132,28 @@ func (s *Session) Set() {
|
|||
s.CookieExpiration = time.Duration(viper.GetInt("session.cookie.expiration")) * time.Hour
|
||||
s.CookieSecure = viper.GetBool("session.cookie.secure")
|
||||
}
|
||||
|
||||
func (s *Session) SetV2() {
|
||||
if viper.IsSet("locks") {
|
||||
locks := viper.GetStringSlice("locks")
|
||||
for _, lock := range locks {
|
||||
switch lock {
|
||||
// TODO: file_transfer
|
||||
case "control":
|
||||
s.LockedControls = true
|
||||
case "login":
|
||||
s.LockedLogins = true
|
||||
}
|
||||
}
|
||||
log.Warn().Msg("you are using v2 configuration 'NEKO_LOCKS' which is deprecated, please use 'NEKO_SESSION_LOCKED_CONTROLS' and 'NEKO_SESSION_LOCKED_LOGINS' instead")
|
||||
}
|
||||
|
||||
if viper.IsSet("implicit_control") {
|
||||
s.ImplicitHosting = viper.GetBool("implicit_control")
|
||||
log.Warn().Msg("you are using v2 configuration 'NEKO_IMPLICIT_CONTROL' which is deprecated, please use 'NEKO_SESSION_IMPLICIT_HOSTING' instead")
|
||||
}
|
||||
if viper.IsSet("control_protection") {
|
||||
s.ControlProtection = viper.GetBool("control_protection")
|
||||
log.Warn().Msg("you are using v2 configuration 'NEKO_CONTROL_PROTECTION' which is deprecated, please use 'NEKO_SESSION_CONTROL_PROTECTION' instead")
|
||||
}
|
||||
}
|
|
@ -4,131 +4,396 @@ import (
|
|||
"encoding/json"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"m1k1o/neko/internal/utils"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/pion/webrtc/v3"
|
||||
"github.com/demodesk/neko/pkg/types"
|
||||
"github.com/demodesk/neko/pkg/utils"
|
||||
)
|
||||
|
||||
type WebRTC struct {
|
||||
ICELite bool
|
||||
ICEServers []webrtc.ICEServer
|
||||
EphemeralMin uint16
|
||||
EphemeralMax uint16
|
||||
NAT1To1IPs []string
|
||||
TCPMUX int
|
||||
UDPMUX int
|
||||
// default stun server
|
||||
const defStunSrv = "stun:stun.l.google.com:19302"
|
||||
|
||||
ImplicitControl bool
|
||||
type WebRTCEstimator struct {
|
||||
Enabled bool
|
||||
Passive bool
|
||||
Debug bool
|
||||
InitialBitrate int
|
||||
|
||||
// how often to read and process bandwidth estimation reports
|
||||
ReadInterval time.Duration
|
||||
// how long to wait for stable connection (only neutral or upward trend) before upgrading
|
||||
StableDuration time.Duration
|
||||
// how long to wait for unstable connection (downward trend) before downgrading
|
||||
UnstableDuration time.Duration
|
||||
// how long to wait for stalled connection (neutral trend with low bandwidth) before downgrading
|
||||
StalledDuration time.Duration
|
||||
// how long to wait before downgrading again after previous downgrade
|
||||
DowngradeBackoff time.Duration
|
||||
// how long to wait before upgrading again after previous upgrade
|
||||
UpgradeBackoff time.Duration
|
||||
// how bigger the difference between estimated and stream bitrate must be to trigger upgrade/downgrade
|
||||
DiffThreshold float64
|
||||
}
|
||||
|
||||
type WebRTC struct {
|
||||
ICELite bool
|
||||
ICETrickle bool
|
||||
ICEServersFrontend []types.ICEServer
|
||||
ICEServersBackend []types.ICEServer
|
||||
EphemeralMin uint16
|
||||
EphemeralMax uint16
|
||||
TCPMux int
|
||||
UDPMux int
|
||||
|
||||
NAT1To1IPs []string
|
||||
IpRetrievalUrl string
|
||||
|
||||
Estimator WebRTCEstimator
|
||||
}
|
||||
|
||||
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")
|
||||
cmd.PersistentFlags().Bool("webrtc.icelite", false, "configures whether or not the ICE agent should be a lite agent")
|
||||
if err := viper.BindPFlag("webrtc.icelite", cmd.PersistentFlags().Lookup("webrtc.icelite")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Bool("webrtc.icetrickle", true, "configures whether cadidates should be sent asynchronously using Trickle ICE")
|
||||
if err := viper.BindPFlag("webrtc.icetrickle", cmd.PersistentFlags().Lookup("webrtc.icetrickle")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Looks like this is conflicting with the frontend and backend ICE servers since latest versions
|
||||
//cmd.PersistentFlags().String("webrtc.iceservers", "[]", "Global STUN and TURN servers in JSON format with `urls`, `username` and `credential` keys")
|
||||
//if err := viper.BindPFlag("webrtc.iceservers", cmd.PersistentFlags().Lookup("webrtc.iceservers")); err != nil {
|
||||
// return err
|
||||
//}
|
||||
|
||||
cmd.PersistentFlags().String("webrtc.iceservers.frontend", "[]", "Frontend only STUN and TURN servers in JSON format with `urls`, `username` and `credential` keys")
|
||||
if err := viper.BindPFlag("webrtc.iceservers.frontend", cmd.PersistentFlags().Lookup("webrtc.iceservers.frontend")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("webrtc.iceservers.backend", "[]", "Backend only STUN and TURN servers in JSON format with `urls`, `username` and `credential` keys")
|
||||
if err := viper.BindPFlag("webrtc.iceservers.backend", cmd.PersistentFlags().Lookup("webrtc.iceservers.backend")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("webrtc.epr", "", "limits the pool of ephemeral ports that ICE UDP connections can allocate from")
|
||||
if err := viper.BindPFlag("webrtc.epr", cmd.PersistentFlags().Lookup("webrtc.epr")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Int("webrtc.tcpmux", 0, "single TCP mux port for all peers")
|
||||
if err := viper.BindPFlag("webrtc.tcpmux", cmd.PersistentFlags().Lookup("webrtc.tcpmux")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Int("webrtc.udpmux", 0, "single UDP mux port for all peers, replaces EPR")
|
||||
if err := viper.BindPFlag("webrtc.udpmux", cmd.PersistentFlags().Lookup("webrtc.udpmux")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().StringSlice("webrtc.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("webrtc.nat1to1", cmd.PersistentFlags().Lookup("webrtc.nat1to1")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("webrtc.ip_retrieval_url", "https://checkip.amazonaws.com", "URL address used for retrieval of the external IP address")
|
||||
if err := viper.BindPFlag("webrtc.ip_retrieval_url", cmd.PersistentFlags().Lookup("webrtc.ip_retrieval_url")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// bandwidth estimator
|
||||
|
||||
cmd.PersistentFlags().Bool("webrtc.estimator.enabled", false, "enables the bandwidth estimator")
|
||||
if err := viper.BindPFlag("webrtc.estimator.enabled", cmd.PersistentFlags().Lookup("webrtc.estimator.enabled")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Bool("webrtc.estimator.passive", false, "passive estimator mode, when it does not switch pipelines, only estimates")
|
||||
if err := viper.BindPFlag("webrtc.estimator.passive", cmd.PersistentFlags().Lookup("webrtc.estimator.passive")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Bool("webrtc.estimator.debug", false, "enables debug logging for the bandwidth estimator")
|
||||
if err := viper.BindPFlag("webrtc.estimator.debug", cmd.PersistentFlags().Lookup("webrtc.estimator.debug")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Int("webrtc.estimator.initial_bitrate", 1_000_000, "initial bitrate for the bandwidth estimator")
|
||||
if err := viper.BindPFlag("webrtc.estimator.initial_bitrate", cmd.PersistentFlags().Lookup("webrtc.estimator.initial_bitrate")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Duration("webrtc.estimator.read_interval", 2*time.Second, "how often to read and process bandwidth estimation reports")
|
||||
if err := viper.BindPFlag("webrtc.estimator.read_interval", cmd.PersistentFlags().Lookup("webrtc.estimator.read_interval")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Duration("webrtc.estimator.stable_duration", 12*time.Second, "how long to wait for stable connection (upward or neutral trend) before upgrading")
|
||||
if err := viper.BindPFlag("webrtc.estimator.stable_duration", cmd.PersistentFlags().Lookup("webrtc.estimator.stable_duration")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Duration("webrtc.estimator.unstable_duration", 6*time.Second, "how long to wait for stalled connection (neutral trend with low bandwidth) before downgrading")
|
||||
if err := viper.BindPFlag("webrtc.estimator.unstable_duration", cmd.PersistentFlags().Lookup("webrtc.estimator.unstable_duration")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Duration("webrtc.estimator.stalled_duration", 24*time.Second, "how long to wait for stalled bandwidth estimation before downgrading")
|
||||
if err := viper.BindPFlag("webrtc.estimator.stalled_duration", cmd.PersistentFlags().Lookup("webrtc.estimator.stalled_duration")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Duration("webrtc.estimator.downgrade_backoff", 10*time.Second, "how long to wait before downgrading again after previous downgrade")
|
||||
if err := viper.BindPFlag("webrtc.estimator.downgrade_backoff", cmd.PersistentFlags().Lookup("webrtc.estimator.downgrade_backoff")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Duration("webrtc.estimator.upgrade_backoff", 5*time.Second, "how long to wait before upgrading again after previous upgrade")
|
||||
if err := viper.BindPFlag("webrtc.estimator.upgrade_backoff", cmd.PersistentFlags().Lookup("webrtc.estimator.upgrade_backoff")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Float64("webrtc.estimator.diff_threshold", 0.15, "how bigger the difference between estimated and stream bitrate must be to trigger upgrade/downgrade")
|
||||
if err := viper.BindPFlag("webrtc.estimator.diff_threshold", cmd.PersistentFlags().Lookup("webrtc.estimator.diff_threshold")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (WebRTC) InitV2(cmd *cobra.Command) error {
|
||||
cmd.PersistentFlags().String("epr", "", "V2: 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")
|
||||
cmd.PersistentFlags().StringSlice("nat1to1", []string{}, "V2: 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().Int("tcpmux", 0, "single TCP mux port for all peers")
|
||||
cmd.PersistentFlags().Int("tcpmux", 0, "V2: single TCP mux port for all peers")
|
||||
if err := viper.BindPFlag("tcpmux", cmd.PersistentFlags().Lookup("tcpmux")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Int("udpmux", 0, "single UDP mux port for all peers")
|
||||
cmd.PersistentFlags().Int("udpmux", 0, "V2: single UDP mux port for all peers")
|
||||
if err := viper.BindPFlag("udpmux", cmd.PersistentFlags().Lookup("udpmux")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("ipfetch", "http://checkip.amazonaws.com", "automatically fetch IP address from given URL when nat1to1 is not present")
|
||||
cmd.PersistentFlags().String("ipfetch", "", "V2: automatically fetch IP address from given URL when nat1to1 is not present")
|
||||
if err := viper.BindPFlag("ipfetch", cmd.PersistentFlags().Lookup("ipfetch")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Bool("icelite", false, "configures whether or not the ice agent should be a lite agent")
|
||||
cmd.PersistentFlags().Bool("icelite", false, "V2: 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")
|
||||
cmd.PersistentFlags().StringSlice("iceserver", []string{}, "V2: 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
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("iceservers", "", "describes a single STUN and TURN server that can be used by the ICEAgent to establish a connection with a peer")
|
||||
cmd.PersistentFlags().String("iceservers", "", "V2: 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("iceservers", cmd.PersistentFlags().Lookup("iceservers")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO: Should be moved to session config.
|
||||
cmd.PersistentFlags().Bool("implicit_control", false, "if enabled members can gain control implicitly")
|
||||
if err := viper.BindPFlag("implicit_control", cmd.PersistentFlags().Lookup("implicit_control")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *WebRTC) Set() {
|
||||
s.NAT1To1IPs = viper.GetStringSlice("nat1to1")
|
||||
s.TCPMUX = viper.GetInt("tcpmux")
|
||||
s.UDPMUX = viper.GetInt("udpmux")
|
||||
s.ICELite = viper.GetBool("icelite")
|
||||
s.ICEServers = []webrtc.ICEServer{}
|
||||
s.ICELite = viper.GetBool("webrtc.icelite")
|
||||
s.ICETrickle = viper.GetBool("webrtc.icetrickle")
|
||||
|
||||
iceServersJson := viper.GetString("iceservers")
|
||||
if iceServersJson != "" {
|
||||
err := json.Unmarshal([]byte(iceServersJson), &s.ICEServers)
|
||||
if err != nil {
|
||||
log.Panic().Err(err).Msg("failed to process iceservers")
|
||||
// parse frontend ice servers
|
||||
if err := viper.UnmarshalKey("webrtc.iceservers.frontend", &s.ICEServersFrontend, viper.DecodeHook(
|
||||
utils.JsonStringAutoDecode([]types.ICEServer{}),
|
||||
)); err != nil {
|
||||
log.Warn().Err(err).Msgf("unable to parse frontend ICE servers")
|
||||
}
|
||||
|
||||
// parse backend ice servers
|
||||
if err := viper.UnmarshalKey("webrtc.iceservers.backend", &s.ICEServersBackend, viper.DecodeHook(
|
||||
utils.JsonStringAutoDecode([]types.ICEServer{}),
|
||||
)); err != nil {
|
||||
log.Warn().Err(err).Msgf("unable to parse backend ICE servers")
|
||||
}
|
||||
|
||||
if s.ICELite && len(s.ICEServersBackend) > 0 {
|
||||
log.Warn().Msgf("ICE Lite is enabled, but backend ICE servers are configured. Backend ICE servers will be ignored.")
|
||||
}
|
||||
|
||||
// if no frontend or backend ice servers are configured
|
||||
if len(s.ICEServersFrontend) == 0 && len(s.ICEServersBackend) == 0 {
|
||||
// parse global ice servers
|
||||
var iceServers []types.ICEServer
|
||||
if err := viper.UnmarshalKey("webrtc.iceservers", &iceServers, viper.DecodeHook(
|
||||
utils.JsonStringAutoDecode([]types.ICEServer{}),
|
||||
)); err != nil {
|
||||
log.Warn().Err(err).Msgf("unable to parse global ICE servers")
|
||||
}
|
||||
|
||||
// add default stun server if none are configured
|
||||
if len(iceServers) == 0 {
|
||||
iceServers = append(iceServers, types.ICEServer{
|
||||
URLs: []string{defStunSrv},
|
||||
})
|
||||
}
|
||||
|
||||
s.ICEServersFrontend = append(s.ICEServersFrontend, iceServers...)
|
||||
s.ICEServersBackend = append(s.ICEServersBackend, iceServers...)
|
||||
}
|
||||
|
||||
s.TCPMux = viper.GetInt("webrtc.tcpmux")
|
||||
s.UDPMux = viper.GetInt("webrtc.udpmux")
|
||||
|
||||
epr := viper.GetString("webrtc.epr")
|
||||
if epr != "" {
|
||||
ports := strings.SplitN(epr, "-", -1)
|
||||
if len(ports) > 1 {
|
||||
min, err := strconv.ParseUint(ports[0], 10, 16)
|
||||
if err != nil {
|
||||
log.Panic().Err(err).Msgf("unable to parse ephemeral min port")
|
||||
}
|
||||
|
||||
max, err := strconv.ParseUint(ports[1], 10, 16)
|
||||
if err != nil {
|
||||
log.Panic().Err(err).Msgf("unable to parse ephemeral max port")
|
||||
}
|
||||
|
||||
s.EphemeralMin = uint16(min)
|
||||
s.EphemeralMax = uint16(max)
|
||||
}
|
||||
|
||||
if s.EphemeralMin > s.EphemeralMax {
|
||||
log.Panic().Msgf("ephemeral min port cannot be bigger than max")
|
||||
}
|
||||
}
|
||||
|
||||
iceServerSlice := viper.GetStringSlice("iceserver")
|
||||
if len(iceServerSlice) > 0 {
|
||||
s.ICEServers = append(s.ICEServers, webrtc.ICEServer{URLs: iceServerSlice})
|
||||
if epr == "" && s.TCPMux == 0 && s.UDPMux == 0 {
|
||||
// using default epr range
|
||||
s.EphemeralMin = 59000
|
||||
s.EphemeralMax = 59100
|
||||
|
||||
log.Warn().
|
||||
Uint16("min", s.EphemeralMin).
|
||||
Uint16("max", s.EphemeralMax).
|
||||
Msgf("no TCP, UDP mux or epr specified, using default epr range")
|
||||
}
|
||||
|
||||
if len(s.NAT1To1IPs) == 0 {
|
||||
ipfetch := viper.GetString("ipfetch")
|
||||
ip, err := utils.GetIP(ipfetch)
|
||||
if err != nil {
|
||||
log.Panic().Err(err).Str("ipfetch", ipfetch).Msg("failed to fetch ip address")
|
||||
}
|
||||
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)
|
||||
s.NAT1To1IPs = viper.GetStringSlice("webrtc.nat1to1")
|
||||
s.IpRetrievalUrl = viper.GetString("webrtc.ip_retrieval_url")
|
||||
if s.IpRetrievalUrl != "" && len(s.NAT1To1IPs) == 0 {
|
||||
ip, err := utils.HttpRequestGET(s.IpRetrievalUrl)
|
||||
if err == nil {
|
||||
min = uint16(start)
|
||||
}
|
||||
|
||||
end, err := strconv.ParseUint(ports[1], 10, 16)
|
||||
if err == nil {
|
||||
max = uint16(end)
|
||||
s.NAT1To1IPs = append(s.NAT1To1IPs, ip)
|
||||
} else {
|
||||
log.Warn().Err(err).Msgf("IP retrieval failed")
|
||||
}
|
||||
}
|
||||
|
||||
if min > max {
|
||||
s.EphemeralMin = max
|
||||
s.EphemeralMax = min
|
||||
} else {
|
||||
s.EphemeralMin = min
|
||||
s.EphemeralMax = max
|
||||
}
|
||||
// bandwidth estimator
|
||||
|
||||
// TODO: Should be moved to session config.
|
||||
s.ImplicitControl = viper.GetBool("implicit_control")
|
||||
s.Estimator.Enabled = viper.GetBool("webrtc.estimator.enabled")
|
||||
s.Estimator.Passive = viper.GetBool("webrtc.estimator.passive")
|
||||
s.Estimator.Debug = viper.GetBool("webrtc.estimator.debug")
|
||||
s.Estimator.InitialBitrate = viper.GetInt("webrtc.estimator.initial_bitrate")
|
||||
s.Estimator.ReadInterval = viper.GetDuration("webrtc.estimator.read_interval")
|
||||
s.Estimator.StableDuration = viper.GetDuration("webrtc.estimator.stable_duration")
|
||||
s.Estimator.UnstableDuration = viper.GetDuration("webrtc.estimator.unstable_duration")
|
||||
s.Estimator.StalledDuration = viper.GetDuration("webrtc.estimator.stalled_duration")
|
||||
s.Estimator.DowngradeBackoff = viper.GetDuration("webrtc.estimator.downgrade_backoff")
|
||||
s.Estimator.UpgradeBackoff = viper.GetDuration("webrtc.estimator.upgrade_backoff")
|
||||
s.Estimator.DiffThreshold = viper.GetFloat64("webrtc.estimator.diff_threshold")
|
||||
}
|
||||
|
||||
func (s *WebRTC) SetV2() {
|
||||
if viper.IsSet("nat1to1") {
|
||||
s.NAT1To1IPs = viper.GetStringSlice("nat1to1")
|
||||
log.Warn().Msg("you are using v2 configuration 'NEKO_NAT1TO1' which is deprecated, please use 'NEKO_WEBRTC_NAT1TO1' instead")
|
||||
}
|
||||
if viper.IsSet("tcpmux") {
|
||||
s.TCPMux = viper.GetInt("tcpmux")
|
||||
log.Warn().Msg("you are using v2 configuration 'NEKO_TCPMUX' which is deprecated, please use 'NEKO_WEBRTC_TCPMUX' instead")
|
||||
}
|
||||
if viper.IsSet("udpmux") {
|
||||
s.UDPMux = viper.GetInt("udpmux")
|
||||
log.Warn().Msg("you are using v2 configuration 'NEKO_UDPMUX' which is deprecated, please use 'NEKO_WEBRTC_UDPMUX' instead")
|
||||
}
|
||||
if viper.IsSet("icelite") {
|
||||
s.ICELite = viper.GetBool("icelite")
|
||||
log.Warn().Msg("you are using v2 configuration 'NEKO_ICELITE' which is deprecated, please use 'NEKO_WEBRTC_ICELITE' instead")
|
||||
}
|
||||
|
||||
if viper.IsSet("iceservers") {
|
||||
iceServers := []types.ICEServer{}
|
||||
iceServersJson := viper.GetString("iceservers")
|
||||
if iceServersJson != "" {
|
||||
err := json.Unmarshal([]byte(iceServersJson), &iceServers)
|
||||
if err != nil {
|
||||
log.Panic().Err(err).Msg("failed to process iceservers")
|
||||
}
|
||||
}
|
||||
s.ICEServersFrontend = iceServers
|
||||
s.ICEServersBackend = iceServers
|
||||
log.Warn().Msg("you are using v2 configuration 'NEKO_ICESERVERS' which is deprecated, please use 'NEKO_WEBRTC_ICESERVERS_FRONTEND' and/or 'NEKO_WEBRTC_ICESERVERS_BACKEND' instead")
|
||||
}
|
||||
|
||||
if viper.IsSet("iceserver") {
|
||||
iceServerSlice := viper.GetStringSlice("iceserver")
|
||||
if len(iceServerSlice) > 0 {
|
||||
s.ICEServersFrontend = append(s.ICEServersFrontend, types.ICEServer{URLs: iceServerSlice})
|
||||
s.ICEServersBackend = append(s.ICEServersBackend, types.ICEServer{URLs: iceServerSlice})
|
||||
}
|
||||
log.Warn().Msg("you are using v2 configuration 'NEKO_ICESERVER' which is deprecated, please use 'NEKO_WEBRTC_ICESERVERS_FRONTEND' and/or 'NEKO_WEBRTC_ICESERVERS_BACKEND' instead")
|
||||
}
|
||||
|
||||
if viper.IsSet("ipfetch") {
|
||||
if len(s.NAT1To1IPs) == 0 {
|
||||
ipfetch := viper.GetString("ipfetch")
|
||||
ip, err := utils.HttpRequestGET(ipfetch)
|
||||
if err != nil {
|
||||
log.Panic().Err(err).Str("ipfetch", ipfetch).Msg("failed to fetch ip address")
|
||||
}
|
||||
s.NAT1To1IPs = append(s.NAT1To1IPs, ip)
|
||||
}
|
||||
log.Warn().Msg("you are using v2 configuration 'NEKO_IPFETCH' which is deprecated, please use 'NEKO_WEBRTC_IP_RETRIEVAL_URL' instead")
|
||||
}
|
||||
|
||||
if viper.IsSet("epr") {
|
||||
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
|
||||
}
|
||||
log.Warn().Msg("you are using v2 configuration 'NEKO_EPR' which is deprecated, please use 'NEKO_WEBRTC_EPR' instead")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,67 +0,0 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type WebSocket struct {
|
||||
Password string
|
||||
AdminPassword string
|
||||
Locks []string
|
||||
|
||||
ControlProtection bool
|
||||
|
||||
FileTransferEnabled bool
|
||||
FileTransferPath string
|
||||
}
|
||||
|
||||
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().StringSlice("locks", []string{}, "resources, that will be locked when starting (control, login)")
|
||||
if err := viper.BindPFlag("locks", cmd.PersistentFlags().Lookup("locks")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Bool("control_protection", false, "control protection means, users can gain control only if at least one admin is in the room")
|
||||
if err := viper.BindPFlag("control_protection", cmd.PersistentFlags().Lookup("control_protection")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// File transfer
|
||||
|
||||
cmd.PersistentFlags().Bool("file_transfer_enabled", false, "enable file transfer feature")
|
||||
if err := viper.BindPFlag("file_transfer_enabled", cmd.PersistentFlags().Lookup("file_transfer_enabled")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("file_transfer_path", "/home/neko/Downloads", "path to use for file transfer")
|
||||
if err := viper.BindPFlag("file_transfer_path", cmd.PersistentFlags().Lookup("file_transfer_path")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *WebSocket) Set() {
|
||||
s.Password = viper.GetString("password")
|
||||
s.AdminPassword = viper.GetString("password_admin")
|
||||
s.Locks = viper.GetStringSlice("locks")
|
||||
|
||||
s.ControlProtection = viper.GetBool("control_protection")
|
||||
|
||||
s.FileTransferEnabled = viper.GetBool("file_transfer_enabled")
|
||||
s.FileTransferPath = viper.GetString("file_transfer_path")
|
||||
s.FileTransferPath = filepath.Clean(s.FileTransferPath)
|
||||
}
|
|
@ -1,11 +1,122 @@
|
|||
package desktop
|
||||
|
||||
import "m1k1o/neko/internal/desktop/clipboard"
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
func (manager *DesktopManagerCtx) ReadClipboard() string {
|
||||
return clipboard.Read()
|
||||
"github.com/demodesk/neko/pkg/types"
|
||||
"github.com/demodesk/neko/pkg/xevent"
|
||||
)
|
||||
|
||||
func (manager *DesktopManagerCtx) ClipboardGetText() (*types.ClipboardText, error) {
|
||||
text, err := manager.ClipboardGetBinary("STRING")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Rich text must not always be available, can fail silently.
|
||||
html, _ := manager.ClipboardGetBinary("text/html")
|
||||
|
||||
return &types.ClipboardText{
|
||||
Text: string(text),
|
||||
HTML: string(html),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (manager *DesktopManagerCtx) WriteClipboard(data string) {
|
||||
clipboard.Write(data)
|
||||
func (manager *DesktopManagerCtx) ClipboardSetText(data types.ClipboardText) error {
|
||||
// TODO: Refactor.
|
||||
// Current implementation is unable to set multiple targets. HTML
|
||||
// is set, if available. Otherwise plain text.
|
||||
|
||||
if data.HTML != "" {
|
||||
return manager.ClipboardSetBinary("text/html", []byte(data.HTML))
|
||||
}
|
||||
|
||||
return manager.ClipboardSetBinary("STRING", []byte(data.Text))
|
||||
}
|
||||
|
||||
func (manager *DesktopManagerCtx) ClipboardGetBinary(mime string) ([]byte, error) {
|
||||
cmd := exec.Command("xclip", "-selection", "clipboard", "-out", "-target", mime)
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
msg := strings.TrimSpace(stderr.String())
|
||||
return nil, fmt.Errorf("%s", msg)
|
||||
}
|
||||
|
||||
return stdout.Bytes(), nil
|
||||
}
|
||||
|
||||
func (manager *DesktopManagerCtx) ClipboardSetBinary(mime string, data []byte) error {
|
||||
cmd := exec.Command("xclip", "-selection", "clipboard", "-in", "-target", mime)
|
||||
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO: Refactor.
|
||||
// We need to wait until the data came to the clipboard.
|
||||
wait := make(chan struct{})
|
||||
xevent.Emmiter.Once("clipboard-updated", func(payload ...any) {
|
||||
wait <- struct{}{}
|
||||
})
|
||||
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
msg := strings.TrimSpace(stderr.String())
|
||||
return fmt.Errorf("%s", msg)
|
||||
}
|
||||
|
||||
_, err = stdin.Write(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
stdin.Close()
|
||||
|
||||
// TODO: Refactor.
|
||||
// cmd.Wait()
|
||||
<-wait
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (manager *DesktopManagerCtx) ClipboardGetTargets() ([]string, error) {
|
||||
cmd := exec.Command("xclip", "-selection", "clipboard", "-out", "-target", "TARGETS")
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
msg := strings.TrimSpace(stderr.String())
|
||||
return nil, fmt.Errorf("%s", msg)
|
||||
}
|
||||
|
||||
var response []string
|
||||
targets := strings.Split(stdout.String(), "\n")
|
||||
for _, target := range targets {
|
||||
if target == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if !strings.Contains(target, "/") {
|
||||
continue
|
||||
}
|
||||
|
||||
response = append(response, target)
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
#include "clipboard.h"
|
||||
|
||||
static clipboard_c *CLIPBOARD = NULL;
|
||||
|
||||
clipboard_c *getClipboard(void) {
|
||||
if (CLIPBOARD == NULL) {
|
||||
CLIPBOARD = clipboard_new(NULL);
|
||||
}
|
||||
|
||||
return CLIPBOARD;
|
||||
}
|
||||
|
||||
void ClipboardSet(char *src) {
|
||||
clipboard_c *cb = getClipboard();
|
||||
clipboard_set_text_ex(cb, src, strlen(src), 0);
|
||||
}
|
||||
|
||||
char *ClipboardGet() {
|
||||
clipboard_c *cb = getClipboard();
|
||||
return clipboard_text_ex(cb, NULL, 0);
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
package clipboard
|
||||
|
||||
/*
|
||||
#cgo linux LDFLAGS: /usr/local/lib/libclipboard.a -lxcb
|
||||
|
||||
#include "clipboard.h"
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
var mu sync.Mutex
|
||||
|
||||
func Read() string {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
clipboardUnsafe := C.ClipboardGet()
|
||||
defer C.free(unsafe.Pointer(clipboardUnsafe))
|
||||
|
||||
return C.GoString(clipboardUnsafe)
|
||||
}
|
||||
|
||||
func Write(data string) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
clipboardUnsafe := C.CString(data)
|
||||
defer C.free(unsafe.Pointer(clipboardUnsafe))
|
||||
|
||||
C.ClipboardSet(clipboardUnsafe)
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <libclipboard.h>
|
||||
#include <string.h>
|
||||
|
||||
clipboard_c *getClipboard(void);
|
||||
|
||||
void ClipboardSet(char *src);
|
||||
char *ClipboardGet();
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue