diff --git a/internal/capture/manager.go b/internal/capture/manager.go index c59702e8..c4d00bf5 100644 --- a/internal/capture/manager.go +++ b/internal/capture/manager.go @@ -2,6 +2,7 @@ package capture import ( "fmt" + "strings" "github.com/rs/zerolog" "github.com/rs/zerolog/log" @@ -56,19 +57,20 @@ 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") - } + for key, cnf := range config.VideoPipelines { + pipelineConf := cnf createPipeline := func() string { - screen := desktop.GetScreenSize() + if pipelineConf.GstPipeline != "" { + return strings.Replace(pipelineConf.GstPipeline, "{display}", config.Display, 1) + } + 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") + logger.Panic().Err(err). + Str("video_id", key). + Msg("unable to get video pipeline") } return fmt.Sprintf( @@ -78,11 +80,14 @@ func New(desktop types.DesktopManager, config *config.Capture) *CaptureManagerCt } // trigger function to catch evaluation errors at startup - _ = createPipeline() + pipeline := createPipeline() + logger.Info(). + Str("video_id", key). + Str("pipeline", pipeline). + Msg("syntax check for video stream pipeline passed") // append to videos - videos[key] = streamNew(codec, createPipeline) - videoIDs = append(videoIDs, key) + videos[key] = streamNew(config.VideoCodec, createPipeline) } return &CaptureManagerCtx{ @@ -106,7 +111,7 @@ func New(desktop types.DesktopManager, config *config.Capture) *CaptureManagerCt ) }), videos: videos, - videoIDs: videoIDs, + videoIDs: config.VideoIDs, } } diff --git a/internal/config/capture.go b/internal/config/capture.go index 1bb4b6f6..e73701d9 100644 --- a/internal/config/capture.go +++ b/internal/config/capture.go @@ -15,7 +15,9 @@ import ( type Capture struct { Display string - Video map[string] types.VideoConfig + VideoCodec codec.RTPCodec + VideoIDs []string + VideoPipelines map[string]types.VideoConfig AudioDevice string AudioCodec codec.RTPCodec @@ -49,6 +51,22 @@ func (Capture) Init(cmd *cobra.Command) error { 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.screencast.audio_bitrate", 128, "broadcast audio bitrate in KB/s") if err := viper.BindPFlag("capture.screencast.audio_bitrate", cmd.PersistentFlags().Lookup("capture.screencast.audio_bitrate")); err != nil { @@ -99,10 +117,42 @@ func (s *Capture) Set() { s.Display = os.Getenv("DISPLAY") // video - if err := viper.UnmarshalKey("capture.video", &s.Video, viper.DecodeHook( - utils.JsonStringAutoDecode(s.Video), + videoCodec := viper.GetString("capture.video.codec") + switch videoCodec { + case "vp8": + s.VideoCodec = codec.VP8() + case "vp9": + s.VideoCodec = codec.VP9() + case "h264": + s.VideoCodec = codec.H264() + default: + 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 settings") + 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": types.VideoConfig{ + GstPipeline: "ximagesrc display-name={display} show-pointer=false use-damage=false "+ + "! video/x-raw "+ + "! videoconvert "+ + "! queue "+ + "! vp8enc end-usage=cbr cpu-used=4 threads=4 deadline=1 keyframe-max-dist=25 "+ + "! appsink name=appsink", + }, + } + s.VideoIDs = []string{"main"} } // audio diff --git a/internal/types/capture.go b/internal/types/capture.go index b80ec2fc..ce095a33 100644 --- a/internal/types/capture.go +++ b/internal/types/capture.go @@ -3,6 +3,7 @@ package types import ( "math" "strings" + "context" "fmt" "github.com/pion/webrtc/v3/pkg/media" @@ -50,33 +51,17 @@ type CaptureManager interface { } type VideoConfig struct { - Codec string `mapstructure:"codec"` Width string `mapstructure:"width"` // expression Height string `mapstructure:"height"` // expression Fps string `mapstructure:"fps"` // expression + GstPrefix string `mapstructure:"gst_prefix"` // pipeline prefix, starts with ! 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") - } + GstSuffix string `mapstructure:"gst_suffix"` // pipeline suffix, starts with ! + GstPipeline string `mapstructure:"gst_pipeline"` // whole pipeline as a string } func (config *VideoConfig) GetPipeline(screen ScreenSize) (string, error) { - if config.GstPipeline != "" { - return config.GstPipeline, nil - } - values := map[string]interface{}{ "width": screen.Width, "height": screen.Height, @@ -92,34 +77,43 @@ func (config *VideoConfig) GetPipeline(screen ScreenSize) (string, error) { // get fps pipeline fpsPipeline := "! video/x-raw ! videoconvert ! queue" if config.Fps != "" { - var err error - val, err := gval.Evaluate(config.Fps, values, language...) + eval, err := gval.Full(language...).NewEvaluable(config.Fps) if err != nil { return "", err } - - if val != nil { - // TODO: To fraction. - fpsPipeline = fmt.Sprintf("! video/x-raw,framerate=%v ! videoconvert ! queue", val) + + val, err := eval.EvalFloat64(context.Background(), values) + if err != nil { + return "", err } + + fpsPipeline = fmt.Sprintf("! video/x-raw,framerate=%d/100 ! videoconvert ! queue", int(val*100)) } // get scale pipeline scalePipeline := "" if config.Width != "" && config.Height != "" { - w, err := gval.Evaluate(config.Width, values, language...) + eval, err := gval.Full(language...).NewEvaluable(config.Width) if err != nil { return "", err } - h, err := gval.Evaluate(config.Height, values, language...) + w, err := eval.EvalInt(context.Background(), values) if err != nil { return "", err } - if w != nil && h != nil { - scalePipeline = fmt.Sprintf("! videoscale ! video/x-raw,width=%v,height=%v ! queue", w, h) + eval, err = gval.Full(language...).NewEvaluable(config.Height) + if err != nil { + return "", err } + + h, err := eval.EvalInt(context.Background(), values) + if err != nil { + return "", err + } + + scalePipeline = fmt.Sprintf("! videoscale ! video/x-raw,width=%d,height=%d ! queue", w, h) } // get encoder pipeline @@ -141,5 +135,12 @@ func (config *VideoConfig) GetPipeline(screen ScreenSize) (string, error) { } } - return fmt.Sprintf("%s %s %s", fpsPipeline, scalePipeline, encPipeline), nil + // join strings with space + return strings.Join([]string{ + fpsPipeline, + scalePipeline, + config.GstPrefix, + encPipeline, + config.GstSuffix, + }[:]," "), nil }