diff --git a/go.mod b/go.mod index dc4b022a..9316514c 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module demodesk/neko go 1.16 require ( + github.com/PaesslerAG/gval v1.1.0 // indirect github.com/go-chi/chi v1.5.4 github.com/go-chi/cors v1.1.1 github.com/google/uuid v1.2.0 // indirect diff --git a/go.sum b/go.sum index 0192d70e..f8b7e7e3 100644 --- a/go.sum +++ b/go.sum @@ -14,6 +14,9 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7 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/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/PaesslerAG/gval v1.1.0 h1:k3RuxeZDO3eejD4cMPSt+74tUSvTnbGvLx0df4mdwFc= +github.com/PaesslerAG/gval v1.1.0/go.mod h1:y/nm5yEyTeX6av0OfKJNp9rBNj2XrGhAf5+v24IBN1I= +github.com/PaesslerAG/jsonpath v0.1.0/go.mod h1:4BzmtoM/PI8fPO4aQGIusjGxGir2BzcV0grWtFzq1Y8= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= diff --git a/internal/capture/manager.go b/internal/capture/manager.go index 46a88718..6b6c8b5d 100644 --- a/internal/capture/manager.go +++ b/internal/capture/manager.go @@ -8,7 +8,6 @@ import ( "demodesk/neko/internal/config" "demodesk/neko/internal/types" - "demodesk/neko/internal/types/codec" ) type CaptureManagerCtx struct { @@ -56,6 +55,37 @@ func New(desktop types.DesktopManager, config *config.Capture) *CaptureManagerCt ) } + videos := map[string]*StreamManagerCtx{} + videoIDs := []string{} + for key, pipelineConf := range config.Video { + codec, err := pipelineConf.GetCodec() + if err != nil { + logger.Panic().Err(err).Str("video_key", key).Msg("unable to get video codec") + } + + createPipeline := func() string { + screen := desktop.GetScreenSize() + + pipeline, err := pipelineConf.GetPipeline(*screen) + if err != nil { + logger.Panic().Err(err).Str("video_key", key).Msg("unable to get video pipeline") + } + + return fmt.Sprintf( + "ximagesrc display-name=%s show-pointer=false use-damage=false "+ + "! %s "+ + "! appsink name=appsink", config.Display, pipeline, + ) + } + + // trigger function to catch evaluation errors at startup + _ = createPipeline() + + // append to videos + videos[key] = streamNew(codec, createPipeline) + videoIDs = append(videoIDs, key) + } + return &CaptureManagerCtx{ logger: logger, desktop: desktop, @@ -76,65 +106,8 @@ func New(desktop types.DesktopManager, config *config.Capture) *CaptureManagerCt "! appsink name=appsink", config.AudioDevice, config.AudioCodec.Pipeline, ) }), - videos: map[string]*StreamManagerCtx{ - "hd": streamNew(codec.VP8(), func() string { - screen := desktop.GetScreenSize() - bitrate := int((screen.Width * screen.Height * 6) / 4) - buffer := bitrate / 1000 - - return fmt.Sprintf( - "ximagesrc display-name=%s show-pointer=false use-damage=false "+ - "! video/x-raw,framerate=25/1 "+ - "! videoconvert "+ - "! queue "+ - "! vp8enc end-usage=cbr target-bitrate=%d cpu-used=4 threads=4 deadline=1 undershoot=95 keyframe-max-dist=25 min-quantizer=3 max-quantizer=32 buffer-size=%d buffer-initial-size=%d buffer-optimal-size=%d "+ - "! appsink name=appsink", config.Display, bitrate, buffer*6, buffer*4, buffer*5, - ) - }), - "hq": streamNew(codec.VP8(), func() string { - screen := desktop.GetScreenSize() - bitrate := int((screen.Width * screen.Height * 6) / 4) / 2 - buffer := bitrate / 1000 - - return fmt.Sprintf( - "ximagesrc display-name=%s show-pointer=false use-damage=false "+ - "! video/x-raw,framerate=15/1 "+ - "! videoconvert "+ - "! queue "+ - "! vp8enc end-usage=cbr target-bitrate=%d cpu-used=4 threads=4 deadline=1 undershoot=95 keyframe-max-dist=25 min-quantizer=3 max-quantizer=32 buffer-size=%d buffer-initial-size=%d buffer-optimal-size=%d "+ - "! appsink name=appsink", config.Display, bitrate, buffer*6, buffer*4, buffer*5, - ) - }), - "mq": streamNew(codec.VP8(), func() string { - screen := desktop.GetScreenSize() - bitrate := int((screen.Width * screen.Height * 6) / 4) / 3 - buffer := bitrate / 1000 - - return fmt.Sprintf( - "ximagesrc display-name=%s show-pointer=false use-damage=false "+ - "! video/x-raw,framerate=10/1 "+ - "! videoconvert "+ - "! queue "+ - "! vp8enc end-usage=cbr target-bitrate=%d cpu-used=4 threads=4 deadline=1 undershoot=95 keyframe-max-dist=25 min-quantizer=3 max-quantizer=32 buffer-size=%d buffer-initial-size=%d buffer-optimal-size=%d "+ - "! appsink name=appsink", config.Display, bitrate, buffer*6, buffer*4, buffer*5, - ) - }), - "lq": streamNew(codec.VP8(), func() string { - screen := desktop.GetScreenSize() - bitrate := int((screen.Width * screen.Height * 6) / 4) / 4 - buffer := bitrate / 1000 - - return fmt.Sprintf( - "ximagesrc display-name=%s show-pointer=false use-damage=false "+ - "! video/x-raw,framerate=5/1 "+ - "! videoconvert "+ - "! queue "+ - "! vp8enc end-usage=cbr target-bitrate=%d cpu-used=4 threads=4 deadline=1 undershoot=95 keyframe-max-dist=25 min-quantizer=3 max-quantizer=32 buffer-size=%d buffer-initial-size=%d buffer-optimal-size=%d "+ - "! appsink name=appsink", config.Display, bitrate, buffer*6, buffer*4, buffer*5, - ) - }), - }, - videoIDs: []string{"hd", "hq", "mq", "lq"}, + videos: videos, + videoIDs: videoIDs, } } diff --git a/internal/config/capture.go b/internal/config/capture.go index d3612263..1bb4b6f6 100644 --- a/internal/config/capture.go +++ b/internal/config/capture.go @@ -7,12 +7,16 @@ import ( "github.com/spf13/cobra" "github.com/spf13/viper" + "demodesk/neko/internal/types" "demodesk/neko/internal/types/codec" + "demodesk/neko/internal/utils" ) type Capture struct { Display string + Video map[string] types.VideoConfig + AudioDevice string AudioCodec codec.RTPCodec AudioPipeline string @@ -94,6 +98,14 @@ func (s *Capture) Set() { // Display is provided by env variable s.Display = os.Getenv("DISPLAY") + // video + if err := viper.UnmarshalKey("capture.video", &s.Video, viper.DecodeHook( + utils.JsonStringAutoDecode(s.Video), + )); err != nil { + log.Warn().Err(err).Msgf("unable to parse video settings") + } + + // audio s.AudioDevice = viper.GetString("capture.audio.device") s.AudioPipeline = viper.GetString("capture.audio.pipeline") @@ -112,11 +124,13 @@ func (s *Capture) Set() { 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") + // screencast s.ScreencastEnabled = viper.GetBool("capture.screencast.enabled") s.ScreencastRate = viper.GetString("capture.screencast.rate") s.ScreencastQuality = viper.GetString("capture.screencast.quality") diff --git a/internal/types/capture.go b/internal/types/capture.go index e8ea6877..4cf3c337 100644 --- a/internal/types/capture.go +++ b/internal/types/capture.go @@ -1,7 +1,12 @@ package types import ( + "math" + "strings" + "fmt" + "github.com/pion/webrtc/v3/pkg/media" + "github.com/PaesslerAG/gval" "demodesk/neko/internal/types/codec" ) @@ -43,3 +48,98 @@ type CaptureManager interface { Video(videoID string) (StreamManager, bool) VideoIDs() []string } + +type VideoConfig struct { + Codec string `mapstructure:"codec"` + Width string `mapstructure:"width"` // expression + Height string `mapstructure:"height"` // expression + Fps string `mapstructure:"fps"` // expression + GstEncoder string `mapstructure:"gst_encoder"` + GstParams map[string]string `mapstructure:"gst_params"` // map of expressions + GstPipeline string `mapstructure:"gst_pipeline"` +} + +func (config *VideoConfig) GetCodec() (codec.RTPCodec, error) { + switch strings.ToLower(config.Codec) { + case "vp8": + return codec.VP8(), nil + case "vp9": + return codec.VP9(), nil + case "h264": + return codec.H264(), nil + default: + return codec.RTPCodec{}, fmt.Errorf("unknown codec") + } +} + +func (config *VideoConfig) GetPipeline(screen ScreenSize) (string, error) { + if config.GstPipeline != "" { + return config.GstPipeline, nil + } + + values := map[string]interface{}{ + "width": screen.Width, + "height": screen.Height, + "fps": screen.Rate, + } + + language := []gval.Language{ + gval.Function("round", func(args ...interface{}) (interface{}, error) { + return (int)(math.Round(args[0].(float64))), nil + }), + } + + // get fps pipeline + fpsPipeline := "video/x-raw ! videoconvert ! queue" + if config.Fps != "" { + var err error + val, err := gval.Evaluate(config.Fps, values, language...) + if err != nil { + return "", err + } + + if val != nil { + // TODO: To fraction. + fpsPipeline = fmt.Sprintf("video/x-raw,framerate=%v ! videoconvert ! queue", val) + } + } + + // get scale pipeline + scalePipeline := "" + if config.Width != "" && config.Height != "" { + w, err := gval.Evaluate(config.Width, values, language...) + if err != nil { + return "", err + } + + h, err := gval.Evaluate(config.Height, values, language...) + if err != nil { + return "", err + } + + if w != nil && h != nil { + scalePipeline = fmt.Sprintf("! videoscale ! video/x-raw,width=%v,height=%v ! queue", w, h) + } + } + + // get encoder pipeline + encPipeline := fmt.Sprintf("! %s", config.GstEncoder) + for key, expr := range config.GstParams { + if expr == "" { + continue + } + + val, err := gval.Evaluate(expr, values, language...) + if err != nil { + return "", err + } + + if val != nil { + encPipeline += fmt.Sprintf(" %s=%v", key, val) + } else { + encPipeline += fmt.Sprintf(" %s=%s", key, expr) + } + } + + return fmt.Sprintf("%s %s %s", fpsPipeline, scalePipeline, encPipeline), nil +}