diff --git a/server/cmd/serve.go b/server/cmd/serve.go index d709a523..13f5de23 100644 --- a/server/cmd/serve.go +++ b/server/cmd/serve.go @@ -87,6 +87,27 @@ func (c *serve) Init(cmd *cobra.Command) error { 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 } @@ -100,6 +121,13 @@ func (c *serve) PreRun(cmd *cobra.Command, args []string) { 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) { diff --git a/server/internal/config/capture.go b/server/internal/config/capture.go index 9f870dc5..700875e3 100644 --- a/server/internal/config/capture.go +++ b/server/internal/config/capture.go @@ -2,7 +2,9 @@ package config import ( "os" + "strings" + "github.com/pion/webrtc/v3" "github.com/rs/zerolog/log" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -12,6 +14,17 @@ import ( "github.com/demodesk/neko/pkg/utils" ) +// Legacy capture configuration +type HwEnc int + +// Legacy capture configuration +const ( + HwEncUnset HwEnc = iota + HwEncNone + HwEncVAAPI + HwEncNVENC +) + type Capture struct { Display string @@ -167,6 +180,132 @@ func (Capture) Init(cmd *cobra.Command) error { return nil } +func (Capture) InitV2(cmd *cobra.Command) error { + cmd.PersistentFlags().String("display", "", "XDisplay to capture") + if err := viper.BindPFlag("display", cmd.PersistentFlags().Lookup("display")); err != nil { + return err + } + + cmd.PersistentFlags().String("video_codec", "", "video codec to be used") + if err := viper.BindPFlag("video_codec", cmd.PersistentFlags().Lookup("video_codec")); err != nil { + return err + } + + // DEPRECATED: video codec + cmd.PersistentFlags().Bool("vp8", false, "DEPRECATED: use video_codec") + if err := viper.BindPFlag("vp8", cmd.PersistentFlags().Lookup("vp8")); err != nil { + return err + } + + // DEPRECATED: video codec + cmd.PersistentFlags().Bool("vp9", false, "DEPRECATED: use video_codec") + if err := viper.BindPFlag("vp9", cmd.PersistentFlags().Lookup("vp9")); err != nil { + return err + } + + // DEPRECATED: video codec + cmd.PersistentFlags().Bool("av1", false, "DEPRECATED: use video_codec") + if err := viper.BindPFlag("av1", cmd.PersistentFlags().Lookup("av1")); err != nil { + return err + } + + // DEPRECATED: video codec + cmd.PersistentFlags().Bool("h264", false, "DEPRECATED: use video_codec") + if err := viper.BindPFlag("h264", cmd.PersistentFlags().Lookup("h264")); err != nil { + return err + } + + cmd.PersistentFlags().String("hwenc", "", "use hardware accelerated encoding") + if err := viper.BindPFlag("hwenc", cmd.PersistentFlags().Lookup("hwenc")); err != nil { + return err + } + + cmd.PersistentFlags().Int("video_bitrate", 0, "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", 0, "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") + if err := viper.BindPFlag("video", cmd.PersistentFlags().Lookup("video")); err != nil { + return err + } + + // + // audio + // + + cmd.PersistentFlags().String("device", "", "audio device to capture") + if err := viper.BindPFlag("device", cmd.PersistentFlags().Lookup("device")); err != nil { + return err + } + + cmd.PersistentFlags().String("audio_codec", "", "audio codec to be used") + if err := viper.BindPFlag("audio_codec", cmd.PersistentFlags().Lookup("audio_codec")); err != nil { + return err + } + + // DEPRECATED: audio codec + cmd.PersistentFlags().Bool("opus", false, "DEPRECATED: use audio_codec") + if err := viper.BindPFlag("opus", cmd.PersistentFlags().Lookup("opus")); err != nil { + return err + } + + // DEPRECATED: audio codec + cmd.PersistentFlags().Bool("g722", false, "DEPRECATED: use audio_codec") + if err := viper.BindPFlag("g722", cmd.PersistentFlags().Lookup("g722")); err != nil { + return err + } + + // DEPRECATED: audio codec + cmd.PersistentFlags().Bool("pcmu", false, "DEPRECATED: use audio_codec") + if err := viper.BindPFlag("pcmu", cmd.PersistentFlags().Lookup("pcmu")); err != nil { + return err + } + + // DEPRECATED: audio codec + cmd.PersistentFlags().Bool("pcma", false, "DEPRECATED: use audio_codec") + if err := viper.BindPFlag("pcma", cmd.PersistentFlags().Lookup("pcma")); err != nil { + return err + } + // audio codecs + + cmd.PersistentFlags().Int("audio_bitrate", 0, "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") + if err := viper.BindPFlag("audio", cmd.PersistentFlags().Lookup("audio")); err != nil { + return err + } + + // + // broadcast + // + + cmd.PersistentFlags().String("broadcast_pipeline", "", "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") + if err := viper.BindPFlag("broadcast_url", cmd.PersistentFlags().Lookup("broadcast_url")); err != nil { + return err + } + + cmd.PersistentFlags().Bool("broadcast_autostart", false, "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 + } + + return nil +} + func (s *Capture) Set() { var ok bool @@ -251,3 +390,135 @@ func (s *Capture) Set() { 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 'DISPLAY' instead") + } + + 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_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_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_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_CAPTURE_VIDEO_CODEC=av1' instead") + } + + 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") + } + } + + 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 + // + + 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") + } + + 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_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_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_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_CAPTURE_AUDIO_CODEC=pcma' instead") + } + + 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 + // + + 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") + } +} diff --git a/server/internal/config/capture_pipeline.go b/server/internal/config/capture_pipeline.go new file mode 100644 index 00000000..35469cd6 --- /dev/null +++ b/server/internal/config/capture_pipeline.go @@ -0,0 +1,235 @@ +// Legacy pipeline configuration for gstreamer. +package config + +import ( + "fmt" + "strings" + + "github.com/demodesk/neko/pkg/gst" + "github.com/demodesk/neko/pkg/types/codec" +) + +/* + apt-get install \ + libgstreamer1.0-0 \ + gstreamer1.0-plugins-base \ + gstreamer1.0-plugins-good \ + gstreamer1.0-plugins-bad \ + gstreamer1.0-plugins-ugly\ + gstreamer1.0-libav \ + gstreamer1.0-doc \ + gstreamer1.0-tools \ + gstreamer1.0-x \ + gstreamer1.0-alsa \ + gstreamer1.0-pulseaudio + + gst-inspect-1.0 --version + gst-inspect-1.0 plugin + gst-launch-1.0 ximagesrc show-pointer=true use-damage=false ! video/x-raw,framerate=30/1 ! videoconvert ! queue ! vp8enc error-resilient=partitions keyframe-max-dist=10 auto-alt-ref=true cpu-used=5 deadline=1 ! autovideosink + gst-launch-1.0 pulsesrc ! audioconvert ! opusenc ! autoaudiosink +*/ + +const ( + videoSrc = "ximagesrc display-name=%s show-pointer=true use-damage=false ! video/x-raw,framerate=%d/1 ! videoconvert ! queue ! " + audioSrc = "pulsesrc device=%s ! audio/x-raw,channels=2 ! audioconvert ! " +) + +func NewBroadcastPipeline(device string, display string, pipelineSrc string, url string) string { + video := fmt.Sprintf(videoSrc, display, 25) + audio := fmt.Sprintf(audioSrc, device) + + var pipelineStr string + if pipelineSrc != "" { + // replace RTMP url + pipelineStr = strings.Replace(pipelineSrc, "{url}", url, -1) + // replace audio device + pipelineStr = strings.Replace(pipelineStr, "{device}", device, -1) + // replace display + pipelineStr = strings.Replace(pipelineStr, "{display}", display, -1) + } else { + pipelineStr = fmt.Sprintf("flvmux name=mux ! rtmpsink location='%s live=1' %s audio/x-raw,channels=2 ! audioconvert ! voaacenc ! mux. %s x264enc bframes=0 key-int-max=60 byte-stream=true tune=zerolatency speed-preset=veryfast ! mux.", url, audio, video) + } + + return pipelineStr +} + +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 + if pipelineSrc != "" { + pipelineStr = fmt.Sprintf(pipelineSrc+pipelineStr, display) + return pipelineStr, nil + } + + // use default fps if not set + if fps == 0 { + fps = 25 + } + + switch rtpCodec.Name { + case codec.VP8().Name: + if hwenc == HwEncVAAPI { + if err := gst.CheckPlugins([]string{"ximagesrc", "vaapi"}); err != nil { + return "", err + } + // vp8 encode is missing from gstreamer.freedesktop.org/documentation + // note that it was removed from some recent intel CPUs: https://trac.ffmpeg.org/wiki/Hardware/QuickSync + // https://gstreamer.freedesktop.org/data/doc/gstreamer/head/gstreamer-vaapi-plugins/html/gstreamer-vaapi-plugins-vaapivp8enc.html + pipelineStr = fmt.Sprintf(videoSrc+"video/x-raw,format=NV12 ! vaapivp8enc rate-control=vbr bitrate=%d keyframe-period=180"+pipelineStr, display, fps, bitrate) + } else { + // https://gstreamer.freedesktop.org/documentation/vpx/vp8enc.html?gi-language=c + // gstreamer1.0-plugins-good + // vp8enc error-resilient=partitions keyframe-max-dist=10 auto-alt-ref=true cpu-used=5 deadline=1 + if err := gst.CheckPlugins([]string{"ximagesrc", "vpx"}); err != nil { + return "", err + } + + pipelineStr = strings.Join([]string{ + fmt.Sprintf(videoSrc, display, fps), + "vp8enc", + fmt.Sprintf("target-bitrate=%d", bitrate*650), + "cpu-used=4", + "end-usage=cbr", + "threads=4", + "deadline=1", + "undershoot=95", + fmt.Sprintf("buffer-size=%d", bitrate*4), + fmt.Sprintf("buffer-initial-size=%d", bitrate*2), + fmt.Sprintf("buffer-optimal-size=%d", bitrate*3), + "keyframe-max-dist=25", + "min-quantizer=4", + "max-quantizer=20", + pipelineStr, + }, " ") + } + case codec.VP9().Name: + // https://gstreamer.freedesktop.org/documentation/vpx/vp9enc.html?gi-language=c + // gstreamer1.0-plugins-good + // vp9enc + if err := gst.CheckPlugins([]string{"ximagesrc", "vpx"}); err != nil { + return "", err + } + + pipelineStr = fmt.Sprintf(videoSrc+"vp9enc target-bitrate=%d cpu-used=-5 threads=4 deadline=1 keyframe-max-dist=30 auto-alt-ref=true"+pipelineStr, display, fps, bitrate*1000) + case codec.AV1().Name: + // https://gstreamer.freedesktop.org/documentation/aom/av1enc.html?gi-language=c + // gstreamer1.0-plugins-bad + // av1enc usage-profile=1 + // TODO: check for plugin. + if err := gst.CheckPlugins([]string{"ximagesrc", "vpx"}); err != nil { + return "", err + } + + pipelineStr = strings.Join([]string{ + fmt.Sprintf(videoSrc, display, fps), + "av1enc", + fmt.Sprintf("target-bitrate=%d", bitrate*650), + "cpu-used=4", + "end-usage=cbr", + // "usage-profile=realtime", + "undershoot=95", + "keyframe-max-dist=25", + "min-quantizer=4", + "max-quantizer=20", + pipelineStr, + }, " ") + case codec.H264().Name: + if err := gst.CheckPlugins([]string{"ximagesrc"}); err != nil { + return "", err + } + + vbvbuf := uint(1000) + if bitrate > 1000 { + vbvbuf = bitrate + } + + 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 == HwEncNVENC { + if err := gst.CheckPlugins([]string{"nvcodec"}); err != nil { + return "", err + } + + pipelineStr = fmt.Sprintf(videoSrc+"video/x-raw,format=NV12 ! nvh264enc name=encoder preset=2 gop-size=25 spatial-aq=true temporal-aq=true bitrate=%d vbv-buffer-size=%d rc-mode=6 ! h264parse config-interval=-1 ! video/x-h264,stream-format=byte-stream,profile=constrained-baseline"+pipelineStr, display, fps, bitrate, vbvbuf) + } else { + // https://gstreamer.freedesktop.org/documentation/openh264/openh264enc.html?gi-language=c#openh264enc + // gstreamer1.0-plugins-bad + // openh264enc multi-thread=4 complexity=high bitrate=3072000 max-bitrate=4096000 + if err := gst.CheckPlugins([]string{"openh264"}); err == nil { + pipelineStr = fmt.Sprintf(videoSrc+"openh264enc multi-thread=4 complexity=high bitrate=%d max-bitrate=%d ! video/x-h264,stream-format=byte-stream,profile=constrained-baseline"+pipelineStr, display, fps, bitrate*1000, (bitrate+1024)*1000) + break + } + + // https://gstreamer.freedesktop.org/documentation/x264/index.html?gi-language=c + // gstreamer1.0-plugins-ugly + // video/x-raw,format=I420 ! x264enc bframes=0 key-int-max=60 byte-stream=true tune=zerolatency speed-preset=veryfast ! video/x-h264,stream-format=byte-stream,profile=constrained-baseline + if err := gst.CheckPlugins([]string{"x264"}); err != nil { + return "", err + } + + pipelineStr = fmt.Sprintf(videoSrc+"video/x-raw,format=NV12 ! x264enc threads=4 bitrate=%d key-int-max=60 vbv-buf-capacity=%d byte-stream=true tune=zerolatency speed-preset=veryfast ! video/x-h264,stream-format=byte-stream,profile=constrained-baseline"+pipelineStr, display, fps, bitrate, vbvbuf) + } + default: + return "", fmt.Errorf("unknown codec %s", rtpCodec.Name) + } + + return pipelineStr, nil +} + +func NewAudioPipeline(rtpCodec codec.RTPCodec, device string, pipelineSrc string, bitrate uint) (string, error) { + pipelineStr := " ! appsink name=appsinkaudio" + + // if using custom pipeline + if pipelineSrc != "" { + pipelineStr = fmt.Sprintf(pipelineSrc+pipelineStr, device) + return pipelineStr, nil + } + + switch rtpCodec.Name { + case codec.Opus().Name: + // https://gstreamer.freedesktop.org/documentation/opus/opusenc.html + // gstreamer1.0-plugins-base + // opusenc + if err := gst.CheckPlugins([]string{"pulseaudio", "opus"}); err != nil { + return "", err + } + + pipelineStr = fmt.Sprintf(audioSrc+"opusenc inband-fec=true bitrate=%d"+pipelineStr, device, bitrate*1000) + case codec.G722().Name: + // https://gstreamer.freedesktop.org/documentation/libav/avenc_g722.html?gi-language=c + // gstreamer1.0-libav + // avenc_g722 + if err := gst.CheckPlugins([]string{"pulseaudio", "libav"}); err != nil { + return "", err + } + + pipelineStr = fmt.Sprintf(audioSrc+"avenc_g722 bitrate=%d"+pipelineStr, device, bitrate*1000) + case codec.PCMU().Name: + // https://gstreamer.freedesktop.org/documentation/mulaw/mulawenc.html?gi-language=c + // gstreamer1.0-plugins-good + // audio/x-raw, rate=8000 ! mulawenc + if err := gst.CheckPlugins([]string{"pulseaudio", "mulaw"}); err != nil { + return "", err + } + + pipelineStr = fmt.Sprintf(audioSrc+"audio/x-raw, rate=8000 ! mulawenc"+pipelineStr, device) + case codec.PCMA().Name: + // https://gstreamer.freedesktop.org/documentation/alaw/alawenc.html?gi-language=c + // gstreamer1.0-plugins-good + // audio/x-raw, rate=8000 ! alawenc + if err := gst.CheckPlugins([]string{"pulseaudio", "alaw"}); err != nil { + return "", err + } + + pipelineStr = fmt.Sprintf(audioSrc+"audio/x-raw, rate=8000 ! alawenc"+pipelineStr, device) + default: + return "", fmt.Errorf("unknown codec %s", rtpCodec.Name) + } + + return pipelineStr, nil +} diff --git a/server/internal/config/desktop.go b/server/internal/config/desktop.go index a021ddfc..5ddf5d4d 100644 --- a/server/internal/config/desktop.go +++ b/server/internal/config/desktop.go @@ -5,6 +5,7 @@ import ( "regexp" "strconv" + "github.com/rs/zerolog/log" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -58,6 +59,15 @@ func (Desktop) Init(cmd *cobra.Command) error { return nil } +func (Desktop) InitV2(cmd *cobra.Command) error { + cmd.PersistentFlags().String("screen", "", "default screen resolution and framerate") + if err := viper.BindPFlag("screen", cmd.PersistentFlags().Lookup("screen")); err != nil { + return err + } + + return nil +} + func (s *Desktop) Set() { // Display is provided by env variable s.Display = os.Getenv("DISPLAY") @@ -89,3 +99,23 @@ func (s *Desktop) Set() { 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") + } +} diff --git a/server/internal/config/member.go b/server/internal/config/member.go index 1cccde36..dd15ff66 100644 --- a/server/internal/config/member.go +++ b/server/internal/config/member.go @@ -68,6 +68,20 @@ func (Member) Init(cmd *cobra.Command) error { return nil } +func (Member) InitV2(cmd *cobra.Command) error { + cmd.PersistentFlags().String("password", "", "password for connecting to stream") + if err := viper.BindPFlag("password", cmd.PersistentFlags().Lookup("password")); err != nil { + return err + } + + cmd.PersistentFlags().String("password_admin", "", "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") + } +} diff --git a/server/internal/config/root.go b/server/internal/config/root.go index e9870eda..97f42581 100644 --- a/server/internal/config/root.go +++ b/server/internal/config/root.go @@ -2,6 +2,8 @@ package config import ( "os" + "path/filepath" + "runtime" "github.com/rs/zerolog" "github.com/rs/zerolog/log" @@ -59,6 +61,15 @@ func (Root) Init(cmd *cobra.Command) error { return nil } +func (Root) InitV2(cmd *cobra.Command) error { + cmd.PersistentFlags().BoolP("logs", "l", false, "save logs to file") + if err := viper.BindPFlag("logs", cmd.PersistentFlags().Lookup("logs")); err != nil { + return err + } + + return nil +} + func (s *Root) Set() { s.Config = viper.GetString("config") @@ -95,3 +106,18 @@ func (s *Root) Set() { 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") + } +} diff --git a/server/internal/config/server.go b/server/internal/config/server.go index 9ed30859..c8e7cd1e 100644 --- a/server/internal/config/server.go +++ b/server/internal/config/server.go @@ -3,6 +3,7 @@ package config import ( "path" + "github.com/rs/zerolog/log" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -70,6 +71,45 @@ func (Server) Init(cmd *cobra.Command) error { return nil } +func (Server) InitV2(cmd *cobra.Command) error { + cmd.PersistentFlags().String("bind", "", "address/port/socket to serve neko") + if err := viper.BindPFlag("bind", cmd.PersistentFlags().Lookup("bind")); err != nil { + return err + } + + cmd.PersistentFlags().String("cert", "", "path to the SSL cert used to secure the neko server") + if err := viper.BindPFlag("cert", cmd.PersistentFlags().Lookup("cert")); err != nil { + return err + } + + cmd.PersistentFlags().String("key", "", "path to the SSL key used to secure the neko server") + if err := viper.BindPFlag("key", cmd.PersistentFlags().Lookup("key")); err != nil { + return err + } + + cmd.PersistentFlags().Bool("proxy", false, "enable reverse proxy mode") + if err := viper.BindPFlag("proxy", cmd.PersistentFlags().Lookup("proxy")); err != nil { + return err + } + + cmd.PersistentFlags().String("static", "", "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") + 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") + if err := viper.BindPFlag("cors", cmd.PersistentFlags().Lookup("cors")); err != nil { + return err + } + + return nil +} + func (s *Server) Set() { s.Cert = viper.GetString("server.cert") s.Key = viper.GetString("server.key") @@ -87,6 +127,41 @@ func (s *Server) Set() { } } +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 } diff --git a/server/internal/config/session.go b/server/internal/config/session.go index 443c02d6..bbeca396 100644 --- a/server/internal/config/session.go +++ b/server/internal/config/session.go @@ -3,6 +3,7 @@ package config import ( "time" + "github.com/rs/zerolog/log" "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -95,6 +96,25 @@ func (Session) Init(cmd *cobra.Command) error { return nil } +func (Session) InitV2(cmd *cobra.Command) error { + 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 + } + + 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 *Session) Set() { s.File = viper.GetString("session.file") @@ -112,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") + } +} diff --git a/server/internal/config/webrtc.go b/server/internal/config/webrtc.go index f3023007..d0f886c0 100644 --- a/server/internal/config/webrtc.go +++ b/server/internal/config/webrtc.go @@ -1,6 +1,7 @@ package config import ( + "encoding/json" "strconv" "strings" "time" @@ -166,6 +167,50 @@ func (WebRTC) Init(cmd *cobra.Command) error { return nil } +func (WebRTC) InitV2(cmd *cobra.Command) error { + cmd.PersistentFlags().String("epr", "", "limits the pool of ephemeral ports that ICE UDP connections can allocate from") + if err := viper.BindPFlag("epr", cmd.PersistentFlags().Lookup("epr")); err != nil { + return err + } + + cmd.PersistentFlags().StringSlice("nat1to1", []string{}, "sets a list of external IP addresses of 1:1 (D)NAT and a candidate type for which the external IP address is used") + if err := viper.BindPFlag("nat1to1", cmd.PersistentFlags().Lookup("nat1to1")); err != nil { + return err + } + + cmd.PersistentFlags().Int("tcpmux", 0, "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") + if err := viper.BindPFlag("udpmux", cmd.PersistentFlags().Lookup("udpmux")); err != nil { + return err + } + + cmd.PersistentFlags().String("ipfetch", "", "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") + if err := viper.BindPFlag("icelite", cmd.PersistentFlags().Lookup("icelite")); err != nil { + return err + } + + cmd.PersistentFlags().StringSlice("iceserver", []string{}, "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") + if err := viper.BindPFlag("iceservers", cmd.PersistentFlags().Lookup("iceservers")); err != nil { + return err + } + + return nil +} + func (s *WebRTC) Set() { s.ICELite = viper.GetBool("webrtc.icelite") s.ICETrickle = viper.GetBool("webrtc.icetrickle") @@ -271,3 +316,84 @@ func (s *WebRTC) Set() { 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") + } +} diff --git a/server/internal/plugins/filetransfer/config.go b/server/internal/plugins/filetransfer/config.go index 593d31fb..04ed8109 100644 --- a/server/internal/plugins/filetransfer/config.go +++ b/server/internal/plugins/filetransfer/config.go @@ -30,6 +30,18 @@ func (Config) Init(cmd *cobra.Command) error { return err } + // v2 config + + 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", "", "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 } @@ -38,4 +50,14 @@ func (s *Config) Set() { rootDir := viper.GetString("filetransfer.dir") s.RootDir = filepath.Clean(rootDir) s.RefreshInterval = viper.GetDuration("filetransfer.refresh_interval") + + // v2 config + + if viper.IsSet("file_transfer_enabled") { + s.Enabled = viper.GetBool("file_transfer_enabled") + } + if viper.IsSet("file_transfer_path") { + rootDir = viper.GetString("file_transfer_path") + s.RootDir = filepath.Clean(rootDir) + } }