From 2b8ced9c59acd83992678fa3e965ce4a41651b7f Mon Sep 17 00:00:00 2001 From: Rob van Oostenrijk Date: Mon, 6 May 2024 08:06:08 +0400 Subject: [PATCH 001/166] Update build.yml --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f0293f3e..8d332487 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -102,14 +102,14 @@ jobs: env: { GOOS: freebsd, GOARCH: amd64 } run: go build -ldflags "-s -w" -trimpath - name: Upload go2rtc_freebsd_amd64 - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: { name: go2rtc_freebsd_amd64, path: go2rtc } - name: Build go2rtc_freebsd_arm64 env: { GOOS: freebsd, GOARCH: arm64 } run: go build -ldflags "-s -w" -trimpath - name: Upload go2rtc_freebsd_arm64 - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: { name: go2rtc_freebsd_arm64, path: go2rtc } docker-master: From ea17b420d68d470395ee3e5d98f3e189b098d9d2 Mon Sep 17 00:00:00 2001 From: Alex X Date: Tue, 28 May 2024 21:36:12 +0300 Subject: [PATCH 002/166] Fix two-way audio for webrtc client --- pkg/webrtc/consumer.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pkg/webrtc/consumer.go b/pkg/webrtc/consumer.go index 9d96ef59..3bcaf49a 100644 --- a/pkg/webrtc/consumer.go +++ b/pkg/webrtc/consumer.go @@ -77,7 +77,13 @@ func (c *Conn) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiv sender.Handler = pcm.RepackG711(false, sender.Handler) } - sender.Bind(track) + // TODO: rewrite this dirty logic + // maybe not best solution, but ActiveProducer connected before AddTrack + if c.Mode != core.ModeActiveProducer { + sender.Bind(track) + } else { + sender.HandleRTP(track) + } c.senders = append(c.senders, sender) return nil From a9e7a73cc8e0a25d21226f00f606703feb83b932 Mon Sep 17 00:00:00 2001 From: Alex X Date: Tue, 28 May 2024 14:01:42 +0300 Subject: [PATCH 003/166] Add video bitrate setting for HomeKit source --- internal/homekit/homekit.go | 32 ++++++++++++++++++++++++++++++-- pkg/hap/camera/stream.go | 13 ++++++++++--- pkg/homekit/client.go | 4 +++- 3 files changed, 43 insertions(+), 6 deletions(-) diff --git a/internal/homekit/homekit.go b/internal/homekit/homekit.go index bfe3e971..743aeab9 100644 --- a/internal/homekit/homekit.go +++ b/internal/homekit/homekit.go @@ -133,12 +133,19 @@ func Init() { var log zerolog.Logger var servers map[string]*server -func streamHandler(url string) (core.Producer, error) { +func streamHandler(rawURL string) (core.Producer, error) { if srtp.Server == nil { return nil, errors.New("homekit: can't work without SRTP server") } - return homekit.Dial(url, srtp.Server) + rawURL, rawQuery, _ := strings.Cut(rawURL, "#") + client, err := homekit.Dial(rawURL, srtp.Server) + if client != nil && rawQuery != "" { + query := streams.ParseQuery(rawQuery) + client.Bitrate = parseBitrate(query.Get("bitrate")) + } + + return client, err } func hapPairSetup(w http.ResponseWriter, r *http.Request) { @@ -199,3 +206,24 @@ func findHomeKitURL(stream *streams.Stream) string { return "" } + +func parseBitrate(s string) int { + n := len(s) + if n == 0 { + return 0 + } + + var k int + switch n--; s[n] { + case 'K': + k = 1024 + s = s[:n] + case 'M': + k = 1024 * 1024 + s = s[:n] + default: + k = 1 + } + + return k * core.Atoi(s) +} diff --git a/pkg/hap/camera/stream.go b/pkg/hap/camera/stream.go index b2ef0d9f..23d53c39 100644 --- a/pkg/hap/camera/stream.go +++ b/pkg/hap/camera/stream.go @@ -15,7 +15,8 @@ type Stream struct { } func NewStream( - client *hap.Client, videoCodec *VideoCodec, audioCodec *AudioCodec, videoSession, audioSession *srtp.Session, + client *hap.Client, videoCodec *VideoCodec, audioCodec *AudioCodec, + videoSession, audioSession *srtp.Session, bitrate int, ) (*Stream, error) { stream := &Stream{ id: core.RandString(16, 0), @@ -30,11 +31,17 @@ func NewStream( return nil, err } + if bitrate != 0 { + bitrate /= 1024 // convert bps to kbps + } else { + bitrate = 4096 // default kbps for general FullHD camera + } + videoCodec.RTPParams = []RTPParams{ { PayloadType: 99, SSRC: videoSession.Local.SSRC, - MaxBitrate: 299, + MaxBitrate: uint16(bitrate), // iPhone query 299Kbps, iPad/AppleTV query 802Kbps RTCPInterval: 0.5, MaxMTU: []uint16{1378}, }, @@ -43,7 +50,7 @@ func NewStream( { PayloadType: 110, SSRC: audioSession.Local.SSRC, - MaxBitrate: 24, + MaxBitrate: 24, // any iDevice query 24Kbps (this is OK for 16KHz and 1 channel) RTCPInterval: 5, ComfortNoisePayloadType: []uint8{13}, diff --git a/pkg/homekit/client.go b/pkg/homekit/client.go index c61acea6..133499d3 100644 --- a/pkg/homekit/client.go +++ b/pkg/homekit/client.go @@ -28,6 +28,8 @@ type Client struct { audioSession *srtp.Session stream *camera.Stream + + Bitrate int // in bits/s } func Dial(rawURL string, server *srtp.Server) (*Client, error) { @@ -132,7 +134,7 @@ func (c *Client) Start() error { c.audioSession = &srtp.Session{Local: c.srtpEndpoint()} var err error - c.stream, err = camera.NewStream(c.hap, videoCodec, audioCodec, c.videoSession, c.audioSession) + c.stream, err = camera.NewStream(c.hap, videoCodec, audioCodec, c.videoSession, c.audioSession, c.Bitrate) if err != nil { return err } From 2ab1d9d774265c050383382fbc6187d6b2789ad6 Mon Sep 17 00:00:00 2001 From: Alex X Date: Wed, 29 May 2024 17:32:11 +0300 Subject: [PATCH 004/166] Add handling if mp4 client drops connection --- internal/mp4/mp4.go | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/internal/mp4/mp4.go b/internal/mp4/mp4.go index 78708a35..2f59ba04 100644 --- a/internal/mp4/mp4.go +++ b/internal/mp4/mp4.go @@ -1,6 +1,7 @@ package mp4 import ( + "context" "net/http" "strconv" "strings" @@ -127,20 +128,20 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) { header.Set("Content-Disposition", `attachment; filename="`+filename+`"`) } - var duration *time.Timer - if s := query.Get("duration"); s != "" { - if i, _ := strconv.Atoi(s); i > 0 { - duration = time.AfterFunc(time.Second*time.Duration(i), func() { - _ = cons.Stop() - }) - } + ctx := r.Context() // handle when the client drops the connection + + if i := core.Atoi(query.Get("duration")); i > 0 { + timeout := time.Second * time.Duration(i) + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, timeout) + defer cancel() } + go func() { + <-ctx.Done() + _ = cons.Stop() + stream.RemoveConsumer(cons) + }() + _, _ = cons.WriteTo(w) - - stream.RemoveConsumer(cons) - - if duration != nil { - duration.Stop() - } } From aa86c1ec25c63af9f7e7250635b3f73e2d9bcb64 Mon Sep 17 00:00:00 2001 From: Sergey Krashevich Date: Thu, 30 May 2024 11:27:29 +0300 Subject: [PATCH 005/166] ci(workflow): add GitHub Container Registry login and update image paths --- .github/workflows/build.yml | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 188727d6..cec5f4a3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -123,7 +123,9 @@ jobs: id: meta uses: docker/metadata-action@v5 with: - images: ${{ github.repository }} + images: | + ${{ github.repository }} + ghcr.io/${{ github.repository }} tags: | type=ref,event=branch type=semver,pattern={{version}},enable=false @@ -142,6 +144,13 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Build and push uses: docker/build-push-action@v5 with: @@ -168,7 +177,9 @@ jobs: id: meta-hw uses: docker/metadata-action@v5 with: - images: ${{ github.repository }} + images: | + ${{ github.repository }} + ghcr.io/${{ github.repository }} flavor: | suffix=-hardware,onlatest=true latest=auto @@ -189,6 +200,14 @@ jobs: with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Login to GitHub Container Registry + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push uses: docker/build-push-action@v5 From 79245eeff4ed093d2ed734af7a14b3f8f450ad41 Mon Sep 17 00:00:00 2001 From: Sergey Krashevich Date: Thu, 30 May 2024 11:48:15 +0300 Subject: [PATCH 006/166] fix(ci): skip GitHub Container Registry login on pull requests --- .github/workflows/build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cec5f4a3..f32c2333 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -145,6 +145,7 @@ jobs: password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GitHub Container Registry + if: github.event_name != 'pull_request' uses: docker/login-action@v3 with: registry: ghcr.io From df1d44d24eb335c4b6190feca6fc073e634a16ca Mon Sep 17 00:00:00 2001 From: Sergey Krashevich Date: Thu, 30 May 2024 17:12:56 +0300 Subject: [PATCH 007/166] chore(deps): update Go version to 1.22 across project files --- .github/workflows/build.yml | 2 +- .github/workflows/test.yml | 2 +- Dockerfile | 2 +- go.mod | 2 +- hardware.Dockerfile | 2 +- pkg/homekit/consumer.go | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 188727d6..4811b59d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,7 +19,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v5 - with: { go-version: '1.21' } + with: { go-version: '1.22' } - name: Build go2rtc_win64 env: { GOOS: windows, GOARCH: amd64 } diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b23faf53..dc47bdb5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,7 +26,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v5 with: - go-version: '1.21' + go-version: '1.22' - name: Build Go binary run: go build -ldflags "-s -w" -trimpath -o ./go2rtc diff --git a/Dockerfile b/Dockerfile index 46b85d30..b3888820 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ # 0. Prepare images ARG PYTHON_VERSION="3.11" -ARG GO_VERSION="1.21" +ARG GO_VERSION="1.22" ARG NGROK_VERSION="3" FROM python:${PYTHON_VERSION}-alpine AS base diff --git a/go.mod b/go.mod index b1ba4b4c..0d5d67c9 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/AlexxIT/go2rtc -go 1.21 +go 1.22 require ( github.com/asticode/go-astits v1.13.0 diff --git a/hardware.Dockerfile b/hardware.Dockerfile index 0aa85374..2254f9be 100644 --- a/hardware.Dockerfile +++ b/hardware.Dockerfile @@ -4,7 +4,7 @@ # only debian 13 (trixie) has latest ffmpeg # https://packages.debian.org/trixie/ffmpeg ARG DEBIAN_VERSION="trixie-slim" -ARG GO_VERSION="1.21-bookworm" +ARG GO_VERSION="1.22-bookworm" ARG NGROK_VERSION="3" FROM debian:${DEBIAN_VERSION} AS base diff --git a/pkg/homekit/consumer.go b/pkg/homekit/consumer.go index 1e04fedf..05ea2427 100644 --- a/pkg/homekit/consumer.go +++ b/pkg/homekit/consumer.go @@ -3,7 +3,7 @@ package homekit import ( "fmt" "io" - "math/rand" + "math/rand/v2" "net" "time" From 756be9801e362379752799a1fda9171547d38f20 Mon Sep 17 00:00:00 2001 From: Alex X Date: Sat, 1 Jun 2024 19:18:26 +0300 Subject: [PATCH 008/166] Code refactoring for app module --- internal/app/app.go | 155 ++++----------------------- internal/app/config.go | 109 +++++++++++++++++++ internal/app/log.go | 58 ++++++---- internal/app/migrate.go | 35 ------ internal/dvrip/dvrip.go | 6 +- internal/ffmpeg/ffmpeg.go | 5 + internal/ffmpeg/hardware/hardware.go | 3 - internal/ffmpeg/version.go | 1 - internal/mjpeg/init.go | 7 +- internal/mpegts/aac.go | 2 - internal/mpegts/mpegts.go | 2 - main.go | 2 + pkg/opus/{opus.go => .opus.go} | 1 - pkg/roborock/client.go | 5 +- pkg/roborock/iot/client.go | 8 +- 15 files changed, 182 insertions(+), 217 deletions(-) create mode 100644 internal/app/config.go delete mode 100644 internal/app/migrate.go rename pkg/opus/{opus.go => .opus.go} (97%) diff --git a/internal/app/app.go b/internal/app/app.go index 9dec2848..9331f041 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -1,29 +1,20 @@ package app import ( - "errors" "flag" "fmt" "os" "os/exec" - "path/filepath" "runtime" "runtime/debug" - "strings" - - "github.com/AlexxIT/go2rtc/pkg/shell" - "github.com/AlexxIT/go2rtc/pkg/yaml" - "github.com/rs/zerolog" - "github.com/rs/zerolog/log" ) -var Version = "1.9.2" -var UserAgent = "go2rtc/" + Version - -var ConfigPath string -var Info = map[string]any{ - "version": Version, -} +var ( + Version string + UserAgent string + ConfigPath string + Info = make(map[string]any) +) const usage = `Usage of go2rtc: @@ -33,12 +24,12 @@ const usage = `Usage of go2rtc: ` func Init() { - var confs Config + var config flagConfig var daemon bool var version bool - flag.Var(&confs, "config", "") - flag.Var(&confs, "c", "") + flag.Var(&config, "config", "") + flag.Var(&config, "c", "") flag.BoolVar(&daemon, "daemon", false, "") flag.BoolVar(&daemon, "d", false, "") flag.BoolVar(&version, "version", false, "") @@ -69,118 +60,30 @@ func Init() { // Re-run the program in background and exit cmd := exec.Command(os.Args[0], args...) if err := cmd.Start(); err != nil { - log.Fatal().Err(err).Send() + fmt.Println(err) + os.Exit(1) } fmt.Println("Running in daemon mode with PID:", cmd.Process.Pid) os.Exit(0) } - if confs == nil { - confs = []string{"go2rtc.yaml"} - } - - for _, conf := range confs { - if len(conf) == 0 { - continue - } - if conf[0] == '{' { - // config as raw YAML or JSON - configs = append(configs, []byte(conf)) - } else if data := parseConfString(conf); data != nil { - configs = append(configs, data) - } else { - // config as file - if ConfigPath == "" { - ConfigPath = conf - } - - if data, _ = os.ReadFile(conf); data == nil { - continue - } - - data = []byte(shell.ReplaceEnvVars(string(data))) - configs = append(configs, data) - } - } - - if ConfigPath != "" { - if !filepath.IsAbs(ConfigPath) { - if cwd, err := os.Getwd(); err == nil { - ConfigPath = filepath.Join(cwd, ConfigPath) - } - } - Info["config_path"] = ConfigPath - } + UserAgent = "go2rtc/" + Version + Info["version"] = Version Info["revision"] = revision - var cfg struct { - Mod map[string]string `yaml:"log"` - } - - cfg.Mod = map[string]string{ - "format": "", // useless, but anyway - "level": "info", - "output": "stdout", // TODO: change to stderr someday - "time": zerolog.TimeFormatUnixMs, - } - - LoadConfig(&cfg) - - log.Logger = NewLogger(cfg.Mod) - - modules = cfg.Mod + initConfig(config) + initLogger() platform := fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH) - log.Info().Str("version", Version).Str("platform", platform).Str("revision", revision).Msg("go2rtc") - log.Debug().Str("version", runtime.Version()).Str("vcs.time", vcsTime).Msg("build") + Logger.Info().Str("version", Version).Str("platform", platform).Str("revision", revision).Msg("go2rtc") + Logger.Debug().Str("version", runtime.Version()).Str("vcs.time", vcsTime).Msg("build") if ConfigPath != "" { - log.Info().Str("path", ConfigPath).Msg("config") - } - - migrateStore() -} - -func LoadConfig(v any) { - for _, data := range configs { - if err := yaml.Unmarshal(data, v); err != nil { - log.Warn().Err(err).Msg("[app] read config") - } + Logger.Info().Str("path", ConfigPath).Msg("config") } } -func PatchConfig(key string, value any, path ...string) error { - if ConfigPath == "" { - return errors.New("config file disabled") - } - - // empty config is OK - b, _ := os.ReadFile(ConfigPath) - - b, err := yaml.Patch(b, key, value, path...) - if err != nil { - return err - } - - return os.WriteFile(ConfigPath, b, 0644) -} - -// internal - -type Config []string - -func (c *Config) String() string { - return strings.Join(*c, " ") -} - -func (c *Config) Set(value string) error { - *c = append(*c, value) - return nil -} - -var configs [][]byte - func readRevisionTime() (revision, vcsTime string) { if info, ok := debug.ReadBuildInfo(); ok { for _, setting := range info.Settings { @@ -202,25 +105,3 @@ func readRevisionTime() (revision, vcsTime string) { } return } - -func parseConfString(s string) []byte { - i := strings.IndexByte(s, '=') - if i < 0 { - return nil - } - - items := strings.Split(s[:i], ".") - if len(items) < 2 { - return nil - } - - // `log.level=trace` => `{log: {level: trace}}` - var pre string - var suf = s[i+1:] - for _, item := range items { - pre += "{" + item + ": " - suf += "}" - } - - return []byte(pre + suf) -} diff --git a/internal/app/config.go b/internal/app/config.go new file mode 100644 index 00000000..8ae6d460 --- /dev/null +++ b/internal/app/config.go @@ -0,0 +1,109 @@ +package app + +import ( + "errors" + "os" + "path/filepath" + "strings" + + "github.com/AlexxIT/go2rtc/pkg/shell" + "github.com/AlexxIT/go2rtc/pkg/yaml" +) + +func LoadConfig(v any) { + for _, data := range configs { + if err := yaml.Unmarshal(data, v); err != nil { + Logger.Warn().Err(err).Send() + } + } +} + +func PatchConfig(key string, value any, path ...string) error { + if ConfigPath == "" { + return errors.New("config file disabled") + } + + // empty config is OK + b, _ := os.ReadFile(ConfigPath) + + b, err := yaml.Patch(b, key, value, path...) + if err != nil { + return err + } + + return os.WriteFile(ConfigPath, b, 0644) +} + +type flagConfig []string + +func (c *flagConfig) String() string { + return strings.Join(*c, " ") +} + +func (c *flagConfig) Set(value string) error { + *c = append(*c, value) + return nil +} + +var configs [][]byte + +func initConfig(confs flagConfig) { + if confs == nil { + confs = []string{"go2rtc.yaml"} + } + + for _, conf := range confs { + if len(conf) == 0 { + continue + } + if conf[0] == '{' { + // config as raw YAML or JSON + configs = append(configs, []byte(conf)) + } else if data := parseConfString(conf); data != nil { + configs = append(configs, data) + } else { + // config as file + if ConfigPath == "" { + ConfigPath = conf + } + + if data, _ = os.ReadFile(conf); data == nil { + continue + } + + data = []byte(shell.ReplaceEnvVars(string(data))) + configs = append(configs, data) + } + } + + if ConfigPath != "" { + if !filepath.IsAbs(ConfigPath) { + if cwd, err := os.Getwd(); err == nil { + ConfigPath = filepath.Join(cwd, ConfigPath) + } + } + Info["config_path"] = ConfigPath + } +} + +func parseConfString(s string) []byte { + i := strings.IndexByte(s, '=') + if i < 0 { + return nil + } + + items := strings.Split(s[:i], ".") + if len(items) < 2 { + return nil + } + + // `log.level=trace` => `{log: {level: trace}}` + var pre string + var suf = s[i+1:] + for _, item := range items { + pre += "{" + item + ": " + suf += "}" + } + + return []byte(pre + suf) +} diff --git a/internal/app/log.go b/internal/app/log.go index 222f6f2b..094dfbbf 100644 --- a/internal/app/log.go +++ b/internal/app/log.go @@ -6,30 +6,49 @@ import ( "github.com/mattn/go-isatty" "github.com/rs/zerolog" - "github.com/rs/zerolog/log" ) var MemoryLog = newBuffer(16) -// NewLogger support: +func GetLogger(module string) zerolog.Logger { + if s, ok := modules[module]; ok { + lvl, err := zerolog.ParseLevel(s) + if err == nil { + return Logger.Level(lvl) + } + Logger.Warn().Err(err).Caller().Send() + } + + return Logger +} + +// initLogger support: // - output: empty (only to memory), stderr, stdout // - format: empty (autodetect color support), color, json, text // - time: empty (disable timestamp), UNIXMS, UNIXMICRO, UNIXNANO // - level: disabled, trace, debug, info, warn, error... -func NewLogger(config map[string]string) zerolog.Logger { +func initLogger() { + var cfg struct { + Mod map[string]string `yaml:"log"` + } + + cfg.Mod = modules // defaults + + LoadConfig(&cfg) + var writer io.Writer - switch config["output"] { + switch modules["output"] { case "stderr": writer = os.Stderr case "stdout": writer = os.Stdout } - timeFormat := config["time"] + timeFormat := modules["time"] if writer != nil { - if format := config["format"]; format != "json" { + if format := modules["format"]; format != "json" { console := &zerolog.ConsoleWriter{Out: writer} switch format { @@ -61,31 +80,24 @@ func NewLogger(config map[string]string) zerolog.Logger { writer = MemoryLog } - logger := zerolog.New(writer) + lvl, _ := zerolog.ParseLevel(modules["level"]) + Logger = zerolog.New(writer).Level(lvl) if timeFormat != "" { zerolog.TimeFieldFormat = timeFormat - logger = logger.With().Timestamp().Logger() + Logger = Logger.With().Timestamp().Logger() } - - lvl, _ := zerolog.ParseLevel(config["level"]) - return logger.Level(lvl) } -func GetLogger(module string) zerolog.Logger { - if s, ok := modules[module]; ok { - lvl, err := zerolog.ParseLevel(s) - if err == nil { - return log.Level(lvl) - } - log.Warn().Err(err).Caller().Send() - } - - return log.Logger -} +var Logger zerolog.Logger // modules log levels -var modules map[string]string +var modules = map[string]string{ + "format": "", // useless, but anyway + "level": "info", + "output": "stdout", // TODO: change to stderr someday + "time": zerolog.TimeFormatUnixMs, +} const chunkSize = 1 << 16 diff --git a/internal/app/migrate.go b/internal/app/migrate.go deleted file mode 100644 index 95c51c51..00000000 --- a/internal/app/migrate.go +++ /dev/null @@ -1,35 +0,0 @@ -package app - -import ( - "encoding/json" - "os" - - "github.com/rs/zerolog/log" -) - -func migrateStore() { - const name = "go2rtc.json" - - data, _ := os.ReadFile(name) - if data == nil { - return - } - - var store struct { - Streams map[string]string `json:"streams"` - } - - if err := json.Unmarshal(data, &store); err != nil { - log.Warn().Err(err).Caller().Send() - return - } - - for id, url := range store.Streams { - if err := PatchConfig(id, url, "streams"); err != nil { - log.Warn().Err(err).Caller().Send() - return - } - } - - _ = os.Remove(name) -} diff --git a/internal/dvrip/dvrip.go b/internal/dvrip/dvrip.go index 470e8afd..095372d2 100644 --- a/internal/dvrip/dvrip.go +++ b/internal/dvrip/dvrip.go @@ -12,7 +12,6 @@ import ( "github.com/AlexxIT/go2rtc/internal/streams" "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/dvrip" - "github.com/rs/zerolog/log" ) func Init() { @@ -92,10 +91,7 @@ func sendBroadcasts(conn *net.UDPConn) { for i := 0; i < 3; i++ { time.Sleep(100 * time.Millisecond) - - if _, err = conn.WriteToUDP(data, addr); err != nil { - log.Err(err).Caller().Send() - } + _, _ = conn.WriteToUDP(data, addr) } } diff --git a/internal/ffmpeg/ffmpeg.go b/internal/ffmpeg/ffmpeg.go index aeba85fb..062e5aaf 100644 --- a/internal/ffmpeg/ffmpeg.go +++ b/internal/ffmpeg/ffmpeg.go @@ -14,6 +14,7 @@ import ( "github.com/AlexxIT/go2rtc/internal/streams" "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/ffmpeg" + "github.com/rs/zerolog" ) func Init() { @@ -29,6 +30,8 @@ func Init() { app.LoadConfig(&cfg) + log = app.GetLogger("ffmpeg") + // zerolog levels: trace debug info warn error fatal panic disabled // FFmpeg levels: trace debug verbose info warning error fatal panic quiet if cfg.Log.Level == "warn" { @@ -145,6 +148,8 @@ var defaults = map[string]string{ "h265/videotoolbox": "-c:v hevc_videotoolbox -g 50 -bf 0 -profile:v main -level:v 5.1", } +var log zerolog.Logger + // configTemplate - return template from config (defaults) if exist or return raw template func configTemplate(template string) string { if s := defaults[template]; s != "" { diff --git a/internal/ffmpeg/hardware/hardware.go b/internal/ffmpeg/hardware/hardware.go index ebbdc4fa..39ce3323 100644 --- a/internal/ffmpeg/hardware/hardware.go +++ b/internal/ffmpeg/hardware/hardware.go @@ -7,8 +7,6 @@ import ( "github.com/AlexxIT/go2rtc/internal/api" "github.com/AlexxIT/go2rtc/pkg/ffmpeg" - - "github.com/rs/zerolog/log" ) const ( @@ -152,7 +150,6 @@ var cache = map[string]string{} func run(bin string, args string) bool { err := exec.Command(bin, strings.Split(args, " ")...).Run() - log.Printf("%v %v", args, err) return err == nil } diff --git a/internal/ffmpeg/version.go b/internal/ffmpeg/version.go index 976c92d0..717e08a4 100644 --- a/internal/ffmpeg/version.go +++ b/internal/ffmpeg/version.go @@ -6,7 +6,6 @@ import ( "sync" "github.com/AlexxIT/go2rtc/pkg/ffmpeg" - "github.com/rs/zerolog/log" ) var verMu sync.Mutex diff --git a/internal/mjpeg/init.go b/internal/mjpeg/init.go index ea65e2d7..0bed95c6 100644 --- a/internal/mjpeg/init.go +++ b/internal/mjpeg/init.go @@ -10,6 +10,7 @@ import ( "github.com/AlexxIT/go2rtc/internal/api" "github.com/AlexxIT/go2rtc/internal/api/ws" + "github.com/AlexxIT/go2rtc/internal/app" "github.com/AlexxIT/go2rtc/internal/ffmpeg" "github.com/AlexxIT/go2rtc/internal/streams" "github.com/AlexxIT/go2rtc/pkg/ascii" @@ -18,7 +19,7 @@ import ( "github.com/AlexxIT/go2rtc/pkg/mjpeg" "github.com/AlexxIT/go2rtc/pkg/tcp" "github.com/AlexxIT/go2rtc/pkg/y4m" - "github.com/rs/zerolog/log" + "github.com/rs/zerolog" ) func Init() { @@ -28,8 +29,12 @@ func Init() { api.HandleFunc("api/stream.y4m", apiStreamY4M) ws.HandleFunc("mjpeg", handlerWS) + + log = app.GetLogger("mjpeg") } +var log zerolog.Logger + func handlerKeyframe(w http.ResponseWriter, r *http.Request) { src := r.URL.Query().Get("src") stream := streams.Get(src) diff --git a/internal/mpegts/aac.go b/internal/mpegts/aac.go index 3008a658..867dc971 100644 --- a/internal/mpegts/aac.go +++ b/internal/mpegts/aac.go @@ -7,7 +7,6 @@ import ( "github.com/AlexxIT/go2rtc/internal/streams" "github.com/AlexxIT/go2rtc/pkg/aac" "github.com/AlexxIT/go2rtc/pkg/tcp" - "github.com/rs/zerolog/log" ) func apiStreamAAC(w http.ResponseWriter, r *http.Request) { @@ -23,7 +22,6 @@ func apiStreamAAC(w http.ResponseWriter, r *http.Request) { cons.UserAgent = r.UserAgent() if err := stream.AddConsumer(cons); err != nil { - log.Error().Err(err).Caller().Send() http.Error(w, err.Error(), http.StatusInternalServerError) return } diff --git a/internal/mpegts/mpegts.go b/internal/mpegts/mpegts.go index 6f4f6ab2..6ef00ba1 100644 --- a/internal/mpegts/mpegts.go +++ b/internal/mpegts/mpegts.go @@ -7,7 +7,6 @@ import ( "github.com/AlexxIT/go2rtc/internal/streams" "github.com/AlexxIT/go2rtc/pkg/mpegts" "github.com/AlexxIT/go2rtc/pkg/tcp" - "github.com/rs/zerolog/log" ) func Init() { @@ -36,7 +35,6 @@ func outputMpegTS(w http.ResponseWriter, r *http.Request) { cons.UserAgent = r.UserAgent() if err := stream.AddConsumer(cons); err != nil { - log.Error().Err(err).Caller().Send() http.Error(w, err.Error(), http.StatusInternalServerError) return } diff --git a/main.go b/main.go index 91bc9938..27490f6a 100644 --- a/main.go +++ b/main.go @@ -36,6 +36,8 @@ import ( ) func main() { + app.Version = "1.9.2" + // 1. Core modules: app, api/ws, streams app.Init() // init config and logs diff --git a/pkg/opus/opus.go b/pkg/opus/.opus.go similarity index 97% rename from pkg/opus/opus.go rename to pkg/opus/.opus.go index 9fe1d8b6..42043977 100644 --- a/pkg/opus/opus.go +++ b/pkg/opus/.opus.go @@ -5,7 +5,6 @@ import ( "github.com/AlexxIT/go2rtc/pkg/core" "github.com/pion/rtp" - "github.com/rs/zerolog/log" ) func Log(handler core.HandlerFunc) core.HandlerFunc { diff --git a/pkg/roborock/client.go b/pkg/roborock/client.go index 39caab88..6a3bf0e0 100644 --- a/pkg/roborock/client.go +++ b/pkg/roborock/client.go @@ -6,7 +6,6 @@ import ( "encoding/json" "errors" "fmt" - "log" "net/rpc" "net/url" "strconv" @@ -138,7 +137,7 @@ func (c *Client) Connect() error { } offer := pc.LocalDescription() - log.Printf("[roborock] offer\n%s", offer.SDP) + //log.Printf("[roborock] offer\n%s", offer.SDP) if err = c.SendSDPtoRobot(offer); err != nil { return err } @@ -151,7 +150,7 @@ func (c *Client) Connect() error { time.Sleep(time.Second) if desc, _ := c.GetDeviceSDP(); desc != nil { - log.Printf("[roborock] answer\n%s", desc.SDP) + //log.Printf("[roborock] answer\n%s", desc.SDP) if err = c.conn.SetAnswer(desc.SDP); err != nil { return err } diff --git a/pkg/roborock/iot/client.go b/pkg/roborock/iot/client.go index 8773455d..c3b2d97f 100644 --- a/pkg/roborock/iot/client.go +++ b/pkg/roborock/iot/client.go @@ -6,12 +6,12 @@ import ( "encoding/hex" "encoding/json" "fmt" - "github.com/AlexxIT/go2rtc/pkg/mqtt" - "github.com/rs/zerolog/log" "net" "net/rpc" "net/url" "time" + + "github.com/AlexxIT/go2rtc/pkg/mqtt" ) type Codec struct { @@ -56,7 +56,7 @@ func (c *Codec) WriteRequest(r *rpc.Request, v any) error { return err } - log.Printf("[roborock] send: %s", payload) + //log.Printf("[roborock] send: %s", payload) payload = c.Encrypt(payload, ts, ts, ts) @@ -86,7 +86,7 @@ func (c *Codec) ReadResponseHeader(r *rpc.Response) error { continue } - log.Printf("[roborock] recv %s", payload) + //log.Printf("[roborock] recv %s", payload) // get content from response payload: // {"t":1676871268,"dps":{"102":"{\"id\":315003,\"result\":[\"ok\"]}"}} From 9bb36ebb6c6af157a274e95de9e93c8430bce69e Mon Sep 17 00:00:00 2001 From: Alex X Date: Wed, 5 Jun 2024 19:59:22 +0300 Subject: [PATCH 009/166] Fix ghost exec/ffmpeg process --- internal/exec/exec.go | 8 ++++++-- internal/streams/producer.go | 6 +++++- pkg/rtsp/client.go | 3 +++ pkg/rtsp/conn.go | 1 + 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/internal/exec/exec.go b/internal/exec/exec.go index 454c54a4..d30b0dbe 100644 --- a/internal/exec/exec.go +++ b/internal/exec/exec.go @@ -113,7 +113,7 @@ func handleRTSP(url string, cmd *exec.Cmd, path string) (core.Producer, error) { cmd.Stdout = os.Stdout } - waiter := make(chan core.Producer) + waiter := make(chan *pkg.Conn, 1) waitersMu.Lock() waiters[path] = waiter @@ -149,6 +149,10 @@ func handleRTSP(url string, cmd *exec.Cmd, path string) (core.Producer, error) { return nil, fmt.Errorf("exec/rtsp\n%s", cmd.Stderr) case prod := <-waiter: log.Debug().Stringer("launch", time.Since(ts)).Msg("[exec] run rtsp") + prod.OnClose = func() error { + log.Debug().Msgf("[exec] kill rtsp") + return errors.Join(cmd.Process.Kill(), cmd.Wait()) + } return prod, nil } } @@ -157,7 +161,7 @@ func handleRTSP(url string, cmd *exec.Cmd, path string) (core.Producer, error) { var ( log zerolog.Logger - waiters = map[string]chan core.Producer{} + waiters = make(map[string]chan *pkg.Conn) waitersMu sync.Mutex ) diff --git a/internal/streams/producer.go b/internal/streams/producer.go index 5a25dba5..daca7edf 100644 --- a/internal/streams/producer.go +++ b/internal/streams/producer.go @@ -207,7 +207,7 @@ func (p *Producer) reconnect(workerID, retry int) { for _, media := range conn.GetMedias() { switch media.Direction { case core.DirectionRecvonly: - for _, receiver := range p.receivers { + for i, receiver := range p.receivers { codec := media.MatchCodec(receiver.Codec) if codec == nil { continue @@ -219,6 +219,7 @@ func (p *Producer) reconnect(workerID, retry int) { } receiver.Replace(track) + p.receivers[i] = track break } @@ -234,6 +235,9 @@ func (p *Producer) reconnect(workerID, retry int) { } } + // stop previous connection after moving tracks (fix ghost exec/ffmpeg) + _ = p.conn.Stop() + // swap connections p.conn = conn go p.worker(conn, workerID) diff --git a/pkg/rtsp/client.go b/pkg/rtsp/client.go index ca32ce32..59f96e94 100644 --- a/pkg/rtsp/client.go +++ b/pkg/rtsp/client.go @@ -304,5 +304,8 @@ func (c *Conn) Close() error { if c.mode == core.ModeActiveProducer { _ = c.Teardown() } + if c.OnClose != nil { + _ = c.OnClose() + } return c.conn.Close() } diff --git a/pkg/rtsp/conn.go b/pkg/rtsp/conn.go index 91465f2c..1d9edf06 100644 --- a/pkg/rtsp/conn.go +++ b/pkg/rtsp/conn.go @@ -24,6 +24,7 @@ type Conn struct { Backchannel bool Media string + OnClose func() error PacketSize uint16 SessionName string Timeout int From e0b1a503561b8def00eaec5ec914ba8b04cfa604 Mon Sep 17 00:00:00 2001 From: Alex X Date: Wed, 5 Jun 2024 20:00:41 +0300 Subject: [PATCH 010/166] Add rtsp_client for testing ghost exec process --- examples/rtsp_client/main.go | 39 ++++++++++++++++++++++++++++++++++++ internal/streams/README.md | 8 ++++++++ pkg/rtsp/client.go | 14 +++++++++++-- 3 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 examples/rtsp_client/main.go create mode 100644 internal/streams/README.md diff --git a/examples/rtsp_client/main.go b/examples/rtsp_client/main.go new file mode 100644 index 00000000..9c2112d1 --- /dev/null +++ b/examples/rtsp_client/main.go @@ -0,0 +1,39 @@ +package main + +import ( + "log" + "os" + + "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/AlexxIT/go2rtc/pkg/rtsp" + "github.com/AlexxIT/go2rtc/pkg/shell" +) + +func main() { + client := rtsp.NewClient(os.Args[1]) + if err := client.Dial(); err != nil { + log.Panic(err) + } + + client.Medias = []*core.Media{ + { + Kind: core.KindAudio, + Direction: core.DirectionRecvonly, + Codecs: []*core.Codec{ + {Name: core.CodecPCMU, ClockRate: 8000}, + }, + ID: "streamid=0", + }, + } + if err := client.Announce(); err != nil { + log.Panic(err) + } + if _, err := client.SetupMedia(client.Medias[0]); err != nil { + log.Panic(err) + } + if err := client.Record(); err != nil { + log.Panic(err) + } + + shell.RunUntilSignal() +} diff --git a/internal/streams/README.md b/internal/streams/README.md new file mode 100644 index 00000000..6bbc268a --- /dev/null +++ b/internal/streams/README.md @@ -0,0 +1,8 @@ +## Testing notes + +```yaml +streams: + test1-basic: ffmpeg:virtual?video#video=h264 + test2-reconnect: ffmpeg:virtual?video&duration=10#video=h264 + test3-execkill: exec:./examples/rtsp_client/rtsp_client/rtsp_client {output} +``` diff --git a/pkg/rtsp/client.go b/pkg/rtsp/client.go index 59f96e94..9002d0a1 100644 --- a/pkg/rtsp/client.go +++ b/pkg/rtsp/client.go @@ -186,10 +186,20 @@ func (c *Conn) Announce() (err error) { return err } - res, err := c.Do(req) + _, err = c.Do(req) + return +} - _ = res +func (c *Conn) Record() (err error) { + req := &tcp.Request{ + Method: MethodRecord, + URL: c.URL, + Header: map[string][]string{ + "Range": {"npt=0.000-"}, + }, + } + _, err = c.Do(req) return } From 31e4ba27222d6f106abc1348f134a583a370e93b Mon Sep 17 00:00:00 2001 From: Alex X Date: Wed, 5 Jun 2024 20:01:47 +0300 Subject: [PATCH 011/166] Rewrite Receiver/Sender classes --- pkg/core/codec.go | 5 + pkg/core/core_test.go | 120 ++++++++++++++++++ pkg/core/node.go | 87 +++++++++++++ pkg/core/track.go | 282 +++++++++++++++++------------------------- 4 files changed, 327 insertions(+), 167 deletions(-) create mode 100644 pkg/core/core_test.go create mode 100644 pkg/core/node.go diff --git a/pkg/core/codec.go b/pkg/core/codec.go index f38d7965..fe813de3 100644 --- a/pkg/core/codec.go +++ b/pkg/core/codec.go @@ -2,6 +2,7 @@ package core import ( "encoding/base64" + "encoding/json" "fmt" "strconv" "strings" @@ -18,6 +19,10 @@ type Codec struct { PayloadType uint8 } +func (c *Codec) MarshalJSON() ([]byte, error) { + return json.Marshal(c.String()) +} + func (c *Codec) String() string { s := fmt.Sprintf("%d %s", c.PayloadType, c.Name) if c.ClockRate != 0 && c.ClockRate != 90000 { diff --git a/pkg/core/core_test.go b/pkg/core/core_test.go new file mode 100644 index 00000000..4a05380a --- /dev/null +++ b/pkg/core/core_test.go @@ -0,0 +1,120 @@ +package core + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" +) + +type producer struct { + Medias []*Media + Receivers []*Receiver + + id byte +} + +func (p *producer) GetMedias() []*Media { + return p.Medias +} + +func (p *producer) GetTrack(_ *Media, codec *Codec) (*Receiver, error) { + for _, receiver := range p.Receivers { + if receiver.Codec == codec { + return receiver, nil + } + } + receiver := NewReceiver(nil, codec) + p.Receivers = append(p.Receivers, receiver) + return receiver, nil +} + +func (p *producer) Start() error { + pkt := &Packet{Payload: []byte{p.id}} + p.Receivers[0].Input(pkt) + return nil +} + +func (p *producer) Stop() error { + for _, receiver := range p.Receivers { + receiver.Close() + } + return nil +} + +type consumer struct { + Medias []*Media + Senders []*Sender + + cache chan byte +} + +func (c *consumer) GetMedias() []*Media { + return c.Medias +} + +func (c *consumer) AddTrack(_ *Media, _ *Codec, track *Receiver) error { + c.cache = make(chan byte, 1) + sender := NewSender(nil, track.Codec) + sender.Output = func(packet *Packet) { + c.cache <- packet.Payload[0] + } + sender.HandleRTP(track) + c.Senders = append(c.Senders, sender) + return nil +} + +func (c *consumer) Stop() error { + for _, sender := range c.Senders { + sender.Close() + } + return nil +} + +func (c *consumer) read() byte { + return <-c.cache +} + +func TestName(t *testing.T) { + GetProducer := func(b byte) Producer { + return &producer{ + Medias: []*Media{ + { + Kind: KindVideo, + Direction: DirectionRecvonly, + Codecs: []*Codec{ + {Name: CodecH264}, + }, + }, + }, + id: b, + } + } + + // stage1 + prod1 := GetProducer(1) + cons2 := &consumer{} + + media1 := prod1.GetMedias()[0] + track1, _ := prod1.GetTrack(media1, media1.Codecs[0]) + + _ = cons2.AddTrack(nil, nil, track1) + + _ = prod1.Start() + require.Equal(t, byte(1), cons2.read()) + + // stage2 + prod2 := GetProducer(2) + media2 := prod2.GetMedias()[0] + require.NotEqual(t, fmt.Sprintf("%p", media1), fmt.Sprintf("%p", media2)) + track2, _ := prod2.GetTrack(media2, media2.Codecs[0]) + track1.Replace(track2) + + _ = prod1.Stop() + + _ = prod2.Start() + require.Equal(t, byte(2), cons2.read()) + + // stage3 + _ = prod2.Stop() +} diff --git a/pkg/core/node.go b/pkg/core/node.go new file mode 100644 index 00000000..fd58f2d7 --- /dev/null +++ b/pkg/core/node.go @@ -0,0 +1,87 @@ +package core + +import ( + "sync" + + "github.com/pion/rtp" +) + +//type Packet struct { +// Payload []byte +// Timestamp uint32 // PTS if DTS == 0 else DTS +// Composition uint32 // CTS = PTS-DTS (for support B-frames) +// Sequence uint16 +//} + +type Packet = rtp.Packet + +// HandlerFunc - process input packets (just like http.HandlerFunc) +type HandlerFunc func(packet *Packet) + +// Filter - a decorator for any HandlerFunc +type Filter func(handler HandlerFunc) HandlerFunc + +// Node - Receiver or Sender or Filter (transform) +type Node struct { + Codec *Codec `json:"codec"` + Input HandlerFunc `json:"-"` + Output HandlerFunc `json:"-"` + + childs []*Node + parent *Node + + mu sync.Mutex +} + +func (n *Node) WithParent(parent *Node) *Node { + parent.AppendChild(n) + return n +} + +func (n *Node) AppendChild(child *Node) { + n.mu.Lock() + n.childs = append(n.childs, child) + n.mu.Unlock() + + child.parent = n +} + +func (n *Node) RemoveChild(child *Node) { + n.mu.Lock() + for i, ch := range n.childs { + if ch == child { + n.childs = append(n.childs[:i], n.childs[i+1:]...) + break + } + } + n.mu.Unlock() +} + +func (n *Node) Close() { + if parent := n.parent; parent != nil { + parent.RemoveChild(n) + + if len(parent.childs) == 0 { + parent.Close() + } + } else { + for _, childs := range n.childs { + childs.Close() + } + } +} + +func MoveNode(dst, src *Node) { + src.mu.Lock() + childs := src.childs + src.childs = nil + src.mu.Unlock() + + dst.mu.Lock() + dst.childs = childs + dst.mu.Unlock() + + for _, child := range childs { + child.parent = dst + } +} diff --git a/pkg/core/track.go b/pkg/core/track.go index 72e47074..83c39e01 100644 --- a/pkg/core/track.go +++ b/pkg/core/track.go @@ -1,225 +1,173 @@ package core import ( - "encoding/json" "errors" - "fmt" - "strconv" - "sync" "github.com/pion/rtp" ) -type Packet struct { - PayloadType uint8 - Sequence uint16 - Timestamp uint32 // PTS if DTS == 0 else DTS - Composition uint32 // CTS = PTS-DTS (for support B-frames) - Payload []byte -} - var ErrCantGetTrack = errors.New("can't get track") type Receiver struct { - Codec *Codec - Media *Media + Node - ID byte // Channel for RTSP, PayloadType for MPEG-TS + // Deprecated: should be removed + Media *Media `json:"-"` + // Deprecated: should be removed + ID byte `json:"-"` // Channel for RTSP, PayloadType for MPEG-TS - senders map[*Sender]chan *rtp.Packet - mu sync.RWMutex - bytes int + Bytes int `json:"bytes,omitempty"` + Packets int `json:"packets,omitempty"` } func NewReceiver(media *Media, codec *Codec) *Receiver { - Assert(codec != nil) - return &Receiver{Codec: codec, Media: media} -} - -// WriteRTP - fast and non blocking write to all readers buffers -func (t *Receiver) WriteRTP(packet *rtp.Packet) { - t.mu.Lock() - t.bytes += len(packet.Payload) - for sender, buffer := range t.senders { - select { - case buffer <- packet: - default: - sender.overflow++ + r := &Receiver{ + Node: Node{Codec: codec}, + Media: media, + } + r.Input = func(packet *Packet) { + r.Bytes += len(packet.Payload) + r.Packets++ + for _, child := range r.childs { + child.Input(packet) } } - t.mu.Unlock() + return r } -func (t *Receiver) Senders() (senders []*Sender) { - t.mu.RLock() - for sender := range t.senders { - senders = append(senders, sender) +// Deprecated: should be removed +func (r *Receiver) WriteRTP(packet *rtp.Packet) { + r.Input(packet) +} + +// Deprecated: should be removed +func (r *Receiver) Senders() []*Sender { + if len(r.childs) > 0 { + return []*Sender{{}} + } else { + return nil } - t.mu.RUnlock() - return } -func (t *Receiver) Close() { - t.mu.Lock() - // close all sender channel buffers and erase senders list - for _, buffer := range t.senders { - close(buffer) - } - t.senders = nil - t.mu.Unlock() +// Deprecated: should be removed +func (r *Receiver) Replace(target *Receiver) { + MoveNode(&target.Node, &r.Node) } -func (t *Receiver) Replace(target *Receiver) { - // move this receiver senders to new receiver - t.mu.Lock() - senders := t.senders - t.mu.Unlock() - - target.mu.Lock() - target.senders = senders - target.mu.Unlock() -} - -func (t *Receiver) String() string { - s := t.Codec.String() + ", bytes=" + strconv.Itoa(t.bytes) - t.mu.RLock() - s += fmt.Sprintf(", senders=%d", len(t.senders)) - t.mu.RUnlock() - return s -} - -func (t *Receiver) MarshalJSON() ([]byte, error) { - return json.Marshal(t.String()) +func (r *Receiver) Close() { + r.Node.Close() } type Sender struct { - Codec *Codec - Media *Media + Node - Handler HandlerFunc + // Deprecated: + Media *Media `json:"-"` + // Deprecated: + Handler HandlerFunc `json:"-"` - receivers []*Receiver - mu sync.RWMutex - bytes int + Bytes int `json:"bytes,omitempty"` + Packets int `json:"packets,omitempty"` + Drops int `json:"drops,omitempty"` - overflow int + buf chan *Packet + done chan struct{} } func NewSender(media *Media, codec *Codec) *Sender { - return &Sender{Codec: codec, Media: media} -} + var bufSize uint16 -// HandlerFunc like http.HandlerFunc -type HandlerFunc func(packet *rtp.Packet) - -func (s *Sender) HandleRTP(track *Receiver) { - s.Bind(track) - go s.worker(track) -} - -func (s *Sender) Bind(track *Receiver) { - var bufferSize uint16 - - if GetKind(track.Codec.Name) == KindVideo { - if track.Codec.IsRTP() { + if GetKind(codec.Name) == KindVideo { + if codec.IsRTP() { // in my tests 40Mbit/s 4K-video can generate up to 1500 items // for the h264.RTPDepay => RTPPay queue - bufferSize = 5000 + bufSize = 4096 } else { - bufferSize = 50 + bufSize = 64 } } else { - bufferSize = 100 + bufSize = 128 } - buffer := make(chan *rtp.Packet, bufferSize) - - track.mu.Lock() - if track.senders == nil { - track.senders = map[*Sender]chan *rtp.Packet{} + buf := make(chan *Packet, bufSize) + s := &Sender{ + Node: Node{Codec: codec}, + Media: media, + buf: buf, } - track.senders[s] = buffer - track.mu.Unlock() - - s.mu.Lock() - s.receivers = append(s.receivers, track) - s.mu.Unlock() + s.Input = func(packet *Packet) { + // writing to nil chan - OK, writing to closed chan - panic + s.mu.Lock() + select { + case s.buf <- packet: + s.Bytes += len(packet.Payload) + s.Packets++ + default: + s.Drops++ + } + s.mu.Unlock() + } + s.Output = func(packet *Packet) { + s.Handler(packet) + } + return s } -func (s *Sender) worker(track *Receiver) { - track.mu.Lock() - buffer := track.senders[s] - track.mu.Unlock() +// Deprecated: should be removed +func (s *Sender) HandleRTP(parent *Receiver) { + s.WithParent(parent) + s.Start() +} - // read packets from buffer channel until it will be closed - if buffer != nil { - for packet := range buffer { - s.bytes += len(packet.Payload) - s.Handler(packet) - } - } +// Deprecated: should be removed +func (s *Sender) Bind(parent *Receiver) { + s.WithParent(parent) +} - // remove current receiver from list - // it can only happen when receiver close buffer channel - s.mu.Lock() - for i, receiver := range s.receivers { - if receiver == track { - s.receivers = append(s.receivers[:i], s.receivers[i+1:]...) - break - } - } - s.mu.Unlock() +func (s *Sender) WithParent(parent *Receiver) *Sender { + s.Node.WithParent(&parent.Node) + return s } func (s *Sender) Start() { s.mu.Lock() - for _, track := range s.receivers { - go s.worker(track) + defer s.mu.Unlock() + + if s.buf == nil || s.done != nil { + return } - s.mu.Unlock() + s.done = make(chan struct{}) + + go func() { + for packet := range s.buf { + s.Output(packet) + } + close(s.done) + }() +} + +func (s *Sender) Wait() { + if done := s.done; s.done != nil { + <-done + } +} + +func (s *Sender) State() string { + if s.buf == nil { + return "closed" + } + if s.done == nil { + return "new" + } + return "connected" } func (s *Sender) Close() { - s.mu.Lock() - // remove this sender from all receivers list - for _, receiver := range s.receivers { - receiver.mu.Lock() - if buffer := receiver.senders[s]; buffer != nil { - // remove channel from list - delete(receiver.senders, s) - // close channel - close(buffer) - } - receiver.mu.Unlock() + // close buffer if exists + if buf := s.buf; buf != nil { + s.buf = nil + defer close(buf) } - s.receivers = nil - s.mu.Unlock() -} -func (s *Sender) String() string { - info := s.Codec.String() + ", bytes=" + strconv.Itoa(s.bytes) - s.mu.RLock() - info += ", receivers=" + strconv.Itoa(len(s.receivers)) - s.mu.RUnlock() - if s.overflow > 0 { - info += ", overflow=" + strconv.Itoa(s.overflow) - } - return info -} - -func (s *Sender) MarshalJSON() ([]byte, error) { - return json.Marshal(s.String()) -} - -// VA - helper, for extract video and audio receivers from list -func VA(receivers []*Receiver) (video, audio *Receiver) { - for _, receiver := range receivers { - switch GetKind(receiver.Codec.Name) { - case KindVideo: - video = receiver - case KindAudio: - audio = receiver - } - } - return + s.Node.Close() } From ec33796bd34bb6c2bbd03d4d17cf900188946558 Mon Sep 17 00:00:00 2001 From: Alex X Date: Wed, 5 Jun 2024 20:02:10 +0300 Subject: [PATCH 012/166] Add goweight to useful commands --- scripts/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/README.md b/scripts/README.md index b893b312..efcef154 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -14,6 +14,7 @@ go get -u go mod tidy go mod why github.com/pion/rtcp go list -deps .\cmd\go2rtc_rtsp\ +./goweight ``` ## Dependencies From 8377ad1d0547a50aecf7bb64a3bb894613b69adf Mon Sep 17 00:00:00 2001 From: Alex X Date: Thu, 6 Jun 2024 13:16:12 +0300 Subject: [PATCH 013/166] Update codec section in stream info --- pkg/core/codec.go | 88 +++++++++++++++++++++++++++++++---------------- pkg/core/media.go | 2 +- 2 files changed, 60 insertions(+), 30 deletions(-) diff --git a/pkg/core/codec.go b/pkg/core/codec.go index fe813de3..91f6fddc 100644 --- a/pkg/core/codec.go +++ b/pkg/core/codec.go @@ -4,7 +4,6 @@ import ( "encoding/base64" "encoding/json" "fmt" - "strconv" "strings" "unicode" @@ -19,38 +18,70 @@ type Codec struct { PayloadType uint8 } +// MarshalJSON - return FFprobe compatible output func (c *Codec) MarshalJSON() ([]byte, error) { - return json.Marshal(c.String()) -} - -func (c *Codec) String() string { - s := fmt.Sprintf("%d %s", c.PayloadType, c.Name) - if c.ClockRate != 0 && c.ClockRate != 90000 { - s = fmt.Sprintf("%s/%d", s, c.ClockRate) + info := map[string]any{} + if name := FFmpegCodecName(c.Name); name != "" { + info["codec_name"] = name + info["codec_type"] = c.Kind() } - if c.Channels > 0 { - s = fmt.Sprintf("%s/%d", s, c.Channels) - } - return s -} - -func (c *Codec) Text() string { - switch c.Name { - case CodecH264: - if profile := DecodeH264(c.FmtpLine); profile != "" { - return "H.264 " + profile + if c.Name == CodecH264 { + profile, level := DecodeH264(c.FmtpLine) + if profile != "" { + info["profile"] = profile + info["level"] = level } - return c.Name } - - s := c.Name if c.ClockRate != 0 && c.ClockRate != 90000 { - s += "/" + strconv.Itoa(int(c.ClockRate)) + info["sample_rate"] = c.ClockRate } if c.Channels > 0 { - s += "/" + strconv.Itoa(int(c.Channels)) + info["channels"] = c.Channels } - return s + return json.Marshal(info) +} + +func FFmpegCodecName(name string) string { + switch name { + case CodecH264: + return "h264" + case CodecH265: + return "h265" + case CodecJPEG: + return "mjpeg" + case CodecRAW: + return "rawvideo" + case CodecPCMA: + return "pcm_alaw" + case CodecPCMU: + return "pcm_mulaw" + case CodecPCM: + return "pcm_s16be" + case CodecPCML: + return "pcm_s16le" + case CodecAAC: + return "aac" + case CodecOpus: + return "opus" + case CodecVP8: + return "vp8" + case CodecVP9: + return "vp9" + case CodecAV1: + return "av1" + } + return "" +} + +func (c *Codec) String() (s string) { + s = c.Name + if c.ClockRate != 0 && c.ClockRate != 90000 { + s += fmt.Sprintf("/%d", c.ClockRate) + } + if c.Channels > 0 { + s += fmt.Sprintf("/%d", c.Channels) + } + return } func (c *Codec) IsRTP() bool { @@ -186,10 +217,9 @@ func UnmarshalCodec(md *sdp.MediaDescription, payloadType string) *Codec { return c } -func DecodeH264(fmtp string) string { +func DecodeH264(fmtp string) (profile string, level byte) { if ps := Between(fmtp, "sprop-parameter-sets=", ","); ps != "" { if sps, _ := base64.StdEncoding.DecodeString(ps); len(sps) >= 4 { - var profile string switch sps[1] { case 0x42: profile = "Baseline" @@ -203,8 +233,8 @@ func DecodeH264(fmtp string) string { profile = fmt.Sprintf("0x%02X", sps[1]) } - return fmt.Sprintf("%s %d.%d", profile, sps[3]/10, sps[3]%10) + level = sps[3] } } - return "" + return } diff --git a/pkg/core/media.go b/pkg/core/media.go index fe58cfd6..ef9ef74b 100644 --- a/pkg/core/media.go +++ b/pkg/core/media.go @@ -22,7 +22,7 @@ type Media struct { func (m *Media) String() string { s := fmt.Sprintf("%s, %s", m.Kind, m.Direction) for _, codec := range m.Codecs { - name := codec.Text() + name := codec.String() if strings.Contains(s, name) { continue From 2bab0a014d91a782ca5117a5019d8513874ca139 Mon Sep 17 00:00:00 2001 From: Alex X Date: Thu, 6 Jun 2024 14:34:16 +0300 Subject: [PATCH 014/166] Update dependencies --- go.mod | 16 ++++++++-------- go.sum | 14 ++++++++++++++ 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/go.mod b/go.mod index 0d5d67c9..d3cb791f 100644 --- a/go.mod +++ b/go.mod @@ -4,8 +4,9 @@ go 1.22 require ( github.com/asticode/go-astits v1.13.0 - github.com/expr-lang/expr v1.16.5 + github.com/expr-lang/expr v1.16.9 github.com/gorilla/websocket v1.5.1 + github.com/mattn/go-isatty v0.0.20 github.com/miekg/dns v1.1.59 github.com/pion/ice/v2 v2.3.24 github.com/pion/interceptor v0.1.29 @@ -15,12 +16,12 @@ require ( github.com/pion/srtp/v2 v2.0.18 github.com/pion/stun v0.6.1 github.com/pion/webrtc/v3 v3.2.40 - github.com/rs/zerolog v1.32.0 + github.com/rs/zerolog v1.33.0 github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1 github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f github.com/stretchr/testify v1.9.0 github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9 - golang.org/x/crypto v0.23.0 + golang.org/x/crypto v0.24.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -30,7 +31,6 @@ require ( github.com/google/uuid v1.6.0 // indirect github.com/kr/pretty v0.2.1 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect github.com/pion/datachannel v1.5.6 // indirect github.com/pion/dtls/v2 v2.2.11 // indirect github.com/pion/logging v0.2.2 // indirect @@ -40,9 +40,9 @@ require ( github.com/pion/transport/v2 v2.2.5 // indirect github.com/pion/turn/v2 v2.1.6 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/mod v0.17.0 // indirect - golang.org/x/net v0.25.0 // indirect + golang.org/x/mod v0.18.0 // indirect + golang.org/x/net v0.26.0 // indirect golang.org/x/sync v0.7.0 // indirect - golang.org/x/sys v0.20.0 // indirect - golang.org/x/tools v0.20.0 // indirect + golang.org/x/sys v0.21.0 // indirect + golang.org/x/tools v0.22.0 // indirect ) diff --git a/go.sum b/go.sum index 09b0abb9..727787ac 100644 --- a/go.sum +++ b/go.sum @@ -8,6 +8,8 @@ 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/expr-lang/expr v1.16.5 h1:m2hvtguFeVaVNTHj8L7BoAyt7O0PAIBaSVbjdHgRXMs= github.com/expr-lang/expr v1.16.5/go.mod h1:uCkhfG+x7fcZ5A5sXHKuQ07jGZRl6J0FCAaf2k4PtVQ= +github.com/expr-lang/expr v1.16.9 h1:WUAzmR0JNI9JCiF0/ewwHB1gmcGw5wW7nWt8gc6PpCI= +github.com/expr-lang/expr v1.16.9/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 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= @@ -85,6 +87,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0= github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= +github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1 h1:NVK+OqnavpyFmUiKfUMHrpvbCi2VFoWTrcpI7aDaJ2I= github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1/go.mod h1:9/etS5gpQq9BJsJMWg1wpLbfuSnkm8dPF6FdW2JXVhA= github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f h1:1R9KdKjCNSd7F8iGTxIpoID9prlYH8nuNYKt0XvweHA= @@ -115,10 +119,14 @@ golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= 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/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= +golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= @@ -134,6 +142,8 @@ golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/sync v0.0.0-20190423024810-112230192c58/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= @@ -160,6 +170,8 @@ golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.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.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -184,6 +196,8 @@ 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/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY= golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg= +golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= +golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= From e3188a0a6dcd872f8c011a119aeb5c565551d146 Mon Sep 17 00:00:00 2001 From: Alex X Date: Thu, 6 Jun 2024 15:18:55 +0300 Subject: [PATCH 015/166] Update docs about config --- internal/app/README.md | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/internal/app/README.md b/internal/app/README.md index e3d0571f..2460daa2 100644 --- a/internal/app/README.md +++ b/internal/app/README.md @@ -1,6 +1,10 @@ - By default go2rtc will search config file `go2rtc.yaml` in current work directory -- go2rtc support multiple config files -- go2rtc support inline config as `YAML`, `JSON` or `key=value` format from command line +- go2rtc support multiple config files: + - `go2rtc -c config1.yaml -c config2.yaml -c config3.yaml` +- go2rtc support inline config as multiple formats from command line: + - **YAML**: `go2rtc -c '{log: {format: text}}'` + - **JSON**: `go2rtc -c '{"log":{"format":"text"}}'` + - **key=value**: `go2rtc -c log.format=text` - Every next config will overwrite previous (but only defined params) ``` @@ -21,15 +25,24 @@ Also go2rtc support templates for using environment variables in any part of con streams: camera1: rtsp://rtsp:${CAMERA_PASSWORD}@192.168.1.123/av_stream/ch0 - ${LOGS:} # empty default value - rtsp: username: ${RTSP_USER:admin} # "admin" if env "RTSP_USER" not set password: ${RTSP_PASS:secret} # "secret" if env "RTSP_PASS" not set ``` +## JSON Schema + +Editors like [GoLand](https://www.jetbrains.com/go/) and [VS Code](https://code.visualstudio.com/) supports autocomplete and syntax validation. + +```yaml +# yaml-language-server: $schema=https://raw.githubusercontent.com/AlexxIT/go2rtc/master/website/schema.json +``` + ## Defaults +- Default values may change in updates +- FFmpeg module has many presets, they are not listed here because they may also change in updates + ```yaml api: listen: ":1984" @@ -38,7 +51,10 @@ ffmpeg: bin: "ffmpeg" log: + format: "color" level: "info" + output: "stdout" + time: "UNIXMS" rtsp: listen: ":8554" @@ -51,4 +67,4 @@ webrtc: listen: ":8555/tcp" ice_servers: - urls: [ "stun:stun.l.google.com:19302" ] -``` \ No newline at end of file +``` From cd777ba2b4c08f3d5aae2cb83704612a5937e85d Mon Sep 17 00:00:00 2001 From: Alex X Date: Thu, 6 Jun 2024 16:01:01 +0300 Subject: [PATCH 016/166] Update version to 1.9.3 --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index 27490f6a..d40851cc 100644 --- a/main.go +++ b/main.go @@ -36,7 +36,7 @@ import ( ) func main() { - app.Version = "1.9.2" + app.Version = "1.9.3" // 1. Core modules: app, api/ws, streams From bf303ed471fb117433332f42ebd7a8cece518d69 Mon Sep 17 00:00:00 2001 From: Alex X Date: Thu, 6 Jun 2024 17:58:31 +0300 Subject: [PATCH 017/166] Fix -d flag --- internal/app/app.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/app/app.go b/internal/app/app.go index 9331f041..1d63a2c8 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -53,7 +53,7 @@ func Init() { args := os.Args[1:] for i, arg := range args { - if arg == "-daemon" { + if arg == "-daemon" || arg == "-d" { args[i] = "" } } From b389d0eb9c390bd218198f5ddf11941765bc621d Mon Sep 17 00:00:00 2001 From: Sergey Krashevich Date: Thu, 6 Jun 2024 18:54:40 +0300 Subject: [PATCH 018/166] fix(app): handle daemon process correctly on Unix systems --- internal/app/app.go | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/internal/app/app.go b/internal/app/app.go index 1d63a2c8..ab4b6c94 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -7,6 +7,7 @@ import ( "os/exec" "runtime" "runtime/debug" + "syscall" ) var ( @@ -45,20 +46,23 @@ func Init() { os.Exit(0) } + if os.Getppid() == 1 || syscall.Getppid() == 1 { + daemon = false + } else { + parent, err := os.FindProcess(os.Getppid()) + if err != nil || parent.Pid < 1 { + daemon = false + } + } + if daemon { if runtime.GOOS == "windows" { fmt.Println("Daemon not supported on Windows") os.Exit(1) } - args := os.Args[1:] - for i, arg := range args { - if arg == "-daemon" || arg == "-d" { - args[i] = "" - } - } // Re-run the program in background and exit - cmd := exec.Command(os.Args[0], args...) + cmd := exec.Command(os.Args[0], os.Args[1:]...) if err := cmd.Start(); err != nil { fmt.Println(err) os.Exit(1) From aca0781c4b38f5ba400fa1be635014f0fd4e5426 Mon Sep 17 00:00:00 2001 From: Alex X Date: Fri, 7 Jun 2024 12:25:58 +0300 Subject: [PATCH 019/166] Code refactoring for api/streams --- internal/streams/api.go | 105 ++++++++++++++++++++++++++++++++++++ internal/streams/streams.go | 100 +--------------------------------- 2 files changed, 106 insertions(+), 99 deletions(-) create mode 100644 internal/streams/api.go diff --git a/internal/streams/api.go b/internal/streams/api.go new file mode 100644 index 00000000..72099425 --- /dev/null +++ b/internal/streams/api.go @@ -0,0 +1,105 @@ +package streams + +import ( + "net/http" + + "github.com/AlexxIT/go2rtc/internal/api" + "github.com/AlexxIT/go2rtc/internal/app" + "github.com/AlexxIT/go2rtc/pkg/probe" + "github.com/AlexxIT/go2rtc/pkg/tcp" +) + +func apiStreams(w http.ResponseWriter, r *http.Request) { + query := r.URL.Query() + src := query.Get("src") + + // without source - return all streams list + if src == "" && r.Method != "POST" { + api.ResponseJSON(w, streams) + return + } + + // Not sure about all this API. Should be rewrited... + switch r.Method { + case "GET": + stream := Get(src) + if stream == nil { + http.Error(w, "", http.StatusNotFound) + return + } + + cons := probe.NewProbe(query) + if len(cons.Medias) != 0 { + cons.RemoteAddr = tcp.RemoteAddr(r) + cons.UserAgent = r.UserAgent() + if err := stream.AddConsumer(cons); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + api.ResponsePrettyJSON(w, stream) + + stream.RemoveConsumer(cons) + } else { + api.ResponsePrettyJSON(w, streams[src]) + } + + case "PUT": + name := query.Get("name") + if name == "" { + name = src + } + + if New(name, src) == nil { + http.Error(w, "", http.StatusBadRequest) + return + } + + if err := app.PatchConfig(name, src, "streams"); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + } + + case "PATCH": + name := query.Get("name") + if name == "" { + http.Error(w, "", http.StatusBadRequest) + return + } + + // support {input} templates: https://github.com/AlexxIT/go2rtc#module-hass + if Patch(name, src) == nil { + http.Error(w, "", http.StatusBadRequest) + } + + case "POST": + // with dst - redirect source to dst + if dst := query.Get("dst"); dst != "" { + if stream := Get(dst); stream != nil { + if err := Validate(src); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + } else if err = stream.Play(src); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } else { + api.ResponseJSON(w, stream) + } + } else if stream = Get(src); stream != nil { + if err := Validate(dst); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + } else if err = stream.Publish(dst); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + } else { + http.Error(w, "", http.StatusNotFound) + } + } else { + http.Error(w, "", http.StatusBadRequest) + } + + case "DELETE": + delete(streams, src) + + if err := app.PatchConfig(src, nil, "streams"); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + } + } +} diff --git a/internal/streams/streams.go b/internal/streams/streams.go index c676fe09..6d6fa773 100644 --- a/internal/streams/streams.go +++ b/internal/streams/streams.go @@ -2,7 +2,6 @@ package streams import ( "errors" - "net/http" "net/url" "regexp" "sync" @@ -10,8 +9,6 @@ import ( "github.com/AlexxIT/go2rtc/internal/api" "github.com/AlexxIT/go2rtc/internal/app" - "github.com/AlexxIT/go2rtc/pkg/probe" - "github.com/AlexxIT/go2rtc/pkg/tcp" "github.com/rs/zerolog" ) @@ -29,7 +26,7 @@ func Init() { streams[name] = NewStream(item) } - api.HandleFunc("api/streams", streamsHandler) + api.HandleFunc("api/streams", apiStreams) if cfg.Publish == nil { return @@ -145,101 +142,6 @@ func Delete(id string) { delete(streams, id) } -func streamsHandler(w http.ResponseWriter, r *http.Request) { - query := r.URL.Query() - src := query.Get("src") - - // without source - return all streams list - if src == "" && r.Method != "POST" { - api.ResponseJSON(w, streams) - return - } - - // Not sure about all this API. Should be rewrited... - switch r.Method { - case "GET": - stream := Get(src) - if stream == nil { - http.Error(w, "", http.StatusNotFound) - return - } - - cons := probe.NewProbe(query) - if len(cons.Medias) != 0 { - cons.RemoteAddr = tcp.RemoteAddr(r) - cons.UserAgent = r.UserAgent() - if err := stream.AddConsumer(cons); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - api.ResponsePrettyJSON(w, stream) - - stream.RemoveConsumer(cons) - } else { - api.ResponsePrettyJSON(w, streams[src]) - } - - case "PUT": - name := query.Get("name") - if name == "" { - name = src - } - - if New(name, src) == nil { - http.Error(w, "", http.StatusBadRequest) - return - } - - if err := app.PatchConfig(name, src, "streams"); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - } - - case "PATCH": - name := query.Get("name") - if name == "" { - http.Error(w, "", http.StatusBadRequest) - return - } - - // support {input} templates: https://github.com/AlexxIT/go2rtc#module-hass - if Patch(name, src) == nil { - http.Error(w, "", http.StatusBadRequest) - } - - case "POST": - // with dst - redirect source to dst - if dst := query.Get("dst"); dst != "" { - if stream := Get(dst); stream != nil { - if err := Validate(src); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - } else if err = stream.Play(src); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } else { - api.ResponseJSON(w, stream) - } - } else if stream = Get(src); stream != nil { - if err := Validate(dst); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - } else if err = stream.Publish(dst); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } - } else { - http.Error(w, "", http.StatusNotFound) - } - } else { - http.Error(w, "", http.StatusBadRequest) - } - - case "DELETE": - delete(streams, src) - - if err := app.PatchConfig(src, nil, "streams"); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - } - } -} - var log zerolog.Logger var streams = map[string]*Stream{} var streamsMu sync.Mutex From 0667683e4d6b25dd17d5e182f255b71989e4e133 Mon Sep 17 00:00:00 2001 From: Alex X Date: Fri, 7 Jun 2024 17:57:36 +0300 Subject: [PATCH 020/166] Restore support old cipher suites after go1.22 #1172 --- pkg/tcp/request.go | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/pkg/tcp/request.go b/pkg/tcp/request.go index 88da83b6..13463cd7 100644 --- a/pkg/tcp/request.go +++ b/pkg/tcp/request.go @@ -19,11 +19,11 @@ func Do(req *http.Request) (*http.Response, error) { switch req.URL.Scheme { case "httpx": - secure = &tls.Config{InsecureSkipVerify: true} + secure = insecureConfig req.URL.Scheme = "https" case "https": if hostname := req.URL.Hostname(); IsIP(hostname) { - secure = &tls.Config{InsecureSkipVerify: true} + secure = insecureConfig } } @@ -144,6 +144,22 @@ type key string var connKey = key("conn") var secureKey = key("secure") +var insecureConfig = &tls.Config{ + InsecureSkipVerify: true, + CipherSuites: []uint16{ + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, + tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, + tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, + + // this cipher suites disabled starting from https://tip.golang.org/doc/go1.22 + // but cameras can't work without them https://github.com/AlexxIT/go2rtc/issues/1172 + tls.TLS_RSA_WITH_AES_128_GCM_SHA256, // insecure + tls.TLS_RSA_WITH_AES_256_GCM_SHA384, // insecure + }, +} + func WithConn() (context.Context, *net.Conn) { pconn := new(net.Conn) return context.WithValue(context.Background(), connKey, pconn), pconn From 03956968667bba2febb6a513d159dc9a00b3e4c8 Mon Sep 17 00:00:00 2001 From: Alex X Date: Fri, 7 Jun 2024 17:59:21 +0300 Subject: [PATCH 021/166] Fix exec pipe output --- internal/exec/exec.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/exec/exec.go b/internal/exec/exec.go index d30b0dbe..6c41fe9e 100644 --- a/internal/exec/exec.go +++ b/internal/exec/exec.go @@ -101,11 +101,12 @@ func handlePipe(_ string, cmd *exec.Cmd, query url.Values) (core.Producer, error prod, err := magic.Open(r) if err != nil { _ = r.Close() + return nil, fmt.Errorf("exec/pipe: %w\n%s", err, cmd.Stderr) } log.Debug().Stringer("launch", time.Since(ts)).Msg("[exec] run pipe") - return prod, fmt.Errorf("exec/pipe: %w\n%s", err, cmd.Stderr) + return prod, nil } func handleRTSP(url string, cmd *exec.Cmd, path string) (core.Producer, error) { From 72d7e8aaaa5bcbaf9e788522c0bd22fb47d44cdb Mon Sep 17 00:00:00 2001 From: Sergey Krashevich Date: Sat, 8 Jun 2024 15:05:26 +0300 Subject: [PATCH 022/166] refactor(app): remove syscall import and improve error messages --- internal/app/app.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/app/app.go b/internal/app/app.go index ab4b6c94..cdbb870b 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -7,7 +7,6 @@ import ( "os/exec" "runtime" "runtime/debug" - "syscall" ) var ( @@ -46,10 +45,11 @@ func Init() { os.Exit(0) } - if os.Getppid() == 1 || syscall.Getppid() == 1 { + ppid := os.Getppid() + if ppid == 1 { daemon = false } else { - parent, err := os.FindProcess(os.Getppid()) + parent, err := os.FindProcess(ppid) if err != nil || parent.Pid < 1 { daemon = false } @@ -57,14 +57,14 @@ func Init() { if daemon { if runtime.GOOS == "windows" { - fmt.Println("Daemon not supported on Windows") + fmt.Println("Daemon mode is not supported on Windows") os.Exit(1) } // Re-run the program in background and exit cmd := exec.Command(os.Args[0], os.Args[1:]...) if err := cmd.Start(); err != nil { - fmt.Println(err) + fmt.Println("Failed to start daemon:", err) os.Exit(1) } fmt.Println("Running in daemon mode with PID:", cmd.Process.Pid) From 1ac9d54dab4911776d22ecadb281f52ca193dcef Mon Sep 17 00:00:00 2001 From: Alex X Date: Mon, 10 Jun 2024 16:42:34 +0300 Subject: [PATCH 023/166] Code refactoring for stream MarshalJSON --- internal/streams/producer.go | 7 +++---- internal/streams/stream.go | 15 ++++----------- 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/internal/streams/producer.go b/internal/streams/producer.go index daca7edf..09e2dcc5 100644 --- a/internal/streams/producer.go +++ b/internal/streams/producer.go @@ -132,11 +132,10 @@ func (p *Producer) AddTrack(media *core.Media, codec *core.Codec, track *core.Re } func (p *Producer) MarshalJSON() ([]byte, error) { - if p.conn != nil { - return json.Marshal(p.conn) + if conn := p.conn; conn != nil { + return json.Marshal(conn) } - - info := core.Info{URL: p.url} + info := map[string]string{"url": p.url} return json.Marshal(info) } diff --git a/internal/streams/stream.go b/internal/streams/stream.go index 5dacf991..bb832694 100644 --- a/internal/streams/stream.go +++ b/internal/streams/stream.go @@ -112,19 +112,12 @@ producers: } func (s *Stream) MarshalJSON() ([]byte, error) { - if !s.mu.TryLock() { - log.Warn().Msgf("[streams] json locked") - return json.Marshal(nil) - } - - var info struct { + var info = struct { Producers []*Producer `json:"producers"` Consumers []core.Consumer `json:"consumers"` + }{ + Producers: s.producers, + Consumers: s.consumers, } - info.Producers = s.producers - info.Consumers = s.consumers - - s.mu.Unlock() - return json.Marshal(info) } From ecfe802065fdcdef770a0ed49aea24339a807212 Mon Sep 17 00:00:00 2001 From: Alex X Date: Fri, 14 Jun 2024 12:48:29 +0300 Subject: [PATCH 024/166] Code refactoring for streams HandleFunc --- internal/bubble/bubble.go | 12 +++--------- internal/debug/debug.go | 8 -------- internal/dvrip/dvrip.go | 11 +---------- internal/gopro/gopro.go | 8 +++----- internal/hass/hass.go | 9 ++------- internal/isapi/init.go | 15 +++------------ internal/ivideon/ivideon.go | 10 ++-------- internal/nest/init.go | 12 +++--------- internal/roborock/roborock.go | 15 +++------------ internal/tapo/tapo.go | 8 ++++---- pkg/bubble/client.go | 8 ++++++-- pkg/isapi/client.go | 8 ++++++-- pkg/ivideon/client.go | 9 +++++++-- pkg/nest/client.go | 2 +- pkg/roborock/client.go | 11 +++++++++-- 15 files changed, 53 insertions(+), 93 deletions(-) diff --git a/internal/bubble/bubble.go b/internal/bubble/bubble.go index 65d0237e..6c526fc5 100644 --- a/internal/bubble/bubble.go +++ b/internal/bubble/bubble.go @@ -7,13 +7,7 @@ import ( ) func Init() { - streams.HandleFunc("bubble", handle) -} - -func handle(url string) (core.Producer, error) { - conn := bubble.NewClient(url) - if err := conn.Dial(); err != nil { - return nil, err - } - return conn, nil + streams.HandleFunc("bubble", func(source string) (core.Producer, error) { + return bubble.Dial(source) + }) } diff --git a/internal/debug/debug.go b/internal/debug/debug.go index 3d40d1f1..fc7d2453 100644 --- a/internal/debug/debug.go +++ b/internal/debug/debug.go @@ -2,16 +2,8 @@ package debug import ( "github.com/AlexxIT/go2rtc/internal/api" - "github.com/AlexxIT/go2rtc/internal/streams" - "github.com/AlexxIT/go2rtc/pkg/core" ) func Init() { api.HandleFunc("api/stack", stackHandler) - - streams.HandleFunc("null", nullHandler) -} - -func nullHandler(string) (core.Producer, error) { - return nil, nil } diff --git a/internal/dvrip/dvrip.go b/internal/dvrip/dvrip.go index 095372d2..db1c60db 100644 --- a/internal/dvrip/dvrip.go +++ b/internal/dvrip/dvrip.go @@ -10,25 +10,16 @@ import ( "github.com/AlexxIT/go2rtc/internal/api" "github.com/AlexxIT/go2rtc/internal/streams" - "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/dvrip" ) func Init() { - streams.HandleFunc("dvrip", handle) + streams.HandleFunc("dvrip", dvrip.Dial) // DVRIP client autodiscovery api.HandleFunc("api/dvrip", apiDvrip) } -func handle(url string) (core.Producer, error) { - client, err := dvrip.Dial(url) - if err != nil { - return nil, err - } - return client, nil -} - const Port = 34569 // UDP port number for dvrip discovery func apiDvrip(w http.ResponseWriter, r *http.Request) { diff --git a/internal/gopro/gopro.go b/internal/gopro/gopro.go index 55d2641b..ee578049 100644 --- a/internal/gopro/gopro.go +++ b/internal/gopro/gopro.go @@ -10,15 +10,13 @@ import ( ) func Init() { - streams.HandleFunc("gopro", handleGoPro) + streams.HandleFunc("gopro", func(source string) (core.Producer, error) { + return gopro.Dial(source) + }) api.HandleFunc("api/gopro", apiGoPro) } -func handleGoPro(rawURL string) (core.Producer, error) { - return gopro.Dial(rawURL) -} - func apiGoPro(w http.ResponseWriter, r *http.Request) { var items []*api.Source diff --git a/internal/hass/hass.go b/internal/hass/hass.go index cd95ffe1..ea172b02 100644 --- a/internal/hass/hass.go +++ b/internal/hass/hass.go @@ -45,14 +45,9 @@ func Init() { return "", nil }) - streams.HandleFunc("hass", func(url string) (core.Producer, error) { + streams.HandleFunc("hass", func(source string) (core.Producer, error) { // support hass://supervisor?entity_id=camera.driveway_doorbell - client, err := hass.NewClient(url) - if err != nil { - return nil, err - } - - return client, nil + return hass.NewClient(source) }) // load static entries from Hass config diff --git a/internal/isapi/init.go b/internal/isapi/init.go index a37afa23..887a6748 100644 --- a/internal/isapi/init.go +++ b/internal/isapi/init.go @@ -7,16 +7,7 @@ import ( ) func Init() { - streams.HandleFunc("isapi", handle) -} - -func handle(url string) (core.Producer, error) { - conn, err := isapi.NewClient(url) - if err != nil { - return nil, err - } - if err = conn.Dial(); err != nil { - return nil, err - } - return conn, nil + streams.HandleFunc("isapi", func(source string) (core.Producer, error) { + return isapi.Dial(source) + }) } diff --git a/internal/ivideon/ivideon.go b/internal/ivideon/ivideon.go index 0ae5dc9f..03feb742 100644 --- a/internal/ivideon/ivideon.go +++ b/internal/ivideon/ivideon.go @@ -4,16 +4,10 @@ import ( "github.com/AlexxIT/go2rtc/internal/streams" "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/ivideon" - "strings" ) func Init() { - streams.HandleFunc("ivideon", func(url string) (core.Producer, error) { - id := strings.Replace(url[8:], "/", ":", 1) - prod := ivideon.NewClient(id) - if err := prod.Dial(); err != nil { - return nil, err - } - return prod, nil + streams.HandleFunc("ivideon", func(source string) (core.Producer, error) { + return ivideon.Dial(source) }) } diff --git a/internal/nest/init.go b/internal/nest/init.go index 1281ccdc..01682414 100644 --- a/internal/nest/init.go +++ b/internal/nest/init.go @@ -10,19 +10,13 @@ import ( ) func Init() { - streams.HandleFunc("nest", streamNest) + streams.HandleFunc("nest", func(source string) (core.Producer, error) { + return nest.Dial(source) + }) api.HandleFunc("api/nest", apiNest) } -func streamNest(url string) (core.Producer, error) { - client, err := nest.NewClient(url) - if err != nil { - return nil, err - } - return client, nil -} - func apiNest(w http.ResponseWriter, r *http.Request) { query := r.URL.Query() cliendID := query.Get("client_id") diff --git a/internal/roborock/roborock.go b/internal/roborock/roborock.go index 27e29bb5..32a436d8 100644 --- a/internal/roborock/roborock.go +++ b/internal/roborock/roborock.go @@ -11,22 +11,13 @@ import ( ) func Init() { - streams.HandleFunc("roborock", handle) + streams.HandleFunc("roborock", func(source string) (core.Producer, error) { + return roborock.Dial(source) + }) api.HandleFunc("api/roborock", apiHandle) } -func handle(url string) (core.Producer, error) { - conn := roborock.NewClient(url) - if err := conn.Dial(); err != nil { - return nil, err - } - if err := conn.Connect(); err != nil { - return nil, err - } - return conn, nil -} - var Auth struct { UserData *roborock.UserInfo `json:"user_data"` BaseURL string `json:"base_url"` diff --git a/internal/tapo/tapo.go b/internal/tapo/tapo.go index a54c8c5e..724c9e86 100644 --- a/internal/tapo/tapo.go +++ b/internal/tapo/tapo.go @@ -8,11 +8,11 @@ import ( ) func Init() { - streams.HandleFunc("kasa", func(url string) (core.Producer, error) { - return kasa.Dial(url) + streams.HandleFunc("kasa", func(source string) (core.Producer, error) { + return kasa.Dial(source) }) - streams.HandleFunc("tapo", func(url string) (core.Producer, error) { - return tapo.Dial(url) + streams.HandleFunc("tapo", func(source string) (core.Producer, error) { + return tapo.Dial(source) }) } diff --git a/pkg/bubble/client.go b/pkg/bubble/client.go index b8b77ae9..c0a79701 100644 --- a/pkg/bubble/client.go +++ b/pkg/bubble/client.go @@ -43,8 +43,12 @@ type Client struct { recv int } -func NewClient(url string) *Client { - return &Client{url: url} +func Dial(rawURL string) (*Client, error) { + client := &Client{url: rawURL} + if err := client.Dial(); err != nil { + return nil, err + } + return client, nil } const ( diff --git a/pkg/isapi/client.go b/pkg/isapi/client.go index e5dfafd4..83dd9026 100644 --- a/pkg/isapi/client.go +++ b/pkg/isapi/client.go @@ -23,7 +23,7 @@ type Client struct { send int } -func NewClient(rawURL string) (*Client, error) { +func Dial(rawURL string) (*Client, error) { // check if url is valid url u, err := url.Parse(rawURL) if err != nil { @@ -33,7 +33,11 @@ func NewClient(rawURL string) (*Client, error) { u.Scheme = "http" u.Path = "" - return &Client{url: u.String()}, nil + client := &Client{url: u.String()} + if err = client.Dial(); err != nil { + return nil, err + } + return client, err } func (c *Client) Dial() (err error) { diff --git a/pkg/ivideon/client.go b/pkg/ivideon/client.go index c1b055b8..7cbf0b38 100644 --- a/pkg/ivideon/client.go +++ b/pkg/ivideon/client.go @@ -46,8 +46,13 @@ type Client struct { recv int } -func NewClient(id string) *Client { - return &Client{ID: id} +func Dial(source string) (*Client, error) { + id := strings.Replace(source[8:], "/", ":", 1) + client := &Client{ID: id} + if err := client.Dial(); err != nil { + return nil, err + } + return client, nil } func (c *Client) Dial() (err error) { diff --git a/pkg/nest/client.go b/pkg/nest/client.go index cb73cc98..2169773b 100644 --- a/pkg/nest/client.go +++ b/pkg/nest/client.go @@ -14,7 +14,7 @@ type Client struct { api *API } -func NewClient(rawURL string) (*Client, error) { +func Dial(rawURL string) (*Client, error) { u, err := url.Parse(rawURL) if err != nil { return nil, err diff --git a/pkg/roborock/client.go b/pkg/roborock/client.go index 6a3bf0e0..522b0e13 100644 --- a/pkg/roborock/client.go +++ b/pkg/roborock/client.go @@ -34,8 +34,15 @@ type Client struct { backchannel bool } -func NewClient(url string) *Client { - return &Client{url: url} +func Dial(rawURL string) (*Client, error) { + client := &Client{url: rawURL} + if err := client.Dial(); err != nil { + return nil, err + } + if err := client.Connect(); err != nil { + return nil, err + } + return client, nil } func (c *Client) Dial() error { From 96504e2fb0c89870a6cd18e08d27af8e1cd1b0e8 Mon Sep 17 00:00:00 2001 From: Alex X Date: Sat, 15 Jun 2024 16:46:03 +0300 Subject: [PATCH 025/166] BIG rewrite stream info --- internal/exec/exec.go | 27 ++++- internal/ffmpeg/producer.go | 7 +- internal/hass/api.go | 2 +- internal/hls/hls.go | 11 +- internal/hls/ws.go | 6 +- internal/http/http.go | 29 +++-- internal/mjpeg/init.go | 27 ++--- internal/mp4/mp4.go | 7 +- internal/mp4/ws.go | 10 +- internal/mpegts/aac.go | 4 +- internal/mpegts/mpegts.go | 4 +- internal/rtmp/rtmp.go | 11 +- internal/streams/api.go | 4 +- internal/streams/handlers.go | 2 +- internal/webrtc/client.go | 8 +- internal/webrtc/kinesis.go | 8 +- internal/webrtc/milestone.go | 4 +- internal/webrtc/openipc.go | 4 +- internal/webrtc/server.go | 8 +- internal/webrtc/webrtc.go | 5 +- internal/webtorrent/init.go | 2 +- pkg/README.md | 82 +++++++++++++ pkg/aac/consumer.go | 23 ++-- pkg/aac/producer.go | 26 ++-- pkg/bubble/client.go | 1 + pkg/bubble/producer.go | 15 ++- pkg/core/codec.go | 2 +- pkg/core/connection.go | 139 ++++++++++++++++++++++ pkg/core/core.go | 89 +------------- pkg/core/media.go | 2 +- pkg/core/node.go | 7 +- pkg/core/track.go | 45 ++++++- pkg/dvrip/{consumer.go => backchannel.go} | 15 +-- pkg/dvrip/dvrip.go | 17 ++- pkg/dvrip/producer.go | 6 +- pkg/flv/consumer.go | 25 ++-- pkg/flv/producer.go | 19 +-- pkg/gopro/{gopro.go => producer.go} | 13 +- pkg/hass/client.go | 4 +- pkg/hls/producer.go | 11 +- pkg/homekit/consumer.go | 48 ++++---- pkg/homekit/{client.go => producer.go} | 17 +-- pkg/image/producer.go | 92 ++++++++++++++ pkg/isapi/{consumer.go => backchannel.go} | 14 ++- pkg/isapi/client.go | 1 + pkg/ivideon/client.go | 1 + pkg/ivideon/producer.go | 16 ++- pkg/kasa/producer.go | 24 ++-- pkg/magic/bitstream/producer.go | 24 ++-- pkg/magic/keyframe.go | 39 +++--- pkg/magic/mjpeg/producer.go | 21 ++-- pkg/magic/producer.go | 34 +++--- pkg/mjpeg/client.go | 75 ------------ pkg/mjpeg/consumer.go | 37 +++--- pkg/mjpeg/producer.go | 61 ---------- pkg/mp4/consumer.go | 20 ++-- pkg/mp4/keyframe.go | 16 +-- pkg/mpegts/consumer.go | 37 +++--- pkg/mpegts/producer.go | 20 ++-- pkg/{multipart => mpjpeg}/multipart.go | 2 +- pkg/mpjpeg/producer.go | 65 ++++++++++ pkg/multipart/producer.go | 68 ----------- pkg/nest/client.go | 4 +- pkg/probe/{probe.go => producer.go} | 20 ++-- pkg/roborock/client.go | 5 +- pkg/rtmp/client.go | 9 +- pkg/rtmp/flv.go | 15 ++- pkg/rtsp/client.go | 16 ++- pkg/rtsp/conn.go | 20 +--- pkg/rtsp/consumer.go | 17 +-- pkg/rtsp/producer.go | 32 ++--- pkg/rtsp/server.go | 30 +++-- pkg/stdin/{consumer.go => backchannel.go} | 10 +- pkg/stdin/client.go | 1 + pkg/tapo/{consumer.go => backchannel.go} | 0 pkg/tapo/client.go | 1 + pkg/tapo/producer.go | 18 ++- pkg/tcp/helpers.go | 12 -- pkg/wav/{wav.go => producer.go} | 22 ++-- pkg/webrtc/client.go | 2 +- pkg/webrtc/conn.go | 51 +++++--- pkg/webrtc/consumer.go | 23 +--- pkg/webrtc/producer.go | 14 +-- pkg/webrtc/server.go | 4 +- pkg/webtorrent/client.go | 8 +- pkg/y4m/consumer.go | 16 ++- pkg/y4m/producer.go | 55 +++------ pkg/y4m/y4m.go | 29 +++++ 88 files changed, 1043 insertions(+), 854 deletions(-) create mode 100644 pkg/core/connection.go rename pkg/dvrip/{consumer.go => backchannel.go} (78%) rename pkg/gopro/{gopro.go => producer.go} (90%) rename pkg/homekit/{client.go => producer.go} (95%) create mode 100644 pkg/image/producer.go rename pkg/isapi/{consumer.go => backchannel.go} (83%) delete mode 100644 pkg/mjpeg/client.go delete mode 100644 pkg/mjpeg/producer.go rename pkg/{multipart => mpjpeg}/multipart.go (98%) create mode 100644 pkg/mpjpeg/producer.go delete mode 100644 pkg/multipart/producer.go rename pkg/probe/{probe.go => producer.go} (72%) rename pkg/stdin/{consumer.go => backchannel.go} (88%) rename pkg/tapo/{consumer.go => backchannel.go} (100%) delete mode 100644 pkg/tcp/helpers.go rename pkg/wav/{wav.go => producer.go} (89%) diff --git a/internal/exec/exec.go b/internal/exec/exec.go index 6c41fe9e..ac1691d3 100644 --- a/internal/exec/exec.go +++ b/internal/exec/exec.go @@ -8,6 +8,7 @@ import ( "net/url" "os" "os/exec" + "slices" "strings" "sync" "time" @@ -80,7 +81,7 @@ func execHandle(rawURL string) (core.Producer, error) { return handleRTSP(rawURL, cmd, path) } -func handlePipe(_ string, cmd *exec.Cmd, query url.Values) (core.Producer, error) { +func handlePipe(source string, cmd *exec.Cmd, query url.Values) (core.Producer, error) { if query.Get("backchannel") == "1" { return stdin.NewClient(cmd) } @@ -104,12 +105,17 @@ func handlePipe(_ string, cmd *exec.Cmd, query url.Values) (core.Producer, error return nil, fmt.Errorf("exec/pipe: %w\n%s", err, cmd.Stderr) } + if info, ok := prod.(core.Info); ok { + info.SetProtocol("pipe") + setRemoteInfo(info, source, cmd.Args) + } + log.Debug().Stringer("launch", time.Since(ts)).Msg("[exec] run pipe") return prod, nil } -func handleRTSP(url string, cmd *exec.Cmd, path string) (core.Producer, error) { +func handleRTSP(source string, cmd *exec.Cmd, path string) (core.Producer, error) { if log.Trace().Enabled() { cmd.Stdout = os.Stdout } @@ -131,7 +137,7 @@ func handleRTSP(url string, cmd *exec.Cmd, path string) (core.Producer, error) { ts := time.Now() if err := cmd.Start(); err != nil { - log.Error().Err(err).Str("url", url).Msg("[exec]") + log.Error().Err(err).Str("source", source).Msg("[exec]") return nil, err } @@ -143,13 +149,14 @@ func handleRTSP(url string, cmd *exec.Cmd, path string) (core.Producer, error) { select { case <-time.After(time.Second * 60): _ = cmd.Process.Kill() - log.Error().Str("url", url).Msg("[exec] timeout") + log.Error().Str("source", source).Msg("[exec] timeout") return nil, errors.New("exec: timeout") case <-done: // limit message size return nil, fmt.Errorf("exec/rtsp\n%s", cmd.Stderr) case prod := <-waiter: log.Debug().Stringer("launch", time.Since(ts)).Msg("[exec] run rtsp") + setRemoteInfo(prod, source, cmd.Args) prod.OnClose = func() error { log.Debug().Msgf("[exec] kill rtsp") return errors.Join(cmd.Process.Kill(), cmd.Wait()) @@ -210,3 +217,15 @@ func trimSpace(b []byte) []byte { } return b[start:stop] } + +func setRemoteInfo(info core.Info, source string, args []string) { + info.SetSource(source) + + if i := slices.Index(args, "-i"); i > 0 && i < len(args)-1 { + rawURL := args[i+1] + if u, err := url.Parse(rawURL); err == nil && u.Host != "" { + info.SetRemoteAddr(u.Host) + info.SetURL(rawURL) + } + } +} diff --git a/internal/ffmpeg/producer.go b/internal/ffmpeg/producer.go index 05df69e3..d132d253 100644 --- a/internal/ffmpeg/producer.go +++ b/internal/ffmpeg/producer.go @@ -13,7 +13,7 @@ import ( ) type Producer struct { - core.SuperProducer + core.Connection url string query url.Values ffmpeg core.Producer @@ -31,7 +31,8 @@ func NewProducer(url string) (core.Producer, error) { return nil, errors.New("ffmpeg: unsupported params: " + url[i:]) } - p.Type = "FFmpeg producer" + p.ID = core.NewID() + p.FormatName = "ffmpeg" p.Medias = []*core.Media{ { // we can support only audio, because don't know FmtpLine for H264 and PayloadType for MJPEG @@ -81,7 +82,7 @@ func (p *Producer) Stop() error { func (p *Producer) MarshalJSON() ([]byte, error) { if p.ffmpeg == nil { - return json.Marshal(p.SuperProducer) + return json.Marshal(p.Connection) } return json.Marshal(p.ffmpeg) } diff --git a/internal/hass/api.go b/internal/hass/api.go index 4628cc11..e3de23b3 100644 --- a/internal/hass/api.go +++ b/internal/hass/api.go @@ -63,7 +63,7 @@ func apiStream(w http.ResponseWriter, r *http.Request) { return } - s, err = webrtc.ExchangeSDP(stream, string(offer), "WebRTC/Hass sync", r.UserAgent()) + s, err = webrtc.ExchangeSDP(stream, string(offer), "hass/webrtc", r.UserAgent()) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return diff --git a/internal/hls/hls.go b/internal/hls/hls.go index 5d3cd918..5c136450 100644 --- a/internal/hls/hls.go +++ b/internal/hls/hls.go @@ -12,7 +12,6 @@ import ( "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/mp4" "github.com/AlexxIT/go2rtc/pkg/mpegts" - "github.com/AlexxIT/go2rtc/pkg/tcp" "github.com/rs/zerolog" ) @@ -63,15 +62,13 @@ func handlerStream(w http.ResponseWriter, r *http.Request) { medias := mp4.ParseQuery(r.URL.Query()) if medias != nil { c := mp4.NewConsumer(medias) - c.Type = "HLS/fMP4 consumer" - c.RemoteAddr = tcp.RemoteAddr(r) - c.UserAgent = r.UserAgent() + c.FormatName = "hls/fmp4" + c.WithRequest(r) cons = c } else { c := mpegts.NewConsumer() - c.Type = "HLS/TS consumer" - c.RemoteAddr = tcp.RemoteAddr(r) - c.UserAgent = r.UserAgent() + c.FormatName = "hls/mpegts" + c.WithRequest(r) cons = c } diff --git a/internal/hls/ws.go b/internal/hls/ws.go index ea1f5a3a..608f515f 100644 --- a/internal/hls/ws.go +++ b/internal/hls/ws.go @@ -8,7 +8,6 @@ import ( "github.com/AlexxIT/go2rtc/internal/api/ws" "github.com/AlexxIT/go2rtc/internal/streams" "github.com/AlexxIT/go2rtc/pkg/mp4" - "github.com/AlexxIT/go2rtc/pkg/tcp" ) func handlerWSHLS(tr *ws.Transport, msg *ws.Message) error { @@ -20,9 +19,8 @@ func handlerWSHLS(tr *ws.Transport, msg *ws.Message) error { codecs := msg.String() medias := mp4.ParseCodecs(codecs, true) cons := mp4.NewConsumer(medias) - cons.Type = "HLS/fMP4 consumer" - cons.RemoteAddr = tcp.RemoteAddr(tr.Request) - cons.UserAgent = tr.Request.UserAgent() + cons.FormatName = "hls/fmp4" + cons.WithRequest(tr.Request) log.Trace().Msgf("[hls] new ws consumer codecs=%s", codecs) diff --git a/internal/http/http.go b/internal/http/http.go index 8b1903f3..a35439d5 100644 --- a/internal/http/http.go +++ b/internal/http/http.go @@ -11,9 +11,9 @@ import ( "github.com/AlexxIT/go2rtc/internal/streams" "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/hls" + "github.com/AlexxIT/go2rtc/pkg/image" "github.com/AlexxIT/go2rtc/pkg/magic" - "github.com/AlexxIT/go2rtc/pkg/mjpeg" - "github.com/AlexxIT/go2rtc/pkg/multipart" + "github.com/AlexxIT/go2rtc/pkg/mpjpeg" "github.com/AlexxIT/go2rtc/pkg/tcp" ) @@ -45,6 +45,21 @@ func handleHTTP(rawURL string) (core.Producer, error) { } } + prod, err := do(req) + if err != nil { + return nil, err + } + + if info, ok := prod.(core.Info); ok { + info.SetProtocol("http") + info.SetRemoteAddr(req.URL.Host) // TODO: rewrite to net.Conn + info.SetURL(rawURL) + } + + return prod, nil +} + +func do(req *http.Request) (core.Producer, error) { res, err := tcp.Do(req) if err != nil { return nil, err @@ -66,14 +81,12 @@ func handleHTTP(rawURL string) (core.Producer, error) { } switch { - case ct == "image/jpeg": - return mjpeg.NewClient(res), nil - - case ct == "multipart/x-mixed-replace": - return multipart.Open(res.Body) - case ct == "application/vnd.apple.mpegurl" || ext == "m3u8": return hls.OpenURL(req.URL, res.Body) + case ct == "image/jpeg": + return image.Open(res) + case ct == "multipart/x-mixed-replace": + return mpjpeg.Open(res.Body) } return magic.Open(res.Body) diff --git a/internal/mjpeg/init.go b/internal/mjpeg/init.go index 0bed95c6..2bb7093a 100644 --- a/internal/mjpeg/init.go +++ b/internal/mjpeg/init.go @@ -17,7 +17,7 @@ import ( "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/magic" "github.com/AlexxIT/go2rtc/pkg/mjpeg" - "github.com/AlexxIT/go2rtc/pkg/tcp" + "github.com/AlexxIT/go2rtc/pkg/mpjpeg" "github.com/AlexxIT/go2rtc/pkg/y4m" "github.com/rs/zerolog" ) @@ -44,8 +44,7 @@ func handlerKeyframe(w http.ResponseWriter, r *http.Request) { } cons := magic.NewKeyframe() - cons.RemoteAddr = tcp.RemoteAddr(r) - cons.UserAgent = r.UserAgent() + cons.WithRequest(r) if err := stream.AddConsumer(cons); err != nil { log.Error().Err(err).Caller().Send() @@ -100,8 +99,7 @@ func outputMjpeg(w http.ResponseWriter, r *http.Request) { } cons := mjpeg.NewConsumer() - cons.RemoteAddr = tcp.RemoteAddr(r) - cons.UserAgent = r.UserAgent() + cons.WithRequest(r) if err := stream.AddConsumer(cons); err != nil { log.Error().Err(err).Msg("[api.mjpeg] add consumer") @@ -117,7 +115,7 @@ func outputMjpeg(w http.ResponseWriter, r *http.Request) { wr := mjpeg.NewWriter(w) _, _ = cons.WriteTo(wr) } else { - cons.Type = "ASCII passive consumer " + cons.FormatName = "ascii" query := r.URL.Query() wr := ascii.NewWriter(w, query.Get("color"), query.Get("back"), query.Get("text")) @@ -135,17 +133,16 @@ func inputMjpeg(w http.ResponseWriter, r *http.Request) { return } - res := &http.Response{Body: r.Body, Header: r.Header, Request: r} - res.Header.Set("Content-Type", "multipart/mixed;boundary=") + prod, _ := mpjpeg.Open(r.Body) + prod.WithRequest(r) - client := mjpeg.NewClient(res) - stream.AddProducer(client) + stream.AddProducer(prod) - if err := client.Start(); err != nil && err != io.EOF { + if err := prod.Start(); err != nil && err != io.EOF { log.Warn().Err(err).Caller().Send() } - stream.RemoveProducer(client) + stream.RemoveProducer(prod) } func handlerWS(tr *ws.Transport, _ *ws.Message) error { @@ -155,8 +152,7 @@ func handlerWS(tr *ws.Transport, _ *ws.Message) error { } cons := mjpeg.NewConsumer() - cons.RemoteAddr = tcp.RemoteAddr(tr.Request) - cons.UserAgent = tr.Request.UserAgent() + cons.WithRequest(tr.Request) if err := stream.AddConsumer(cons); err != nil { log.Debug().Err(err).Msg("[mjpeg] add consumer") @@ -183,8 +179,7 @@ func apiStreamY4M(w http.ResponseWriter, r *http.Request) { } cons := y4m.NewConsumer() - cons.RemoteAddr = tcp.RemoteAddr(r) - cons.UserAgent = r.UserAgent() + cons.WithRequest(r) if err := stream.AddConsumer(cons); err != nil { log.Error().Err(err).Caller().Send() diff --git a/internal/mp4/mp4.go b/internal/mp4/mp4.go index 2f59ba04..cca5220c 100644 --- a/internal/mp4/mp4.go +++ b/internal/mp4/mp4.go @@ -13,7 +13,6 @@ import ( "github.com/AlexxIT/go2rtc/internal/streams" "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/mp4" - "github.com/AlexxIT/go2rtc/pkg/tcp" "github.com/rs/zerolog" ) @@ -100,9 +99,9 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) { medias := mp4.ParseQuery(r.URL.Query()) cons := mp4.NewConsumer(medias) - cons.Type = "MP4/HTTP active consumer" - cons.RemoteAddr = tcp.RemoteAddr(r) - cons.UserAgent = r.UserAgent() + cons.FormatName = "mp4" + cons.Protocol = "http" + cons.WithRequest(r) if err := stream.AddConsumer(cons); err != nil { log.Error().Err(err).Caller().Send() diff --git a/internal/mp4/ws.go b/internal/mp4/ws.go index 060ff5f6..c880fb58 100644 --- a/internal/mp4/ws.go +++ b/internal/mp4/ws.go @@ -8,7 +8,6 @@ import ( "github.com/AlexxIT/go2rtc/internal/streams" "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/mp4" - "github.com/AlexxIT/go2rtc/pkg/tcp" ) func handlerWSMSE(tr *ws.Transport, msg *ws.Message) error { @@ -24,9 +23,8 @@ func handlerWSMSE(tr *ws.Transport, msg *ws.Message) error { } cons := mp4.NewConsumer(medias) - cons.Type = "MSE/WebSocket active consumer" - cons.RemoteAddr = tcp.RemoteAddr(tr.Request) - cons.UserAgent = tr.Request.UserAgent() + cons.FormatName = "mse/fmp4" + cons.WithRequest(tr.Request) if err := stream.AddConsumer(cons); err != nil { log.Debug().Err(err).Msg("[mp4] add consumer") @@ -57,9 +55,7 @@ func handlerWSMP4(tr *ws.Transport, msg *ws.Message) error { } cons := mp4.NewKeyframe(medias) - cons.Type = "MP4/WebSocket active consumer" - cons.RemoteAddr = tcp.RemoteAddr(tr.Request) - cons.UserAgent = tr.Request.UserAgent() + cons.WithRequest(tr.Request) if err := stream.AddConsumer(cons); err != nil { log.Error().Err(err).Caller().Send() diff --git a/internal/mpegts/aac.go b/internal/mpegts/aac.go index 867dc971..3b1522fe 100644 --- a/internal/mpegts/aac.go +++ b/internal/mpegts/aac.go @@ -6,7 +6,6 @@ import ( "github.com/AlexxIT/go2rtc/internal/api" "github.com/AlexxIT/go2rtc/internal/streams" "github.com/AlexxIT/go2rtc/pkg/aac" - "github.com/AlexxIT/go2rtc/pkg/tcp" ) func apiStreamAAC(w http.ResponseWriter, r *http.Request) { @@ -18,8 +17,7 @@ func apiStreamAAC(w http.ResponseWriter, r *http.Request) { } cons := aac.NewConsumer() - cons.RemoteAddr = tcp.RemoteAddr(r) - cons.UserAgent = r.UserAgent() + cons.WithRequest(r) if err := stream.AddConsumer(cons); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) diff --git a/internal/mpegts/mpegts.go b/internal/mpegts/mpegts.go index 6ef00ba1..d5f7752b 100644 --- a/internal/mpegts/mpegts.go +++ b/internal/mpegts/mpegts.go @@ -6,7 +6,6 @@ import ( "github.com/AlexxIT/go2rtc/internal/api" "github.com/AlexxIT/go2rtc/internal/streams" "github.com/AlexxIT/go2rtc/pkg/mpegts" - "github.com/AlexxIT/go2rtc/pkg/tcp" ) func Init() { @@ -31,8 +30,7 @@ func outputMpegTS(w http.ResponseWriter, r *http.Request) { } cons := mpegts.NewConsumer() - cons.RemoteAddr = tcp.RemoteAddr(r) - cons.UserAgent = r.UserAgent() + cons.WithRequest(r) if err := stream.AddConsumer(cons); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) diff --git a/internal/rtmp/rtmp.go b/internal/rtmp/rtmp.go index 07aa5f71..afc363a9 100644 --- a/internal/rtmp/rtmp.go +++ b/internal/rtmp/rtmp.go @@ -12,7 +12,6 @@ import ( "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/flv" "github.com/AlexxIT/go2rtc/pkg/rtmp" - "github.com/AlexxIT/go2rtc/pkg/tcp" "github.com/rs/zerolog" ) @@ -128,11 +127,7 @@ func tcpHandle(netConn net.Conn) error { var log zerolog.Logger func streamsHandle(url string) (core.Producer, error) { - client, err := rtmp.DialPlay(url) - if err != nil { - return nil, err - } - return client, nil + return rtmp.DialPlay(url) } func streamsConsumerHandle(url string) (core.Consumer, func(), error) { @@ -165,9 +160,7 @@ func outputFLV(w http.ResponseWriter, r *http.Request) { } cons := flv.NewConsumer() - cons.Type = "HTTP-FLV consumer" - cons.RemoteAddr = tcp.RemoteAddr(r) - cons.UserAgent = r.UserAgent() + cons.WithRequest(r) if err := stream.AddConsumer(cons); err != nil { log.Error().Err(err).Caller().Send() diff --git a/internal/streams/api.go b/internal/streams/api.go index 72099425..69d2276a 100644 --- a/internal/streams/api.go +++ b/internal/streams/api.go @@ -6,7 +6,6 @@ import ( "github.com/AlexxIT/go2rtc/internal/api" "github.com/AlexxIT/go2rtc/internal/app" "github.com/AlexxIT/go2rtc/pkg/probe" - "github.com/AlexxIT/go2rtc/pkg/tcp" ) func apiStreams(w http.ResponseWriter, r *http.Request) { @@ -30,8 +29,7 @@ func apiStreams(w http.ResponseWriter, r *http.Request) { cons := probe.NewProbe(query) if len(cons.Medias) != 0 { - cons.RemoteAddr = tcp.RemoteAddr(r) - cons.UserAgent = r.UserAgent() + cons.WithRequest(r) if err := stream.AddConsumer(cons); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return diff --git a/internal/streams/handlers.go b/internal/streams/handlers.go index 3009dd66..3240abb5 100644 --- a/internal/streams/handlers.go +++ b/internal/streams/handlers.go @@ -7,7 +7,7 @@ import ( "github.com/AlexxIT/go2rtc/pkg/core" ) -type Handler func(url string) (core.Producer, error) +type Handler func(source string) (core.Producer, error) var handlers = map[string]Handler{} diff --git a/internal/webrtc/client.go b/internal/webrtc/client.go index ae1a455b..4b8b1b9a 100644 --- a/internal/webrtc/client.go +++ b/internal/webrtc/client.go @@ -41,7 +41,7 @@ func streamsHandler(rawURL string) (core.Producer, error) { // https://aws.amazon.com/kinesis/video-streams/ // https://docs.aws.amazon.com/kinesisvideostreams-webrtc-dg/latest/devguide/what-is-kvswebrtc.html // https://github.com/orgs/awslabs/repositories?q=kinesis+webrtc - return kinesisClient(rawURL, query, "WebRTC/Kinesis") + return kinesisClient(rawURL, query, "webrtc/kinesis") } else if format == "openipc" { return openIPCClient(rawURL, query) } else { @@ -86,8 +86,9 @@ func go2rtcClient(url string) (core.Producer, error) { var connMu sync.Mutex prod := webrtc.NewConn(pc) - prod.Desc = "WebRTC/WebSocket async" prod.Mode = core.ModeActiveProducer + prod.Protocol = "ws" + prod.URL = url prod.Listen(func(msg any) { switch msg := msg.(type) { case *pion.ICECandidate: @@ -180,8 +181,9 @@ func whepClient(url string) (core.Producer, error) { } prod := webrtc.NewConn(pc) - prod.Desc = "WebRTC/WHEP sync" prod.Mode = core.ModeActiveProducer + prod.Protocol = "http" + prod.URL = url medias := []*core.Media{ {Kind: core.KindVideo, Direction: core.DirectionRecvonly}, diff --git a/internal/webrtc/kinesis.go b/internal/webrtc/kinesis.go index 7ef9d9bb..2ea1cf7a 100644 --- a/internal/webrtc/kinesis.go +++ b/internal/webrtc/kinesis.go @@ -34,7 +34,7 @@ func (k kinesisResponse) String() string { return fmt.Sprintf("type=%s, payload=%s", k.Type, k.Payload) } -func kinesisClient(rawURL string, query url.Values, desc string) (core.Producer, error) { +func kinesisClient(rawURL string, query url.Values, format string) (core.Producer, error) { // 1. Connect to signalign server conn, _, err := websocket.DefaultDialer.Dial(rawURL, nil) if err != nil { @@ -79,8 +79,10 @@ func kinesisClient(rawURL string, query url.Values, desc string) (core.Producer, } prod := webrtc.NewConn(pc) - prod.Desc = desc + prod.FormatName = format prod.Mode = core.ModeActiveProducer + prod.Protocol = "ws" + prod.URL = rawURL prod.Listen(func(msg any) { switch msg := msg.(type) { case *pion.ICECandidate: @@ -216,5 +218,5 @@ func wyzeClient(rawURL string) (core.Producer, error) { "ice_servers": []string{string(kvs.Servers)}, } - return kinesisClient(kvs.URL, query, "WebRTC/Wyze") + return kinesisClient(kvs.URL, query, "webrtc/wyze") } diff --git a/internal/webrtc/milestone.go b/internal/webrtc/milestone.go index b4e695c9..6a696cb0 100644 --- a/internal/webrtc/milestone.go +++ b/internal/webrtc/milestone.go @@ -193,8 +193,10 @@ func milestoneClient(rawURL string, query url.Values) (core.Producer, error) { } prod := webrtc.NewConn(pc) - prod.Desc = "WebRTC/Milestone" + prod.FormatName = "webrtc/milestone" prod.Mode = core.ModeActiveProducer + prod.Protocol = "http" + prod.URL = rawURL offer, err := mc.GetOffer() if err != nil { diff --git a/internal/webrtc/openipc.go b/internal/webrtc/openipc.go index 8055ea91..8a951d04 100644 --- a/internal/webrtc/openipc.go +++ b/internal/webrtc/openipc.go @@ -53,8 +53,10 @@ func openIPCClient(rawURL string, query url.Values) (core.Producer, error) { var connState core.Waiter prod := webrtc.NewConn(pc) - prod.Desc = "WebRTC/OpenIPC" + prod.FormatName = "webrtc/openipc" prod.Mode = core.ModeActiveProducer + prod.Protocol = "ws" + prod.URL = rawURL prod.Listen(func(msg any) { switch msg := msg.(type) { case *pion.ICECandidate: diff --git a/internal/webrtc/server.go b/internal/webrtc/server.go index fcb72b85..91a237db 100644 --- a/internal/webrtc/server.go +++ b/internal/webrtc/server.go @@ -100,11 +100,11 @@ func outputWebRTC(w http.ResponseWriter, r *http.Request) { switch mediaType { case "application/json": - desc = "WebRTC/JSON sync" + desc = "webrtc/json" case MimeSDP: - desc = "WebRTC/WHEP sync" + desc = "webrtc/whep" default: - desc = "WebRTC/HTTP sync" + desc = "webrtc/post" } answer, err := ExchangeSDP(stream, offer, desc, r.UserAgent()) @@ -168,8 +168,8 @@ func inputWebRTC(w http.ResponseWriter, r *http.Request) { // create new webrtc instance prod := webrtc.NewConn(pc) - prod.Desc = "WebRTC/WHIP sync" prod.Mode = core.ModePassiveProducer + prod.Protocol = "http" prod.UserAgent = r.UserAgent() if err = prod.SetOffer(string(offer)); err != nil { diff --git a/internal/webrtc/webrtc.go b/internal/webrtc/webrtc.go index cabd88b7..8b4943c3 100644 --- a/internal/webrtc/webrtc.go +++ b/internal/webrtc/webrtc.go @@ -117,8 +117,8 @@ func asyncHandler(tr *ws.Transport, msg *ws.Message) error { defer sendAnswer.Done(nil) conn := webrtc.NewConn(pc) - conn.Desc = "WebRTC/WebSocket async" conn.Mode = mode + conn.Protocol = "ws" conn.UserAgent = tr.Request.UserAgent() conn.Listen(func(msg any) { switch msg := msg.(type) { @@ -207,8 +207,9 @@ func ExchangeSDP(stream *streams.Stream, offer, desc, userAgent string) (answer // create new webrtc instance conn := webrtc.NewConn(pc) - conn.Desc = desc + conn.FormatName = desc conn.UserAgent = userAgent + conn.Protocol = "http" conn.Listen(func(msg any) { switch msg := msg.(type) { case pion.PeerConnectionState: diff --git a/internal/webtorrent/init.go b/internal/webtorrent/init.go index 25b7ef9b..b1c25c76 100644 --- a/internal/webtorrent/init.go +++ b/internal/webtorrent/init.go @@ -47,7 +47,7 @@ func Init() { if stream == nil { return "", errors.New(api.StreamNotFound) } - return webrtc.ExchangeSDP(stream, offer, "WebRTC/WebTorrent sync", "") + return webrtc.ExchangeSDP(stream, offer, "webtorrent", "") }, } diff --git a/pkg/README.md b/pkg/README.md index c875dc35..b12f0a70 100644 --- a/pkg/README.md +++ b/pkg/README.md @@ -1,3 +1,85 @@ +# Notes + +go2rtc tries to name formats, protocols and codecs the same way they are named in FFmpeg. +Some formats and protocols go2rtc supports exclusively. They have no equivalent in FFmpeg. + +## Producers (input) + +- The initiator of the connection can be go2rtc - **Source protocols** +- The initiator of the connection can be an external program - **Ingress protocols** +- Codecs can be incoming - **Recevers codecs** +- Codecs can be outgoing (two way audio) - **Senders codecs** + +| Format | Source protocols | Ingress protocols | Recevers codecs | Senders codecs | Example | +|--------------|------------------|-------------------|------------------------------|--------------------|---------------| +| adts | http,tcp,pipe | http | aac | | `http:` | +| bubble | http | | h264,hevc,pcm_alaw | | `bubble:` | +| dvrip | tcp | | h264,hevc,pcm_alaw,pcm_mulaw | pcm_alaw | `dvrip:` | +| flv | http,tcp,pipe | http | h264,aac | | `http:` | +| gopro | http+udp | | TODO | | `gopro:` | +| hass/webrtc | ws+udp,tcp | | TODO | | `hass:` | +| hls/mpegts | http | | h264,h265,aac,opus | | `http:` | +| homekit | homekit+udp | | h264,eld* | | `homekit:` | +| isapi | http | | | pcm_alaw,pcm_mulaw | `isapi:` | +| ivideon | ws | | h264 | | `ivideon:` | +| kasa | http | | h264,pcm_mulaw | | `kasa:` | +| h264 | http,tcp,pipe | http | h264 | | `http:` | +| hevc | http,tcp,pipe | http | hevc | | `http:` | +| mjpeg | http,tcp,pipe | http | mjpeg | | `http:` | +| mpjpeg | http,tcp,pipe | http | mjpeg | | `http:` | +| mpegts | http,tcp,pipe | http | h264,hevc,aac,opus | | `http:` | +| nest/webrtc | http+udp | | TODO | | `nest:` | +| roborock | mqtt+udp | | h264,opus | opus | `roborock:` | +| rtmp | rtmp | rtmp | h264,aac | | `rtmp:` | +| rtsp | rtsp+tcp,ws | rtsp+tcp | h264,hevc,aac,pcm*,opus | pcm*,opus | `rtsp:` | +| stdin | pipe | | | pcm_alaw,pcm_mulaw | `stdin:` | +| tapo | http | | h264,pcma | pcm_alaw | `tapo:` | +| wav | http,tcp,pipe | http | pcm_alaw,pcm_mulaw | | `http:` | +| webrtc* | TODO | TODO | h264,pcm_alaw,pcm_mulaw,opus | pcm_alaw,pcm_mulaw | `webrtc:` | +| webtorrent | TODO | TODO | TODO | TODO | `webtorrent:` | +| yuv4mpegpipe | http,tcp,pipe | http | rawvideo | | `http:` | + +- **eld** - rare variant of aac codec +- **pcm** - pcm_alaw pcm_mulaw pcm_s16be pcm_s16le +- **webrtc** - webrtc/kinesis, webrtc/openipc, webrtc/milestone, webrtc/wyze, webrtc/whep + +## Consumers (output) + +| Format | Protocol | Send codecs | Recv codecs | Example | +|--------------|-------------|------------------------------|-------------------------|---------------------------------------| +| adts | http | aac | | `GET /api/stream.adts` | +| ascii | http | mjpeg | | `GET /api/stream.ascii` | +| flv | http | h264,aac | | `GET /api/stream.flv` | +| hls/mpegts | http | h264,hevc,aac | | `GET /api/stream.m3u8` | +| hls/fmp4 | http | h264,hevc,aac,pcm*,opus | | `GET /api/stream.m3u8?mp4` | +| homekit | homekit+udp | h264,opus | | Apple HomeKit app | +| mjpeg | ws | mjpeg | | `{"type":"mjpeg"}` -> `/api/ws` | +| mpjpeg | http | mjpeg | | `GET /api/stream.mjpeg` | +| mp4 | http | h264,hevc,aac,pcm*,opus | | `GET /api/stream.mp4` | +| mse/fmp4 | ws | h264,hevc,aac,pcm*,opus | | `{"type":"mse"}` -> `/api/ws` | +| mpegts | http | h264,hevc,aac | | `GET /api/stream.ts` | +| rtmp | rtmp | h264,aac | | `rtmp://localhost:1935/{stream_name}` | +| rtsp | rtsp+tcp | h264,hevc,aac,pcm*,opus | | `rtsp://localhost:8554/{stream_name}` | +| webrtc | TODO | h264,pcm_alaw,pcm_mulaw,opus | pcm_alaw,pcm_mulaw,opus | `{"type":"webrtc"}` -> `/api/ws` | +| yuv4mpegpipe | http | rawvideo | | `GET /api/stream.y4m` | + +- **pcm** - pcm_alaw pcm_mulaw pcm_s16be pcm_s16le + +## Snapshots + +| Format | Protocol | Send codecs | Example | +|--------|----------|-------------|-----------------------| +| jpeg | http | mjpeg | `GET /api/frame.jpeg` | +| mp4 | http | h264,hevc | `GET /api/frame.mp4` | + +## Developers + +File naming: + +- `pkg/{format}/producer.go` - producer for this format (also if support backchannel) +- `pkg/{format}/consumer.go` - consumer for this format +- `pkg/{format}/backchanel.go` - producer with only backchannel func + ## Useful links - https://www.wowza.com/blog/streaming-protocols diff --git a/pkg/aac/consumer.go b/pkg/aac/consumer.go index e785adc5..fc67d2a4 100644 --- a/pkg/aac/consumer.go +++ b/pkg/aac/consumer.go @@ -8,15 +8,12 @@ import ( ) type Consumer struct { - core.SuperConsumer + core.Connection wr *core.WriteBuffer } func NewConsumer() *Consumer { - cons := &Consumer{ - wr: core.NewWriteBuffer(nil), - } - cons.Medias = []*core.Media{ + medias := []*core.Media{ { Kind: core.KindAudio, Direction: core.DirectionSendonly, @@ -25,7 +22,16 @@ func NewConsumer() *Consumer { }, }, } - return cons + wr := core.NewWriteBuffer(nil) + return &Consumer{ + Connection: core.Connection{ + ID: core.NewID(), + FormatName: "adts", + Medias: medias, + Transport: wr, + }, + wr: wr, + } } func (c *Consumer) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error { @@ -51,8 +57,3 @@ func (c *Consumer) AddTrack(media *core.Media, codec *core.Codec, track *core.Re func (c *Consumer) WriteTo(wr io.Writer) (int64, error) { return c.wr.WriteTo(wr) } - -func (c *Consumer) Stop() error { - _ = c.SuperConsumer.Close() - return c.wr.Close() -} diff --git a/pkg/aac/producer.go b/pkg/aac/producer.go index e9be71fd..efd2d175 100644 --- a/pkg/aac/producer.go +++ b/pkg/aac/producer.go @@ -10,9 +10,8 @@ import ( ) type Producer struct { - core.SuperProducer + core.Connection rd *bufio.Reader - cl io.Closer } func Open(r io.Reader) (*Producer, error) { @@ -23,18 +22,22 @@ func Open(r io.Reader) (*Producer, error) { return nil, err } - codec := ADTSToCodec(b) - - prod := &Producer{rd: rd, cl: r.(io.Closer)} - prod.Type = "ADTS producer" - prod.Medias = []*core.Media{ + medias := []*core.Media{ { Kind: core.KindAudio, Direction: core.DirectionRecvonly, - Codecs: []*core.Codec{codec}, + Codecs: []*core.Codec{ADTSToCodec(b)}, }, } - return prod, nil + return &Producer{ + Connection: core.Connection{ + ID: core.NewID(), + FormatName: "adts", + Medias: medias, + Transport: r, + }, + rd: rd, + }, nil } func (c *Producer) Start() error { @@ -66,8 +69,3 @@ func (c *Producer) Start() error { c.Receivers[0].WriteRTP(pkt) } } - -func (c *Producer) Stop() error { - _ = c.SuperProducer.Close() - return c.cl.Close() -} diff --git a/pkg/bubble/client.go b/pkg/bubble/client.go index c0a79701..5afba779 100644 --- a/pkg/bubble/client.go +++ b/pkg/bubble/client.go @@ -22,6 +22,7 @@ import ( "github.com/pion/rtp" ) +// Deprecated: should be rewritten to core.Connection type Client struct { core.Listener diff --git a/pkg/bubble/producer.go b/pkg/bubble/producer.go index a7aaa56e..9fa18f25 100644 --- a/pkg/bubble/producer.go +++ b/pkg/bubble/producer.go @@ -65,11 +65,16 @@ func (c *Client) Stop() error { } func (c *Client) MarshalJSON() ([]byte, error) { - info := &core.Info{ - Type: "Bubble active producer", - Medias: c.medias, - Recv: c.recv, - Receivers: c.receivers, + info := &core.Connection{ + ID: core.ID(c), + FormatName: "bubble", + Protocol: "http", + Medias: c.medias, + Recv: c.recv, + Receivers: c.receivers, + } + if c.conn != nil { + info.RemoteAddr = c.conn.RemoteAddr().String() } return json.Marshal(info) } diff --git a/pkg/core/codec.go b/pkg/core/codec.go index 91f6fddc..d07b8b74 100644 --- a/pkg/core/codec.go +++ b/pkg/core/codec.go @@ -46,7 +46,7 @@ func FFmpegCodecName(name string) string { case CodecH264: return "h264" case CodecH265: - return "h265" + return "hevc" case CodecJPEG: return "mjpeg" case CodecRAW: diff --git a/pkg/core/connection.go b/pkg/core/connection.go new file mode 100644 index 00000000..1055c381 --- /dev/null +++ b/pkg/core/connection.go @@ -0,0 +1,139 @@ +package core + +import ( + "io" + "net/http" + "reflect" + "sync/atomic" +) + +func NewID() uint32 { + return id.Add(1) +} + +// Deprecated: use NewID instead +func ID(v any) uint32 { + p := uintptr(reflect.ValueOf(v).UnsafePointer()) + return 0x8000_0000 | uint32(p) +} + +var id atomic.Uint32 + +type Info interface { + SetProtocol(string) + SetRemoteAddr(string) + SetSource(string) + SetURL(string) + WithRequest(*http.Request) +} + +// Connection just like webrtc.PeerConnection +// - ID and RemoteAddr used for building Connection(s) graph +// - FormatName, Protocol, RemoteAddr, Source, URL, SDP, UserAgent used for info about Connection +// - FormatName and Protocol has FFmpeg compatible names +// - Transport used for auto closing on Stop +type Connection struct { + ID uint32 `json:"id,omitempty"` + FormatName string `json:"format_name,omitempty"` // rtsp, webrtc, mp4, mjpeg, mpjpeg... + Protocol string `json:"protocol,omitempty"` // tcp, udp, http, ws, pipe... + RemoteAddr string `json:"remote_addr,omitempty"` // host:port other info + Source string `json:"source,omitempty"` + URL string `json:"url,omitempty"` + SDP string `json:"sdp,omitempty"` + UserAgent string `json:"user_agent,omitempty"` + + Medias []*Media `json:"medias,omitempty"` + Receivers []*Receiver `json:"receivers,omitempty"` + Senders []*Sender `json:"senders,omitempty"` + Recv int `json:"bytes_recv,omitempty"` + Send int `json:"bytes_send,omitempty"` + + Transport any `json:"-"` +} + +func (c *Connection) GetMedias() []*Media { + return c.Medias +} + +func (c *Connection) GetTrack(media *Media, codec *Codec) (*Receiver, error) { + for _, receiver := range c.Receivers { + if receiver.Codec == codec { + return receiver, nil + } + } + receiver := NewReceiver(media, codec) + c.Receivers = append(c.Receivers, receiver) + return receiver, nil +} + +func (c *Connection) Stop() error { + for _, receiver := range c.Receivers { + receiver.Close() + } + for _, sender := range c.Senders { + sender.Close() + } + if closer, ok := c.Transport.(io.Closer); ok { + return closer.Close() + } + return nil +} + +// Deprecated: +func (c *Connection) Codecs() []*Codec { + codecs := make([]*Codec, len(c.Senders)) + for i, sender := range c.Senders { + codecs[i] = sender.Codec + } + return codecs +} + +func (c *Connection) SetProtocol(s string) { + c.Protocol = s +} + +func (c *Connection) SetRemoteAddr(s string) { + if c.RemoteAddr == "" { + c.RemoteAddr = s + } else { + c.RemoteAddr += " forward " + c.RemoteAddr + } +} + +func (c *Connection) SetSource(s string) { + c.Source = s +} + +func (c *Connection) SetURL(s string) { + c.URL = s +} + +func (c *Connection) WithRequest(r *http.Request) { + if r.Header.Get("Upgrade") == "websocket" { + c.Protocol = "ws" + } else { + c.Protocol = "http" + } + + c.RemoteAddr = r.RemoteAddr + if remote := r.Header.Get("X-Forwarded-For"); remote != "" { + c.RemoteAddr += " forwarded " + remote + } + + c.UserAgent = r.UserAgent() +} + +// Create like os.Create, init Consumer with existing Transport +func Create(w io.Writer) (*Connection, error) { + return &Connection{Transport: w}, nil +} + +// Open like os.Open, init Producer from existing Transport +func Open(r io.Reader) (*Connection, error) { + return &Connection{Transport: r}, nil +} + +// Dial like net.Dial, init Producer via Dialing +func Dial(rawURL string) (*Connection, error) { + return &Connection{}, nil +} diff --git a/pkg/core/core.go b/pkg/core/core.go index bc855ccc..9555ecfa 100644 --- a/pkg/core/core.go +++ b/pkg/core/core.go @@ -1,5 +1,7 @@ package core +import "encoding/json" + const ( DirectionRecvonly = "recvonly" DirectionSendonly = "sendonly" @@ -90,89 +92,6 @@ func (m Mode) String() string { return "unknown" } -type Info struct { - Type string `json:"type,omitempty"` - URL string `json:"url,omitempty"` - RemoteAddr string `json:"remote_addr,omitempty"` - UserAgent string `json:"user_agent,omitempty"` - SDP string `json:"sdp,omitempty"` - Medias []*Media `json:"medias,omitempty"` - Receivers []*Receiver `json:"receivers,omitempty"` - Senders []*Sender `json:"senders,omitempty"` - Recv int `json:"recv,omitempty"` - Send int `json:"send,omitempty"` -} - -const ( - UnsupportedCodec = "unsupported codec" - WrongMediaDirection = "wrong media direction" -) - -type SuperProducer struct { - Type string `json:"type,omitempty"` - URL string `json:"url,omitempty"` - SDP string `json:"sdp,omitempty"` - Medias []*Media `json:"medias,omitempty"` - Receivers []*Receiver `json:"receivers,omitempty"` - Recv int `json:"recv,omitempty"` -} - -func (s *SuperProducer) GetMedias() []*Media { - return s.Medias -} - -func (s *SuperProducer) GetTrack(media *Media, codec *Codec) (*Receiver, error) { - for _, receiver := range s.Receivers { - if receiver.Codec == codec { - return receiver, nil - } - } - receiver := NewReceiver(media, codec) - s.Receivers = append(s.Receivers, receiver) - return receiver, nil -} - -func (s *SuperProducer) Close() error { - for _, receiver := range s.Receivers { - receiver.Close() - } - return nil -} - -type SuperConsumer struct { - Type string `json:"type,omitempty"` - URL string `json:"url,omitempty"` - RemoteAddr string `json:"remote_addr,omitempty"` - UserAgent string `json:"user_agent,omitempty"` - SDP string `json:"sdp,omitempty"` - Medias []*Media `json:"medias,omitempty"` - Senders []*Sender `json:"senders,omitempty"` - Send int `json:"send,omitempty"` -} - -func (s *SuperConsumer) GetMedias() []*Media { - return s.Medias -} - -func (s *SuperConsumer) AddTrack(media *Media, codec *Codec, track *Receiver) error { - return nil -} - -//func (b *SuperConsumer) WriteTo(w io.Writer) (n int64, err error) { -// return 0, nil -//} - -func (s *SuperConsumer) Close() error { - for _, sender := range s.Senders { - sender.Close() - } - return nil -} - -func (s *SuperConsumer) Codecs() []*Codec { - codecs := make([]*Codec, len(s.Senders)) - for i, sender := range s.Senders { - codecs[i] = sender.Codec - } - return codecs +func (m Mode) MarshalJSON() ([]byte, error) { + return json.Marshal(m.String()) } diff --git a/pkg/core/media.go b/pkg/core/media.go index ef9ef74b..2284d0cd 100644 --- a/pkg/core/media.go +++ b/pkg/core/media.go @@ -92,7 +92,7 @@ func (m *Media) Equal(media *Media) bool { func GetKind(name string) string { switch name { - case CodecH264, CodecH265, CodecVP8, CodecVP9, CodecAV1, CodecJPEG: + case CodecH264, CodecH265, CodecVP8, CodecVP9, CodecAV1, CodecJPEG, CodecRAW: return KindVideo case CodecPCMU, CodecPCMA, CodecAAC, CodecOpus, CodecG722, CodecMP3, CodecPCM, CodecPCML, CodecELD, CodecFLAC: return KindAudio diff --git a/pkg/core/node.go b/pkg/core/node.go index fd58f2d7..a9959c3d 100644 --- a/pkg/core/node.go +++ b/pkg/core/node.go @@ -23,10 +23,11 @@ type Filter func(handler HandlerFunc) HandlerFunc // Node - Receiver or Sender or Filter (transform) type Node struct { - Codec *Codec `json:"codec"` - Input HandlerFunc `json:"-"` - Output HandlerFunc `json:"-"` + Codec *Codec + Input HandlerFunc + Output HandlerFunc + id uint32 childs []*Node parent *Node diff --git a/pkg/core/track.go b/pkg/core/track.go index 83c39e01..8bc65374 100644 --- a/pkg/core/track.go +++ b/pkg/core/track.go @@ -1,6 +1,7 @@ package core import ( + "encoding/json" "errors" "github.com/pion/rtp" @@ -22,7 +23,7 @@ type Receiver struct { func NewReceiver(media *Media, codec *Codec) *Receiver { r := &Receiver{ - Node: Node{Codec: codec}, + Node: Node{id: NewID(), Codec: codec}, Media: media, } r.Input = func(packet *Packet) { @@ -91,7 +92,7 @@ func NewSender(media *Media, codec *Codec) *Sender { buf := make(chan *Packet, bufSize) s := &Sender{ - Node: Node{Codec: codec}, + Node: Node{id: NewID(), Codec: codec}, Media: media, buf: buf, } @@ -171,3 +172,43 @@ func (s *Sender) Close() { s.Node.Close() } + +func (r *Receiver) MarshalJSON() ([]byte, error) { + v := struct { + ID uint32 `json:"id"` + Codec *Codec `json:"codec"` + Childs []uint32 `json:"childs,omitempty"` + Bytes int `json:"bytes,omitempty"` + Packets int `json:"packets,omitempty"` + }{ + ID: r.Node.id, + Codec: r.Node.Codec, + Bytes: r.Bytes, + Packets: r.Packets, + } + for _, child := range r.childs { + v.Childs = append(v.Childs, child.id) + } + return json.Marshal(v) +} + +func (s *Sender) MarshalJSON() ([]byte, error) { + v := struct { + ID uint32 `json:"id"` + Codec *Codec `json:"codec"` + Parent uint32 `json:"parent,omitempty"` + Bytes int `json:"bytes,omitempty"` + Packets int `json:"packets,omitempty"` + Drops int `json:"drops,omitempty"` + }{ + ID: s.Node.id, + Codec: s.Node.Codec, + Bytes: s.Bytes, + Packets: s.Packets, + Drops: s.Drops, + } + if s.parent != nil { + v.Parent = s.parent.id + } + return json.Marshal(v) +} diff --git a/pkg/dvrip/consumer.go b/pkg/dvrip/backchannel.go similarity index 78% rename from pkg/dvrip/consumer.go rename to pkg/dvrip/backchannel.go index 7652c079..0424e965 100644 --- a/pkg/dvrip/consumer.go +++ b/pkg/dvrip/backchannel.go @@ -8,16 +8,16 @@ import ( "github.com/pion/rtp" ) -type Consumer struct { - core.SuperConsumer +type Backchannel struct { + core.Connection client *Client } -func (c *Consumer) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) { +func (c *Backchannel) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) { return nil, core.ErrCantGetTrack } -func (c *Consumer) Start() error { +func (c *Backchannel) Start() error { if err := c.client.conn.SetReadDeadline(time.Time{}); err != nil { return err } @@ -30,12 +30,7 @@ func (c *Consumer) Start() error { } } -func (c *Consumer) Stop() error { - _ = c.SuperConsumer.Close() - return c.client.Close() -} - -func (c *Consumer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiver) error { +func (c *Backchannel) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiver) error { if err := c.client.Talk(); err != nil { return err } diff --git a/pkg/dvrip/dvrip.go b/pkg/dvrip/dvrip.go index 0f914640..c4980a80 100644 --- a/pkg/dvrip/dvrip.go +++ b/pkg/dvrip/dvrip.go @@ -8,17 +8,22 @@ func Dial(url string) (core.Producer, error) { return nil, err } + conn := core.Connection{ + ID: core.NewID(), + FormatName: "dvrip", + Protocol: "tcp", + RemoteAddr: client.conn.RemoteAddr().String(), + Transport: client.conn, + } + if client.stream != "" { - prod := &Producer{client: client} - prod.Type = "DVRIP active producer" + prod := &Producer{Connection: conn, client: client} if err := prod.probe(); err != nil { return nil, err } return prod, nil } else { - cons := &Consumer{client: client} - cons.Type = "DVRIP active consumer" - cons.Medias = []*core.Media{ + conn.Medias = []*core.Media{ { Kind: core.KindAudio, Direction: core.DirectionSendonly, @@ -29,6 +34,6 @@ func Dial(url string) (core.Producer, error) { }, }, } - return cons, nil + return &Backchannel{Connection: conn, client: client}, nil } } diff --git a/pkg/dvrip/producer.go b/pkg/dvrip/producer.go index 412dd0a3..c87017b4 100644 --- a/pkg/dvrip/producer.go +++ b/pkg/dvrip/producer.go @@ -15,7 +15,7 @@ import ( ) type Producer struct { - core.SuperProducer + core.Connection client *Client @@ -92,10 +92,6 @@ func (c *Producer) Start() error { } } -func (c *Producer) Stop() error { - return c.client.Close() -} - func (c *Producer) probe() error { if err := c.client.Play(); err != nil { return err diff --git a/pkg/flv/consumer.go b/pkg/flv/consumer.go index 59e65d9c..fe966bfc 100644 --- a/pkg/flv/consumer.go +++ b/pkg/flv/consumer.go @@ -10,17 +10,13 @@ import ( ) type Consumer struct { - core.SuperConsumer + core.Connection wr *core.WriteBuffer muxer *Muxer } func NewConsumer() *Consumer { - c := &Consumer{ - wr: core.NewWriteBuffer(nil), - muxer: &Muxer{}, - } - c.Medias = []*core.Media{ + medias := []*core.Media{ { Kind: core.KindVideo, Direction: core.DirectionSendonly, @@ -36,7 +32,17 @@ func NewConsumer() *Consumer { }, }, } - return c + wr := core.NewWriteBuffer(nil) + return &Consumer{ + Connection: core.Connection{ + ID: core.NewID(), + FormatName: "flv", + Medias: medias, + Transport: wr, + }, + wr: wr, + muxer: &Muxer{}, + } } func (c *Consumer) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error { @@ -86,8 +92,3 @@ func (c *Consumer) WriteTo(wr io.Writer) (int64, error) { } return c.wr.WriteTo(wr) } - -func (c *Consumer) Stop() error { - _ = c.SuperConsumer.Close() - return c.wr.Close() -} diff --git a/pkg/flv/producer.go b/pkg/flv/producer.go index 3972e666..66755217 100644 --- a/pkg/flv/producer.go +++ b/pkg/flv/producer.go @@ -15,18 +15,24 @@ import ( ) type Producer struct { - core.SuperProducer + core.Connection rd *core.ReadBuffer video, audio *core.Receiver } func Open(rd io.Reader) (*Producer, error) { - prod := &Producer{rd: core.NewReadBuffer(rd)} + prod := &Producer{ + Connection: core.Connection{ + ID: core.NewID(), + FormatName: "flv", + Transport: rd, + }, + rd: core.NewReadBuffer(rd), + } if err := prod.probe(); err != nil { return nil, err } - prod.Type = "FLV producer" return prod, nil } @@ -57,7 +63,7 @@ const ( ) func (c *Producer) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) { - receiver, _ := c.SuperProducer.GetTrack(media, codec) + receiver, _ := c.Connection.GetTrack(media, codec) if media.Kind == core.KindVideo { c.video = receiver } else { @@ -117,11 +123,6 @@ func (c *Producer) Start() error { } } -func (c *Producer) Stop() error { - _ = c.SuperProducer.Close() - return c.rd.Close() -} - func (c *Producer) probe() error { if err := c.readHeader(); err != nil { return err diff --git a/pkg/gopro/gopro.go b/pkg/gopro/producer.go similarity index 90% rename from pkg/gopro/gopro.go rename to pkg/gopro/producer.go index 2d6a098b..1873159f 100644 --- a/pkg/gopro/gopro.go +++ b/pkg/gopro/producer.go @@ -8,11 +8,10 @@ import ( "net/url" "time" - "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/mpegts" ) -func Dial(rawURL string) (core.Producer, error) { +func Dial(rawURL string) (*mpegts.Producer, error) { u, err := url.Parse(rawURL) if err != nil { return nil, err @@ -32,7 +31,15 @@ func Dial(rawURL string) (core.Producer, error) { return nil, err } - return mpegts.Open(r) + prod, err := mpegts.Open(r) + if err != nil { + return nil, err + } + + prod.FormatName = "gopro" + prod.RemoteAddr = u.Host + + return prod, nil } type listener struct { diff --git a/pkg/hass/client.go b/pkg/hass/client.go index c1ed5b4b..5b236051 100644 --- a/pkg/hass/client.go +++ b/pkg/hass/client.go @@ -61,8 +61,10 @@ func NewClient(rawURL string) (*Client, error) { } conn := webrtc.NewConn(pc) - conn.Desc = "Hass" + conn.FormatName = "hass/webrtc" conn.Mode = core.ModeActiveProducer + conn.Protocol = "ws" + conn.URL = rawURL // https://developers.google.com/nest/device-access/traits/device/camera-live-stream#generatewebrtcstream-request-fields medias := []*core.Media{ diff --git a/pkg/hls/producer.go b/pkg/hls/producer.go index 410e771a..e1c3ed43 100644 --- a/pkg/hls/producer.go +++ b/pkg/hls/producer.go @@ -4,14 +4,19 @@ import ( "io" "net/url" - "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/mpegts" ) -func OpenURL(u *url.URL, body io.ReadCloser) (core.Producer, error) { +func OpenURL(u *url.URL, body io.ReadCloser) (*mpegts.Producer, error) { rd, err := NewReader(u, body) if err != nil { return nil, err } - return mpegts.Open(rd) + prod, err := mpegts.Open(rd) + if err != nil { + return nil, err + } + prod.FormatName = "hls/mpegts" + prod.RemoteAddr = u.Host + return prod, nil } diff --git a/pkg/homekit/consumer.go b/pkg/homekit/consumer.go index 05ea2427..1c665233 100644 --- a/pkg/homekit/consumer.go +++ b/pkg/homekit/consumer.go @@ -16,7 +16,7 @@ import ( ) type Consumer struct { - core.SuperConsumer + core.Connection conn net.Conn srtp *srtp.Server @@ -29,28 +29,31 @@ type Consumer struct { } func NewConsumer(conn net.Conn, server *srtp.Server) *Consumer { - return &Consumer{ - SuperConsumer: core.SuperConsumer{ - Type: "HomeKit passive consumer", - RemoteAddr: conn.RemoteAddr().String(), - Medias: []*core.Media{ - { - Kind: core.KindVideo, - Direction: core.DirectionSendonly, - Codecs: []*core.Codec{ - {Name: core.CodecH264}, - }, - }, - { - Kind: core.KindAudio, - Direction: core.DirectionSendonly, - Codecs: []*core.Codec{ - {Name: core.CodecOpus}, - }, - }, + medias := []*core.Media{ + { + Kind: core.KindVideo, + Direction: core.DirectionSendonly, + Codecs: []*core.Codec{ + {Name: core.CodecH264}, }, }, - + { + Kind: core.KindAudio, + Direction: core.DirectionSendonly, + Codecs: []*core.Codec{ + {Name: core.CodecOpus}, + }, + }, + } + return &Consumer{ + Connection: core.Connection{ + ID: core.NewID(), + FormatName: "homekit", + Protocol: "udp", + RemoteAddr: conn.RemoteAddr().String(), + Medias: medias, + Transport: conn, + }, conn: conn, srtp: server, } @@ -175,11 +178,10 @@ func (c *Consumer) WriteTo(io.Writer) (int64, error) { } func (c *Consumer) Stop() error { - _ = c.SuperConsumer.Close() if c.deadline != nil { c.deadline.Reset(0) } - return c.SuperConsumer.Close() + return c.Connection.Stop() } func (c *Consumer) srtpEndpoint() *srtp.Endpoint { diff --git a/pkg/homekit/client.go b/pkg/homekit/producer.go similarity index 95% rename from pkg/homekit/client.go rename to pkg/homekit/producer.go index 133499d3..c2781e27 100644 --- a/pkg/homekit/client.go +++ b/pkg/homekit/producer.go @@ -15,8 +15,9 @@ import ( "github.com/pion/rtp" ) +// Deprecated: rename to Producer type Client struct { - core.SuperProducer + core.Connection hap *hap.Client srtp *srtp.Server @@ -52,9 +53,12 @@ func Dial(rawURL string, server *srtp.Server) (*Client, error) { } client := &Client{ - SuperProducer: core.SuperProducer{ - Type: "HomeKit active producer", - URL: conn.URL(), + Connection: core.Connection{ + ID: core.NewID(), + FormatName: "homekit", + Protocol: "udp", + Source: conn.URL(), + Transport: conn, }, hap: conn, srtp: server, @@ -93,7 +97,6 @@ func (c *Client) GetMedias() []*core.Media { return nil } - c.URL = c.hap.URL() c.SDP = fmt.Sprintf("%+v\n%+v", c.videoConfig, c.audioConfig) c.Medias = []*core.Media{ @@ -175,8 +178,6 @@ func (c *Client) Start() error { } func (c *Client) Stop() error { - _ = c.SuperProducer.Close() - if c.videoSession != nil && c.videoSession.Remote != nil { c.srtp.DelSession(c.videoSession) } @@ -184,7 +185,7 @@ func (c *Client) Stop() error { c.srtp.DelSession(c.audioSession) } - return c.hap.Close() + return c.Connection.Stop() } func (c *Client) trackByKind(kind string) *core.Receiver { diff --git a/pkg/image/producer.go b/pkg/image/producer.go new file mode 100644 index 00000000..2081c048 --- /dev/null +++ b/pkg/image/producer.go @@ -0,0 +1,92 @@ +package image + +import ( + "errors" + "io" + "net/http" + + "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/AlexxIT/go2rtc/pkg/tcp" + "github.com/pion/rtp" +) + +type Producer struct { + core.Connection + + closed bool + res *http.Response +} + +func Open(res *http.Response) (*Producer, error) { + return &Producer{ + Connection: core.Connection{ + ID: core.NewID(), + FormatName: "image", + Protocol: "http", + RemoteAddr: res.Request.URL.Host, + Transport: res.Body, + Medias: []*core.Media{ + { + Kind: core.KindVideo, + Direction: core.DirectionRecvonly, + Codecs: []*core.Codec{ + { + Name: core.CodecJPEG, + ClockRate: 90000, + PayloadType: core.PayloadTypeRAW, + }, + }, + }, + }, + }, + res: res, + }, nil +} + +func (c *Producer) Start() error { + body, err := io.ReadAll(c.res.Body) + if err != nil { + return err + } + + pkt := &rtp.Packet{ + Header: rtp.Header{Timestamp: core.Now90000()}, + Payload: body, + } + c.Receivers[0].WriteRTP(pkt) + + c.Recv += len(body) + + req := c.res.Request + + for !c.closed { + res, err := tcp.Do(req) + if err != nil { + return err + } + + if res.StatusCode != http.StatusOK { + return errors.New("wrong status: " + res.Status) + } + + body, err = io.ReadAll(res.Body) + if err != nil { + return err + } + + c.Recv += len(body) + + pkt = &rtp.Packet{ + Header: rtp.Header{Timestamp: core.Now90000()}, + Payload: body, + } + c.Receivers[0].WriteRTP(pkt) + } + + return nil +} + +func (c *Producer) Stop() error { + c.closed = true + return c.Connection.Stop() +} diff --git a/pkg/isapi/consumer.go b/pkg/isapi/backchannel.go similarity index 83% rename from pkg/isapi/consumer.go rename to pkg/isapi/backchannel.go index c7b51c9d..ade16255 100644 --- a/pkg/isapi/consumer.go +++ b/pkg/isapi/backchannel.go @@ -2,6 +2,7 @@ package isapi import ( "encoding/json" + "github.com/AlexxIT/go2rtc/pkg/core" "github.com/pion/rtp" ) @@ -51,10 +52,15 @@ func (c *Client) Stop() (err error) { } func (c *Client) MarshalJSON() ([]byte, error) { - info := &core.Info{ - Type: "ISAPI active consumer", - Medias: c.medias, - Send: c.send, + info := &core.Connection{ + ID: core.ID(c), + FormatName: "isapi", + Protocol: "http", + Medias: c.medias, + Send: c.send, + } + if c.conn != nil { + info.RemoteAddr = c.conn.RemoteAddr().String() } if c.sender != nil { info.Senders = []*core.Sender{c.sender} diff --git a/pkg/isapi/client.go b/pkg/isapi/client.go index 83dd9026..ba3e6887 100644 --- a/pkg/isapi/client.go +++ b/pkg/isapi/client.go @@ -11,6 +11,7 @@ import ( "github.com/AlexxIT/go2rtc/pkg/tcp" ) +// Deprecated: should be rewritten to core.Connection type Client struct { core.Listener diff --git a/pkg/ivideon/client.go b/pkg/ivideon/client.go index 7cbf0b38..ef79010e 100644 --- a/pkg/ivideon/client.go +++ b/pkg/ivideon/client.go @@ -26,6 +26,7 @@ const ( StateHandle ) +// Deprecated: should be rewritten to core.Connection type Client struct { core.Listener diff --git a/pkg/ivideon/producer.go b/pkg/ivideon/producer.go index d0a8fcba..78084123 100644 --- a/pkg/ivideon/producer.go +++ b/pkg/ivideon/producer.go @@ -2,6 +2,7 @@ package ivideon import ( "encoding/json" + "github.com/AlexxIT/go2rtc/pkg/core" ) @@ -32,11 +33,16 @@ func (c *Client) Stop() error { } func (c *Client) MarshalJSON() ([]byte, error) { - info := &core.Info{ - Type: "Ivideon active producer", - URL: c.ID, - Medias: c.medias, - Recv: c.recv, + info := &core.Connection{ + ID: core.ID(c), + FormatName: "ivideon", + Protocol: "ws", + URL: c.ID, + Medias: c.medias, + Recv: c.recv, + } + if c.conn != nil { + info.RemoteAddr = c.conn.RemoteAddr().String() } if c.receiver != nil { info.Receivers = []*core.Receiver{c.receiver} diff --git a/pkg/kasa/producer.go b/pkg/kasa/producer.go index d138cb68..22d10216 100644 --- a/pkg/kasa/producer.go +++ b/pkg/kasa/producer.go @@ -12,13 +12,13 @@ import ( "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/h264" "github.com/AlexxIT/go2rtc/pkg/h264/annexb" - "github.com/AlexxIT/go2rtc/pkg/multipart" + "github.com/AlexxIT/go2rtc/pkg/mpjpeg" "github.com/AlexxIT/go2rtc/pkg/tcp" "github.com/pion/rtp" ) type Producer struct { - core.SuperProducer + core.Connection rd *core.ReadBuffer reader *bufio.Reader @@ -65,11 +65,18 @@ func Dial(url string) (*Producer, error) { rd.Reader = httputil.NewChunkedReader(buf) } - prod := &Producer{rd: core.NewReadBuffer(rd)} + prod := &Producer{ + Connection: core.Connection{ + ID: core.NewID(), + FormatName: "kasa", + Protocol: "http", + Transport: rd, + }, + rd: core.NewReadBuffer(rd), + } if err = prod.probe(); err != nil { return nil, err } - prod.Type = "Kasa producer" return prod, nil } @@ -90,7 +97,7 @@ func (c *Producer) Start() error { } for { - header, body, err := multipart.Next(c.reader) + header, body, err := mpjpeg.Next(c.reader) if err != nil { return err } @@ -128,11 +135,6 @@ func (c *Producer) Start() error { } } -func (c *Producer) Stop() error { - _ = c.SuperProducer.Close() - return c.rd.Close() -} - const ( MimeVideo = "video/x-h264" MimeG711U = "audio/g711u" @@ -151,7 +153,7 @@ func (c *Producer) probe() error { timeout := time.Now().Add(core.ProbeTimeout) for (waitVideo || waitAudio) && time.Now().Before(timeout) { - header, body, err := multipart.Next(c.reader) + header, body, err := mpjpeg.Next(c.reader) if err != nil { return err } diff --git a/pkg/magic/bitstream/producer.go b/pkg/magic/bitstream/producer.go index 2ffa964e..b84f049b 100644 --- a/pkg/magic/bitstream/producer.go +++ b/pkg/magic/bitstream/producer.go @@ -13,7 +13,7 @@ import ( ) type Producer struct { - core.SuperProducer + core.Connection rd *core.ReadBuffer } @@ -28,26 +28,35 @@ func Open(r io.Reader) (*Producer, error) { buf = annexb.EncodeToAVCC(buf, false) // won't break original buffer var codec *core.Codec + var format string switch { case h264.NALUType(buf) == h264.NALUTypeSPS: codec = h264.AVCCToCodec(buf) + format = "h264" case h265.NALUType(buf) == h265.NALUTypeVPS: codec = h265.AVCCToCodec(buf) + format = "hevc" default: return nil, errors.New("bitstream: unsupported header: " + hex.EncodeToString(buf[:8])) } - prod := &Producer{rd: rd} - prod.Type = "Bitstream producer" - prod.Medias = []*core.Media{ + medias := []*core.Media{ { Kind: core.KindVideo, Direction: core.DirectionRecvonly, Codecs: []*core.Codec{codec}, }, } - return prod, nil + return &Producer{ + Connection: core.Connection{ + ID: core.NewID(), + FormatName: format, + Medias: medias, + Transport: r, + }, + rd: rd, + }, nil } func (c *Producer) Start() error { @@ -84,8 +93,3 @@ func (c *Producer) Start() error { } } } - -func (c *Producer) Stop() error { - _ = c.SuperProducer.Close() - return c.rd.Close() -} diff --git a/pkg/magic/keyframe.go b/pkg/magic/keyframe.go index d2ae80bd..8f70eec6 100644 --- a/pkg/magic/keyframe.go +++ b/pkg/magic/keyframe.go @@ -12,26 +12,32 @@ import ( ) type Keyframe struct { - core.SuperConsumer + core.Connection wr *core.WriteBuffer } +// Deprecated: should be rewritten func NewKeyframe() *Keyframe { - return &Keyframe{ - core.SuperConsumer{ - Medias: []*core.Media{ - { - Kind: core.KindVideo, - Direction: core.DirectionSendonly, - Codecs: []*core.Codec{ - {Name: core.CodecJPEG}, - {Name: core.CodecH264}, - {Name: core.CodecH265}, - }, - }, + medias := []*core.Media{ + { + Kind: core.KindVideo, + Direction: core.DirectionSendonly, + Codecs: []*core.Codec{ + {Name: core.CodecJPEG}, + {Name: core.CodecH264}, + {Name: core.CodecH265}, }, }, - core.NewWriteBuffer(nil), + } + wr := core.NewWriteBuffer(nil) + return &Keyframe{ + Connection: core.Connection{ + ID: core.NewID(), + FormatName: "keyframe", + Medias: medias, + Transport: wr, + }, + wr: wr, } } @@ -98,8 +104,3 @@ func (k *Keyframe) CodecName() string { func (k *Keyframe) WriteTo(wr io.Writer) (int64, error) { return k.wr.WriteTo(wr) } - -func (k *Keyframe) Stop() error { - _ = k.SuperConsumer.Close() - return k.wr.Close() -} diff --git a/pkg/magic/mjpeg/producer.go b/pkg/magic/mjpeg/producer.go index e5627fd7..e47c168d 100644 --- a/pkg/magic/mjpeg/producer.go +++ b/pkg/magic/mjpeg/producer.go @@ -9,14 +9,12 @@ import ( ) type Producer struct { - core.SuperProducer + core.Connection rd *core.ReadBuffer } func Open(rd io.Reader) (*Producer, error) { - prod := &Producer{rd: core.NewReadBuffer(rd)} - prod.Type = "MJPEG producer" - prod.Medias = []*core.Media{ + medias := []*core.Media{ { Kind: core.KindVideo, Direction: core.DirectionRecvonly, @@ -29,7 +27,15 @@ func Open(rd io.Reader) (*Producer, error) { }, }, } - return prod, nil + return &Producer{ + Connection: core.Connection{ + ID: core.NewID(), + FormatName: "mjpeg", + Medias: medias, + Transport: rd, + }, + rd: core.NewReadBuffer(rd), + }, nil } func (c *Producer) Start() error { @@ -70,8 +76,3 @@ func (c *Producer) Start() error { buf = buf[i:] } } - -func (c *Producer) Stop() error { - _ = c.SuperProducer.Close() - return c.rd.Close() -} diff --git a/pkg/magic/producer.go b/pkg/magic/producer.go index 9bde508d..3742ccf9 100644 --- a/pkg/magic/producer.go +++ b/pkg/magic/producer.go @@ -13,7 +13,7 @@ import ( "github.com/AlexxIT/go2rtc/pkg/magic/bitstream" "github.com/AlexxIT/go2rtc/pkg/magic/mjpeg" "github.com/AlexxIT/go2rtc/pkg/mpegts" - "github.com/AlexxIT/go2rtc/pkg/multipart" + "github.com/AlexxIT/go2rtc/pkg/mpjpeg" "github.com/AlexxIT/go2rtc/pkg/wav" "github.com/AlexxIT/go2rtc/pkg/y4m" ) @@ -26,29 +26,31 @@ func Open(r io.Reader) (core.Producer, error) { return nil, err } - switch { - case string(b) == annexb.StartCode: + switch string(b) { + case annexb.StartCode: return bitstream.Open(rd) - - case string(b) == wav.FourCC: + case wav.FourCC: return wav.Open(rd) - - case string(b) == y4m.FourCC: + case y4m.FourCC: return y4m.Open(rd) + } - case bytes.HasPrefix(b, []byte{0xFF, 0xD8}): - return mjpeg.Open(rd) - - case bytes.HasPrefix(b, []byte(flv.Signature)): + switch string(b[:3]) { + case flv.Signature: return flv.Open(rd) + } - case bytes.HasPrefix(b, []byte("--")): - return multipart.Open(rd) - - case b[0] == 0xFF && (b[1] == 0xF1 || b[1] == 0xF9): + switch string(b[:2]) { + case "\xFF\xD8": + return mjpeg.Open(rd) + case "\xFF\xF1", "\xFF\xF9": return aac.Open(rd) + case "--": + return mpjpeg.Open(rd) + } - case b[0] == mpegts.SyncByte: + switch b[0] { + case mpegts.SyncByte: return mpegts.Open(rd) } diff --git a/pkg/mjpeg/client.go b/pkg/mjpeg/client.go deleted file mode 100644 index f16c42cd..00000000 --- a/pkg/mjpeg/client.go +++ /dev/null @@ -1,75 +0,0 @@ -package mjpeg - -import ( - "errors" - "io" - "net/http" - - "github.com/AlexxIT/go2rtc/pkg/core" - "github.com/AlexxIT/go2rtc/pkg/tcp" - "github.com/pion/rtp" -) - -type Client struct { - core.Listener - - UserAgent string - RemoteAddr string - - closed bool - res *http.Response - - medias []*core.Media - receiver *core.Receiver - - recv int -} - -func NewClient(res *http.Response) *Client { - return &Client{res: res} -} - -func (c *Client) Handle() error { - body, err := io.ReadAll(c.res.Body) - if err != nil { - return err - } - - pkt := &rtp.Packet{ - Header: rtp.Header{Timestamp: core.Now90000()}, - Payload: body, - } - c.receiver.WriteRTP(pkt) - - c.recv += len(body) - - req := c.res.Request - - for !c.closed { - res, err := tcp.Do(req) - if err != nil { - return err - } - - if res.StatusCode != http.StatusOK { - return errors.New("wrong status: " + res.Status) - } - - body, err = io.ReadAll(res.Body) - if err != nil { - return err - } - - c.recv += len(body) - - if c.receiver != nil { - pkt = &rtp.Packet{ - Header: rtp.Header{Timestamp: core.Now90000()}, - Payload: body, - } - c.receiver.WriteRTP(pkt) - } - } - - return nil -} diff --git a/pkg/mjpeg/consumer.go b/pkg/mjpeg/consumer.go index d5fb0d51..16edc895 100644 --- a/pkg/mjpeg/consumer.go +++ b/pkg/mjpeg/consumer.go @@ -8,26 +8,30 @@ import ( ) type Consumer struct { - core.SuperConsumer + core.Connection wr *core.WriteBuffer } func NewConsumer() *Consumer { - return &Consumer{ - core.SuperConsumer{ - Type: "MJPEG passive consumer", - Medias: []*core.Media{ - { - Kind: core.KindVideo, - Direction: core.DirectionSendonly, - Codecs: []*core.Codec{ - {Name: core.CodecJPEG}, - {Name: core.CodecRAW}, - }, - }, + medias := []*core.Media{ + { + Kind: core.KindVideo, + Direction: core.DirectionSendonly, + Codecs: []*core.Codec{ + {Name: core.CodecJPEG}, + {Name: core.CodecRAW}, }, }, - core.NewWriteBuffer(nil), + } + wr := core.NewWriteBuffer(nil) + return &Consumer{ + Connection: core.Connection{ + ID: core.NewID(), + FormatName: "mjpeg", + Medias: medias, + Transport: wr, + }, + wr: wr, } } @@ -53,8 +57,3 @@ func (c *Consumer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiv func (c *Consumer) WriteTo(wr io.Writer) (int64, error) { return c.wr.WriteTo(wr) } - -func (c *Consumer) Stop() error { - _ = c.SuperConsumer.Close() - return c.wr.Close() -} diff --git a/pkg/mjpeg/producer.go b/pkg/mjpeg/producer.go deleted file mode 100644 index 5b352252..00000000 --- a/pkg/mjpeg/producer.go +++ /dev/null @@ -1,61 +0,0 @@ -package mjpeg - -import ( - "encoding/json" - - "github.com/AlexxIT/go2rtc/pkg/core" -) - -func (c *Client) GetMedias() []*core.Media { - if c.medias == nil { - c.medias = []*core.Media{{ - Kind: core.KindVideo, - Direction: core.DirectionRecvonly, - Codecs: []*core.Codec{ - { - Name: core.CodecJPEG, - ClockRate: 90000, - PayloadType: core.PayloadTypeRAW, - }, - }, - }} - } - return c.medias -} - -func (c *Client) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) { - if c.receiver == nil { - c.receiver = core.NewReceiver(media, codec) - } - return c.receiver, nil -} - -func (c *Client) Start() error { - // https://github.com/AlexxIT/go2rtc/issues/278 - return c.Handle() -} - -func (c *Client) Stop() error { - if c.receiver != nil { - c.receiver.Close() - } - // important for close reader/writer gorutines - _ = c.res.Body.Close() - c.closed = true - return nil -} - -func (c *Client) MarshalJSON() ([]byte, error) { - info := &core.Info{ - Type: "JPEG active producer", - URL: c.res.Request.URL.String(), - RemoteAddr: c.RemoteAddr, - UserAgent: c.UserAgent, - Medias: c.medias, - Recv: c.recv, - } - if c.receiver != nil { - info.Receivers = []*core.Receiver{c.receiver} - } - return json.Marshal(info) -} diff --git a/pkg/mp4/consumer.go b/pkg/mp4/consumer.go index 83b2d2e3..34849863 100644 --- a/pkg/mp4/consumer.go +++ b/pkg/mp4/consumer.go @@ -14,7 +14,7 @@ import ( ) type Consumer struct { - core.SuperConsumer + core.Connection wr *core.WriteBuffer muxer *Muxer mu sync.Mutex @@ -47,12 +47,17 @@ func NewConsumer(medias []*core.Media) *Consumer { } } - cons := &Consumer{ + wr := core.NewWriteBuffer(nil) + return &Consumer{ + Connection: core.Connection{ + ID: core.NewID(), + FormatName: "mp4", + Medias: medias, + Transport: wr, + }, muxer: &Muxer{}, - wr: core.NewWriteBuffer(nil), + wr: wr, } - cons.Medias = medias - return cons } func (c *Consumer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiver) error { @@ -182,8 +187,3 @@ func (c *Consumer) WriteTo(wr io.Writer) (int64, error) { return c.wr.WriteTo(wr) } - -func (c *Consumer) Stop() error { - _ = c.SuperConsumer.Close() - return c.wr.Close() -} diff --git a/pkg/mp4/keyframe.go b/pkg/mp4/keyframe.go index 25a6983d..399f95e7 100644 --- a/pkg/mp4/keyframe.go +++ b/pkg/mp4/keyframe.go @@ -10,11 +10,12 @@ import ( ) type Keyframe struct { - core.SuperConsumer + core.Connection wr *core.WriteBuffer muxer *Muxer } +// Deprecated: should be rewritten func NewKeyframe(medias []*core.Media) *Keyframe { if medias == nil { medias = []*core.Media{ @@ -29,9 +30,15 @@ func NewKeyframe(medias []*core.Media) *Keyframe { } } + wr := core.NewWriteBuffer(nil) cons := &Keyframe{ + Connection: core.Connection{ + ID: core.NewID(), + FormatName: "mp4", + Transport: wr, + }, muxer: &Muxer{}, - wr: core.NewWriteBuffer(nil), + wr: wr, } cons.Medias = medias return cons @@ -95,8 +102,3 @@ func (c *Keyframe) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiv func (c *Keyframe) WriteTo(wr io.Writer) (int64, error) { return c.wr.WriteTo(wr) } - -func (c *Keyframe) Stop() error { - _ = c.SuperConsumer.Close() - return c.wr.Close() -} diff --git a/pkg/mpegts/consumer.go b/pkg/mpegts/consumer.go index eb0902fc..fcb57c74 100644 --- a/pkg/mpegts/consumer.go +++ b/pkg/mpegts/consumer.go @@ -11,17 +11,13 @@ import ( ) type Consumer struct { - core.SuperConsumer + core.Connection muxer *Muxer wr *core.WriteBuffer } func NewConsumer() *Consumer { - c := &Consumer{ - muxer: NewMuxer(), - wr: core.NewWriteBuffer(nil), - } - c.Medias = []*core.Media{ + medias := []*core.Media{ { Kind: core.KindVideo, Direction: core.DirectionSendonly, @@ -38,7 +34,17 @@ func NewConsumer() *Consumer { }, }, } - return c + wr := core.NewWriteBuffer(nil) + return &Consumer{ + core.Connection{ + ID: core.NewID(), + FormatName: "mpegts", + Medias: medias, + Transport: wr, + }, + NewMuxer(), + wr, + } } func (c *Consumer) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error { @@ -110,14 +116,9 @@ func (c *Consumer) WriteTo(wr io.Writer) (int64, error) { return c.wr.WriteTo(wr) } -func (c *Consumer) Stop() error { - _ = c.SuperConsumer.Close() - return c.wr.Close() -} - -func TimestampFromRTP(rtp *rtp.Packet, codec *core.Codec) { - if codec.ClockRate == ClockRate { - return - } - rtp.Timestamp = uint32(float64(rtp.Timestamp) / float64(codec.ClockRate) * ClockRate) -} +//func TimestampFromRTP(rtp *rtp.Packet, codec *core.Codec) { +// if codec.ClockRate == ClockRate { +// return +// } +// rtp.Timestamp = uint32(float64(rtp.Timestamp) / float64(codec.ClockRate) * ClockRate) +//} diff --git a/pkg/mpegts/producer.go b/pkg/mpegts/producer.go index 78f320a2..2c72d8aa 100644 --- a/pkg/mpegts/producer.go +++ b/pkg/mpegts/producer.go @@ -13,12 +13,19 @@ import ( ) type Producer struct { - core.SuperProducer + core.Connection rd *core.ReadBuffer } func Open(rd io.Reader) (*Producer, error) { - prod := &Producer{rd: core.NewReadBuffer(rd)} + prod := &Producer{ + Connection: core.Connection{ + ID: core.NewID(), + FormatName: "mpegts", + Transport: rd, + }, + rd: core.NewReadBuffer(rd), + } if err := prod.probe(); err != nil { return nil, err } @@ -26,7 +33,7 @@ func Open(rd io.Reader) (*Producer, error) { } func (c *Producer) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) { - receiver, _ := c.SuperProducer.GetTrack(media, codec) + receiver, _ := c.Connection.GetTrack(media, codec) receiver.ID = StreamType(codec) return receiver, nil } @@ -40,6 +47,8 @@ func (c *Producer) Start() error { return err } + c.Recv += len(pkt.Payload) + //log.Printf("[mpegts] size: %6d, muxer: %10d, pt: %2d", len(pkt.Payload), pkt.Timestamp, pkt.PayloadType) for _, receiver := range c.Receivers { @@ -52,11 +61,6 @@ func (c *Producer) Start() error { } } -func (c *Producer) Stop() error { - _ = c.SuperProducer.Close() - return c.rd.Close() -} - func (c *Producer) probe() error { c.rd.BufferSize = core.ProbeSize defer c.rd.Reset() diff --git a/pkg/multipart/multipart.go b/pkg/mpjpeg/multipart.go similarity index 98% rename from pkg/multipart/multipart.go rename to pkg/mpjpeg/multipart.go index aea1b828..abceea43 100644 --- a/pkg/multipart/multipart.go +++ b/pkg/mpjpeg/multipart.go @@ -1,4 +1,4 @@ -package multipart +package mpjpeg import ( "bufio" diff --git a/pkg/mpjpeg/producer.go b/pkg/mpjpeg/producer.go new file mode 100644 index 00000000..a8d5e16a --- /dev/null +++ b/pkg/mpjpeg/producer.go @@ -0,0 +1,65 @@ +package mpjpeg + +import ( + "bufio" + "errors" + "io" + + "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/pion/rtp" +) + +type Producer struct { + core.Connection + rd *bufio.Reader +} + +func Open(rd io.Reader) (*Producer, error) { + return &Producer{ + Connection: core.Connection{ + ID: core.NewID(), + FormatName: "mpjpeg", // Multipart JPEG + Transport: rd, + Medias: []*core.Media{ + { + Kind: core.KindVideo, + Direction: core.DirectionRecvonly, + Codecs: []*core.Codec{ + { + Name: core.CodecJPEG, + ClockRate: 90000, + PayloadType: core.PayloadTypeRAW, + }, + }, + }, + }, + }, + }, nil +} + +func (c *Producer) Start() error { + if len(c.Receivers) != 1 { + return errors.New("mjpeg: no receivers") + } + + rd := bufio.NewReader(c.Transport.(io.Reader)) + + mjpeg := c.Receivers[0] + + for { + _, body, err := Next(rd) + if err != nil { + return err + } + + c.Recv += len(body) + + if mjpeg != nil { + packet := &rtp.Packet{ + Header: rtp.Header{Timestamp: core.Now90000()}, + Payload: body, + } + mjpeg.WriteRTP(packet) + } + } +} diff --git a/pkg/multipart/producer.go b/pkg/multipart/producer.go deleted file mode 100644 index 70a2c547..00000000 --- a/pkg/multipart/producer.go +++ /dev/null @@ -1,68 +0,0 @@ -package multipart - -import ( - "bufio" - "errors" - "io" - - "github.com/AlexxIT/go2rtc/pkg/core" - "github.com/pion/rtp" -) - -type Producer struct { - core.SuperProducer - closer io.Closer - reader *bufio.Reader -} - -func Open(rd io.Reader) (*Producer, error) { - prod := &Producer{ - closer: rd.(io.Closer), - reader: bufio.NewReader(rd), - } - prod.Medias = []*core.Media{ - { - Kind: core.KindVideo, - Direction: core.DirectionRecvonly, - Codecs: []*core.Codec{ - { - Name: core.CodecJPEG, - ClockRate: 90000, - PayloadType: core.PayloadTypeRAW, - }, - }, - }, - } - prod.Type = "Multipart producer" - return prod, nil -} - -func (c *Producer) Start() error { - if len(c.Receivers) != 1 { - return errors.New("mjpeg: no receivers") - } - - mjpeg := c.Receivers[0] - - for { - _, body, err := Next(c.reader) - if err != nil { - return err - } - - c.Recv += len(body) - - if mjpeg != nil { - packet := &rtp.Packet{ - Header: rtp.Header{Timestamp: core.Now90000()}, - Payload: body, - } - mjpeg.WriteRTP(packet) - } - } -} - -func (c *Producer) Stop() error { - _ = c.SuperProducer.Close() - return c.closer.Close() -} diff --git a/pkg/nest/client.go b/pkg/nest/client.go index 2169773b..0b243384 100644 --- a/pkg/nest/client.go +++ b/pkg/nest/client.go @@ -48,8 +48,10 @@ func Dial(rawURL string) (*Client, error) { } conn := webrtc.NewConn(pc) - conn.Desc = "Nest" + conn.FormatName = "nest/webrtc" conn.Mode = core.ModeActiveProducer + conn.Protocol = "http" + conn.URL = rawURL // https://developers.google.com/nest/device-access/traits/device/camera-live-stream#generatewebrtcstream-request-fields medias := []*core.Media{ diff --git a/pkg/probe/probe.go b/pkg/probe/producer.go similarity index 72% rename from pkg/probe/probe.go rename to pkg/probe/producer.go index 61a2b361..1fbd3efb 100644 --- a/pkg/probe/probe.go +++ b/pkg/probe/producer.go @@ -8,17 +8,11 @@ import ( ) type Probe struct { - Type string `json:"type,omitempty"` - RemoteAddr string `json:"remote_addr,omitempty"` - UserAgent string `json:"user_agent,omitempty"` - Medias []*core.Media `json:"medias,omitempty"` - Receivers []*core.Receiver `json:"receivers,omitempty"` - Senders []*core.Sender `json:"senders,omitempty"` + core.Connection } func NewProbe(query url.Values) *Probe { - c := &Probe{Type: "probe"} - c.Medias = core.ParseQuery(query) + medias := core.ParseQuery(query) for _, value := range query["microphone"] { media := &core.Media{Kind: core.KindAudio, Direction: core.DirectionRecvonly} @@ -32,10 +26,16 @@ func NewProbe(query url.Values) *Probe { media.Codecs = append(media.Codecs, &core.Codec{Name: name}) } - c.Medias = append(c.Medias, media) + medias = append(medias, media) } - return c + return &Probe{ + Connection: core.Connection{ + ID: core.NewID(), + FormatName: "probe", + Medias: medias, + }, + } } func (p *Probe) GetMedias() []*core.Media { diff --git a/pkg/roborock/client.go b/pkg/roborock/client.go index 522b0e13..ef221e65 100644 --- a/pkg/roborock/client.go +++ b/pkg/roborock/client.go @@ -18,6 +18,7 @@ import ( pion "github.com/pion/webrtc/v3" ) +// Deprecated: should be rewritten to core.Connection type Client struct { core.Listener @@ -110,8 +111,10 @@ func (c *Client) Connect() error { var sendOffer sync.WaitGroup c.conn = webrtc.NewConn(pc) - c.conn.Desc = "Roborock" + c.conn.FormatName = "roborock" c.conn.Mode = core.ModeActiveProducer + c.conn.Protocol = "mqtt" + c.conn.URL = c.url c.conn.Listen(func(msg any) { switch msg := msg.(type) { case *pion.ICECandidate: diff --git a/pkg/rtmp/client.go b/pkg/rtmp/client.go index aff8e23c..138d727d 100644 --- a/pkg/rtmp/client.go +++ b/pkg/rtmp/client.go @@ -8,10 +8,11 @@ import ( "strings" "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/AlexxIT/go2rtc/pkg/flv" "github.com/AlexxIT/go2rtc/pkg/tcp" ) -func DialPlay(rawURL string) (core.Producer, error) { +func DialPlay(rawURL string) (*flv.Producer, error) { u, err := url.Parse(rawURL) if err != nil { return nil, err @@ -22,16 +23,16 @@ func DialPlay(rawURL string) (core.Producer, error) { return nil, err } - rtmpConn, err := NewClient(conn, u) + client, err := NewClient(conn, u) if err != nil { return nil, err } - if err = rtmpConn.play(); err != nil { + if err = client.play(); err != nil { return nil, err } - return rtmpConn.Producer() + return client.Producer() } func DialPublish(rawURL string) (io.Writer, error) { diff --git a/pkg/rtmp/flv.go b/pkg/rtmp/flv.go index 87bef0a8..350f4c3c 100644 --- a/pkg/rtmp/flv.go +++ b/pkg/rtmp/flv.go @@ -1,11 +1,10 @@ package rtmp import ( - "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/flv" ) -func (c *Conn) Producer() (core.Producer, error) { +func (c *Conn) Producer() (*flv.Producer, error) { c.rdBuf = []byte{ 'F', 'L', 'V', // signature 1, // version @@ -13,7 +12,17 @@ func (c *Conn) Producer() (core.Producer, error) { 0, 0, 0, 9, // header size } - return flv.Open(c) + prod, err := flv.Open(c) + if err != nil { + return nil, err + } + + prod.FormatName = "rtmp" + prod.Protocol = "rtmp" + prod.RemoteAddr = c.conn.RemoteAddr().String() + prod.URL = c.url + + return prod, nil } // Read - convert RTMP to FLV format diff --git a/pkg/rtsp/client.go b/pkg/rtsp/client.go index 9002d0a1..352c00a1 100644 --- a/pkg/rtsp/client.go +++ b/pkg/rtsp/client.go @@ -20,7 +20,13 @@ import ( var Timeout = time.Second * 5 func NewClient(uri string) *Conn { - return &Conn{uri: uri} + return &Conn{ + Connection: core.Connection{ + ID: core.NewID(), + FormatName: "rtsp", + }, + uri: uri, + } } func (c *Conn) Dial() (err error) { @@ -36,8 +42,10 @@ func (c *Conn) Dial() (err error) { timeout = time.Second * time.Duration(c.Timeout) } conn, err = tcp.Dial(c.URL, timeout) + c.Protocol = "rtsp+tcp" } else { conn, err = websocket.Dial(c.Transport) + c.Protocol = "ws" } if err != nil { return @@ -53,6 +61,10 @@ func (c *Conn) Dial() (err error) { c.sequence = 0 c.state = StateConn + c.Connection.RemoteAddr = conn.RemoteAddr().String() + c.Connection.Transport = conn + c.Connection.URL = c.uri + return nil } @@ -143,7 +155,7 @@ func (c *Conn) Describe() error { } } - c.sdp = string(res.Body) // for info + c.SDP = string(res.Body) // for info medias, err := UnmarshalSDP(res.Body) if err != nil { diff --git a/pkg/rtsp/conn.go b/pkg/rtsp/conn.go index 1d9edf06..0c2009d7 100644 --- a/pkg/rtsp/conn.go +++ b/pkg/rtsp/conn.go @@ -18,6 +18,7 @@ import ( ) type Conn struct { + core.Connection core.Listener // public @@ -30,9 +31,7 @@ type Conn struct { Timeout int Transport string // custom transport support, ex. RTSP over WebSocket - Medias []*core.Media - UserAgent string - URL *url.URL + URL *url.URL // internal @@ -44,19 +43,10 @@ type Conn struct { reader *bufio.Reader sequence int session string - sdp string uri string state State stateMu sync.Mutex - - receivers []*core.Receiver - senders []*core.Sender - - // stats - - recv int - send int } const ( @@ -114,7 +104,7 @@ func (c *Conn) Handle() (err error) { // polling frames from remote RTSP Server (ex Camera) timeout = time.Second * 5 - if len(c.receivers) == 0 { + if len(c.Receivers) == 0 { // if we only send audio to camera // https://github.com/AlexxIT/go2rtc/issues/659 timeout += keepaliveDT @@ -239,7 +229,7 @@ func (c *Conn) Handle() (err error) { return } - c.recv += int(size) + c.Recv += int(size) if channelID&1 == 0 { packet := &rtp.Packet{} @@ -247,7 +237,7 @@ func (c *Conn) Handle() (err error) { return } - for _, receiver := range c.receivers { + for _, receiver := range c.Receivers { if receiver.ID == channelID { receiver.WriteRTP(packet) break diff --git a/pkg/rtsp/consumer.go b/pkg/rtsp/consumer.go index 79e2b348..b6df188f 100644 --- a/pkg/rtsp/consumer.go +++ b/pkg/rtsp/consumer.go @@ -18,15 +18,6 @@ func (c *Conn) GetMedias() []*core.Media { } func (c *Conn) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) (err error) { - core.Assert(media.Direction == core.DirectionSendonly) - - for _, sender := range c.senders { - if sender.Codec == codec { - sender.HandleRTP(track) - return - } - } - var channel byte switch c.mode { @@ -47,12 +38,12 @@ func (c *Conn) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiv c.state = StateSetup case core.ModePassiveConsumer: - channel = byte(len(c.senders)) * 2 + channel = byte(len(c.Senders)) * 2 // for consumer is better to use original track codec codec = track.Codec.Clone() // generate new payload type, starting from 96 - codec.PayloadType = byte(96 + len(c.senders)) + codec.PayloadType = byte(96 + len(c.Senders)) default: panic(core.Caller()) @@ -70,7 +61,7 @@ func (c *Conn) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiv sender.HandleRTP(track) - c.senders = append(c.senders, sender) + c.Senders = append(c.Senders, sender) return nil } @@ -99,7 +90,7 @@ func (c *Conn) packetWriter(codec *core.Codec, channel, payloadType uint8) core. } //log.Printf("[rtsp] channel:%2d write_size:%6d buffer_size:%6d", channel, n, len(buf)) if _, err := c.conn.Write(buf[:n]); err == nil { - c.send += n + c.Send += n } n = 0 } diff --git a/pkg/rtsp/producer.go b/pkg/rtsp/producer.go index d0f36a1c..de115808 100644 --- a/pkg/rtsp/producer.go +++ b/pkg/rtsp/producer.go @@ -10,7 +10,7 @@ import ( func (c *Conn) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) { core.Assert(media.Direction == core.DirectionRecvonly) - for _, track := range c.receivers { + for _, track := range c.Receivers { if track.Codec == codec { return track, nil } @@ -34,7 +34,7 @@ func (c *Conn) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, e track := core.NewReceiver(media, codec) track.ID = channel - c.receivers = append(c.receivers, track) + c.Receivers = append(c.Receivers, track) return track, nil } @@ -81,10 +81,10 @@ func (c *Conn) Start() (err error) { } func (c *Conn) Stop() (err error) { - for _, receiver := range c.receivers { + for _, receiver := range c.Receivers { receiver.Close() } - for _, sender := range c.senders { + for _, sender := range c.Senders { sender.Close() } @@ -99,25 +99,7 @@ func (c *Conn) Stop() (err error) { } func (c *Conn) MarshalJSON() ([]byte, error) { - info := &core.Info{ - Type: "RTSP " + c.mode.String(), - SDP: c.sdp, - UserAgent: c.UserAgent, - Medias: c.Medias, - Receivers: c.receivers, - Senders: c.senders, - Recv: c.recv, - Send: c.send, - } - - if c.URL != nil { - info.URL = c.URL.String() - } - if c.conn != nil { - info.RemoteAddr = c.conn.RemoteAddr().String() - } - - return json.Marshal(info) + return json.Marshal(c.Connection) } func (c *Conn) Reconnect() error { @@ -135,12 +117,12 @@ func (c *Conn) Reconnect() error { } // restore previous medias - for _, receiver := range c.receivers { + for _, receiver := range c.Receivers { if _, err := c.SetupMedia(receiver.Media); err != nil { return err } } - for _, sender := range c.senders { + for _, sender := range c.Senders { if _, err := c.SetupMedia(sender.Media); err != nil { return err } diff --git a/pkg/rtsp/server.go b/pkg/rtsp/server.go index 8e0d3134..7953b0dc 100644 --- a/pkg/rtsp/server.go +++ b/pkg/rtsp/server.go @@ -14,10 +14,16 @@ import ( ) func NewServer(conn net.Conn) *Conn { - c := new(Conn) - c.conn = conn - c.reader = bufio.NewReader(conn) - return c + return &Conn{ + Connection: core.Connection{ + ID: core.NewID(), + FormatName: "rtsp", + Protocol: "rtsp+tcp", + RemoteAddr: conn.RemoteAddr().String(), + }, + conn: conn, + reader: bufio.NewReader(conn), + } } func (c *Conn) Auth(username, password string) { @@ -70,7 +76,7 @@ func (c *Conn) Accept() error { return errors.New("wrong content type") } - c.sdp = string(req.Body) // for info + c.SDP = string(req.Body) // for info c.Medias, err = UnmarshalSDP(req.Body) if err != nil { @@ -81,7 +87,7 @@ func (c *Conn) Accept() error { for i, media := range c.Medias { track := core.NewReceiver(media, media.Codecs[0]) track.ID = byte(i * 2) - c.receivers = append(c.receivers, track) + c.Receivers = append(c.Receivers, track) } c.mode = core.ModePassiveProducer @@ -96,7 +102,7 @@ func (c *Conn) Accept() error { c.mode = core.ModePassiveConsumer c.Fire(MethodDescribe) - if c.senders == nil { + if c.Senders == nil { res := &tcp.Response{ Status: "404 Not Found", Request: req, @@ -113,7 +119,7 @@ func (c *Conn) Accept() error { // convert tracks to real output medias medias var medias []*core.Media - for i, track := range c.senders { + for i, track := range c.Senders { media := &core.Media{ Kind: core.GetKind(track.Codec.Name), Direction: core.DirectionRecvonly, @@ -128,7 +134,7 @@ func (c *Conn) Accept() error { return err } - c.sdp = string(res.Body) // for info + c.SDP = string(res.Body) // for info if err = c.WriteResponse(res); err != nil { return err @@ -148,9 +154,9 @@ func (c *Conn) Accept() error { c.state = StateSetup if c.mode == core.ModePassiveConsumer { - if i := reqTrackID(req); i >= 0 && i < len(c.senders) { + if i := reqTrackID(req); i >= 0 && i < len(c.Senders) { // mark sender as SETUP - c.senders[i].Media.ID = MethodSetup + c.Senders[i].Media.ID = MethodSetup tr = fmt.Sprintf("RTP/AVP/TCP;unicast;interleaved=%d-%d", i*2, i*2+1) res.Header.Set("Transport", tr) } else { @@ -170,7 +176,7 @@ func (c *Conn) Accept() error { case MethodRecord, MethodPlay: if c.mode == core.ModePassiveConsumer { // stop unconfigured senders - for _, track := range c.senders { + for _, track := range c.Senders { if track.Media.ID != MethodSetup { track.Close() } diff --git a/pkg/stdin/consumer.go b/pkg/stdin/backchannel.go similarity index 88% rename from pkg/stdin/consumer.go rename to pkg/stdin/backchannel.go index a1284948..b9a4a6d4 100644 --- a/pkg/stdin/consumer.go +++ b/pkg/stdin/backchannel.go @@ -49,10 +49,12 @@ func (c *Client) Stop() (err error) { } func (c *Client) MarshalJSON() ([]byte, error) { - info := &core.Info{ - Type: "Exec active consumer", - Medias: c.medias, - Send: c.send, + info := &core.Connection{ + ID: core.ID(c), + FormatName: "exec", + Protocol: "pipe", + Medias: c.medias, + Send: c.send, } if c.sender != nil { info.Senders = []*core.Sender{c.sender} diff --git a/pkg/stdin/client.go b/pkg/stdin/client.go index 51db30ee..09e525ad 100644 --- a/pkg/stdin/client.go +++ b/pkg/stdin/client.go @@ -6,6 +6,7 @@ import ( "github.com/AlexxIT/go2rtc/pkg/core" ) +// Deprecated: should be rewritten to core.Connection type Client struct { cmd *exec.Cmd diff --git a/pkg/tapo/consumer.go b/pkg/tapo/backchannel.go similarity index 100% rename from pkg/tapo/consumer.go rename to pkg/tapo/backchannel.go diff --git a/pkg/tapo/client.go b/pkg/tapo/client.go index ed79e500..3585011c 100644 --- a/pkg/tapo/client.go +++ b/pkg/tapo/client.go @@ -23,6 +23,7 @@ import ( "github.com/AlexxIT/go2rtc/pkg/tcp" ) +// Deprecated: should be rewritten to core.Connection type Client struct { core.Listener diff --git a/pkg/tapo/producer.go b/pkg/tapo/producer.go index ac213e15..7d66d907 100644 --- a/pkg/tapo/producer.go +++ b/pkg/tapo/producer.go @@ -2,6 +2,7 @@ package tapo import ( "encoding/json" + "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/mpegts" ) @@ -74,15 +75,20 @@ func (c *Client) Stop() error { } func (c *Client) MarshalJSON() ([]byte, error) { - info := &core.Info{ - Type: "Tapo active producer", - Medias: c.medias, - Recv: c.recv, - Receivers: c.receivers, - Send: c.send, + info := &core.Connection{ + ID: core.ID(c), + FormatName: "tapo", + Protocol: "http", + Medias: c.medias, + Recv: c.recv, + Receivers: c.receivers, + Send: c.send, } if c.sender != nil { info.Senders = []*core.Sender{c.sender} } + if c.conn1 != nil { + info.RemoteAddr = c.conn1.RemoteAddr().String() + } return json.Marshal(info) } diff --git a/pkg/tcp/helpers.go b/pkg/tcp/helpers.go deleted file mode 100644 index 9db42a89..00000000 --- a/pkg/tcp/helpers.go +++ /dev/null @@ -1,12 +0,0 @@ -package tcp - -import ( - "net/http" -) - -func RemoteAddr(r *http.Request) string { - if remote := r.Header.Get("X-Forwarded-For"); remote != "" { - return remote + ", " + r.RemoteAddr - } - return r.RemoteAddr -} diff --git a/pkg/wav/wav.go b/pkg/wav/producer.go similarity index 89% rename from pkg/wav/wav.go rename to pkg/wav/producer.go index 5f572bd6..63f6d01a 100644 --- a/pkg/wav/wav.go +++ b/pkg/wav/producer.go @@ -54,22 +54,27 @@ func Open(r io.Reader) (*Producer, error) { return nil, errors.New("waw: unsupported codec") } - prod := &Producer{rd: rd, cl: r.(io.Closer)} - prod.Type = "WAV producer" - prod.Medias = []*core.Media{ + medias := []*core.Media{ { Kind: core.KindAudio, Direction: core.DirectionRecvonly, Codecs: []*core.Codec{codec}, }, } - return prod, nil + return &Producer{ + Connection: core.Connection{ + ID: core.NewID(), + FormatName: "wav", + Medias: medias, + Transport: r, + }, + rd: rd, + }, nil } type Producer struct { - core.SuperProducer + core.Connection rd *bufio.Reader - cl io.Closer } func (c *Producer) Start() error { @@ -106,11 +111,6 @@ func (c *Producer) Start() error { } } -func (c *Producer) Stop() error { - _ = c.SuperProducer.Close() - return c.cl.Close() -} - func readChunk(r io.Reader) (chunkID string, data []byte, err error) { b := make([]byte, 8) if _, err = io.ReadFull(r, b); err != nil { diff --git a/pkg/webrtc/client.go b/pkg/webrtc/client.go index 50c7773d..9a7a7b2f 100644 --- a/pkg/webrtc/client.go +++ b/pkg/webrtc/client.go @@ -71,7 +71,7 @@ func (c *Conn) SetAnswer(answer string) (err error) { return } - c.medias = UnmarshalMedias(sd.MediaDescriptions) + c.Medias = UnmarshalMedias(sd.MediaDescriptions) return nil } diff --git a/pkg/webrtc/conn.go b/pkg/webrtc/conn.go index 0e10874e..3e3ecc4f 100644 --- a/pkg/webrtc/conn.go +++ b/pkg/webrtc/conn.go @@ -1,6 +1,9 @@ package webrtc import ( + "encoding/json" + "fmt" + "strings" "time" "github.com/AlexxIT/go2rtc/pkg/core" @@ -10,28 +13,25 @@ import ( ) type Conn struct { + core.Connection core.Listener - UserAgent string - Desc string - Mode core.Mode + Mode core.Mode `json:"mode"` pc *webrtc.PeerConnection - medias []*core.Media - receivers []*core.Receiver - senders []*core.Sender - - recv int - send int - offer string - remote string closed core.Waiter } func NewConn(pc *webrtc.PeerConnection) *Conn { - c := &Conn{pc: pc} + c := &Conn{ + Connection: core.Connection{ + ID: core.NewID(), + FormatName: "webrtc", + }, + pc: pc, + } pc.OnICECandidate(func(candidate *webrtc.ICECandidate) { // last candidate will be empty @@ -50,7 +50,15 @@ func NewConn(pc *webrtc.PeerConnection) *Conn { } pc.SCTP().Transport().ICETransport().OnSelectedCandidatePairChange( func(pair *webrtc.ICECandidatePair) { - c.remote = pair.Remote.String() + c.Protocol += "+" + pair.Remote.Protocol.String() + c.RemoteAddr = fmt.Sprintf( + "%s:%d %s", sanitizeIP6(pair.Remote.Address), pair.Remote.Port, pair.Remote.Typ, + ) + if pair.Remote.RelatedAddress != "" { + c.RemoteAddr += fmt.Sprintf( + " %s:%d", sanitizeIP6(pair.Remote.RelatedAddress), pair.Remote.RelatedPort, + ) + } }, ) }) @@ -92,7 +100,7 @@ func NewConn(pc *webrtc.PeerConnection) *Conn { return } - c.recv += n + c.Recv += n packet := &rtp.Packet{} if err := packet.Unmarshal(b[:n]); err != nil { @@ -121,7 +129,7 @@ func NewConn(pc *webrtc.PeerConnection) *Conn { switch state { case webrtc.PeerConnectionStateConnected: - for _, sender := range c.senders { + for _, sender := range c.Senders { sender.Start() } case webrtc.PeerConnectionStateDisconnected, webrtc.PeerConnectionStateFailed, webrtc.PeerConnectionStateClosed: @@ -134,6 +142,10 @@ func NewConn(pc *webrtc.PeerConnection) *Conn { return c } +func (c *Conn) MarshalJSON() ([]byte, error) { + return json.Marshal(c.Connection) +} + func (c *Conn) Close() error { c.closed.Done(nil) return c.pc.Close() @@ -172,7 +184,7 @@ func (c *Conn) getMediaCodec(remote *webrtc.TrackRemote) (*core.Media, *core.Cod } // search Media for this MID - for _, media := range c.medias { + for _, media := range c.Medias { if media.ID != tr.Mid() || media.Direction != core.DirectionRecvonly { continue } @@ -194,3 +206,10 @@ func (c *Conn) getMediaCodec(remote *webrtc.TrackRemote) (*core.Media, *core.Cod return nil, nil } + +func sanitizeIP6(host string) string { + if strings.IndexByte(host, ':') > 0 { + return "[" + host + "]" + } + return host +} diff --git a/pkg/webrtc/consumer.go b/pkg/webrtc/consumer.go index 3bcaf49a..2dcab436 100644 --- a/pkg/webrtc/consumer.go +++ b/pkg/webrtc/consumer.go @@ -1,7 +1,6 @@ package webrtc import ( - "encoding/json" "errors" "github.com/AlexxIT/go2rtc/pkg/core" @@ -12,13 +11,13 @@ import ( ) func (c *Conn) GetMedias() []*core.Media { - return WithResampling(c.medias) + return WithResampling(c.Medias) } func (c *Conn) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error { core.Assert(media.Direction == core.DirectionSendonly) - for _, sender := range c.senders { + for _, sender := range c.Senders { if sender.Codec == codec { sender.Bind(track) return nil @@ -42,7 +41,7 @@ func (c *Conn) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiv sender := core.NewSender(media, codec) sender.Handler = func(packet *rtp.Packet) { - c.send += packet.MarshalSize() + c.Send += packet.MarshalSize() //important to send with remote PayloadType _ = localTrack.WriteRTP(payloadType, packet) } @@ -85,20 +84,6 @@ func (c *Conn) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiv sender.HandleRTP(track) } - c.senders = append(c.senders, sender) + c.Senders = append(c.Senders, sender) return nil } - -func (c *Conn) MarshalJSON() ([]byte, error) { - info := &core.Info{ - Type: c.Desc + " " + c.Mode.String(), - RemoteAddr: c.remote, - UserAgent: c.UserAgent, - Medias: c.medias, - Receivers: c.receivers, - Senders: c.senders, - Recv: c.recv, - Send: c.send, - } - return json.Marshal(info) -} diff --git a/pkg/webrtc/producer.go b/pkg/webrtc/producer.go index d4136a5c..a0910c39 100644 --- a/pkg/webrtc/producer.go +++ b/pkg/webrtc/producer.go @@ -8,7 +8,7 @@ import ( func (c *Conn) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) { core.Assert(media.Direction == core.DirectionRecvonly) - for _, track := range c.receivers { + for _, track := range c.Receivers { if track.Codec == codec { return track, nil } @@ -39,7 +39,7 @@ func (c *Conn) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, e } track := core.NewReceiver(media, codec) - c.receivers = append(c.receivers, track) + c.Receivers = append(c.Receivers, track) return track, nil } @@ -47,13 +47,3 @@ func (c *Conn) Start() error { c.closed.Wait() return nil } - -func (c *Conn) Stop() error { - for _, receiver := range c.receivers { - receiver.Close() - } - for _, sender := range c.senders { - sender.Close() - } - return c.pc.Close() -} diff --git a/pkg/webrtc/server.go b/pkg/webrtc/server.go index ce462e45..9cc89778 100644 --- a/pkg/webrtc/server.go +++ b/pkg/webrtc/server.go @@ -42,7 +42,7 @@ func (c *Conn) SetOffer(offer string) (err error) { } } - c.medias = UnmarshalMedias(sd.MediaDescriptions) + c.Medias = UnmarshalMedias(sd.MediaDescriptions) return } @@ -57,7 +57,7 @@ func (c *Conn) GetAnswer() (answer string, err error) { // disable transceivers if we don't have track, make direction=inactive transeivers: for _, tr := range c.pc.GetTransceivers() { - for _, sender := range c.senders { + for _, sender := range c.Senders { if sender.Media.ID == tr.Mid() { continue transeivers } diff --git a/pkg/webtorrent/client.go b/pkg/webtorrent/client.go index de6b21c7..3594679d 100644 --- a/pkg/webtorrent/client.go +++ b/pkg/webtorrent/client.go @@ -3,19 +3,21 @@ package webtorrent import ( "encoding/base64" "fmt" + "strconv" + "time" + "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/webrtc" "github.com/gorilla/websocket" pion "github.com/pion/webrtc/v3" - "strconv" - "time" ) func NewClient(tracker, share, pwd string, pc *pion.PeerConnection) (*webrtc.Conn, error) { // 1. Create WebRTC producer prod := webrtc.NewConn(pc) - prod.Desc = "WebRTC/WebTorrent sync" + prod.FormatName = "webtorrent" prod.Mode = core.ModeActiveProducer + prod.Protocol = "ws" medias := []*core.Media{ {Kind: core.KindVideo, Direction: core.DirectionRecvonly}, diff --git a/pkg/y4m/consumer.go b/pkg/y4m/consumer.go index 01bece31..dd9b46e9 100644 --- a/pkg/y4m/consumer.go +++ b/pkg/y4m/consumer.go @@ -9,14 +9,17 @@ import ( ) type Consumer struct { - core.SuperConsumer + core.Connection wr *core.WriteBuffer } func NewConsumer() *Consumer { + wr := core.NewWriteBuffer(nil) return &Consumer{ - core.SuperConsumer{ - Type: "YUV4MPEG2 passive consumer", + core.Connection{ + ID: core.NewID(), + Transport: wr, + FormatName: "yuv4mpegpipe", Medias: []*core.Media{ { Kind: core.KindVideo, @@ -27,7 +30,7 @@ func NewConsumer() *Consumer { }, }, }, - core.NewWriteBuffer(nil), + wr, } } @@ -60,8 +63,3 @@ func (c *Consumer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiv func (c *Consumer) WriteTo(wr io.Writer) (int64, error) { return c.wr.WriteTo(wr) } - -func (c *Consumer) Stop() error { - _ = c.SuperConsumer.Close() - return c.wr.Close() -} diff --git a/pkg/y4m/producer.go b/pkg/y4m/producer.go index 05f98a6f..ee2dd731 100644 --- a/pkg/y4m/producer.go +++ b/pkg/y4m/producer.go @@ -2,7 +2,6 @@ package y4m import ( "bufio" - "bytes" "errors" "io" @@ -19,41 +18,13 @@ func Open(r io.Reader) (*Producer, error) { b = b[:len(b)-1] // remove \n - sdp := string(b) - var fmtp string - - for b != nil { - // YUV4MPEG2 W1280 H720 F24:1 Ip A1:1 C420mpeg2 XYSCSS=420MPEG2 - // https://manned.org/yuv4mpeg.5 - // https://github.com/FFmpeg/FFmpeg/blob/master/libavformat/yuv4mpegenc.c - key := b[0] - var value string - if i := bytes.IndexByte(b, ' '); i > 0 { - value = string(b[1:i]) - b = b[i+1:] - } else { - value = string(b[1:]) - b = nil - } - - switch key { - case 'W': - fmtp = "width=" + value - case 'H': - fmtp += ";height=" + value - case 'C': - fmtp += ";colorspace=" + value - } - } + fmtp := ParseHeader(b) if GetSize(fmtp) == 0 { - return nil, errors.New("y4m: unsupported format: " + sdp) + return nil, errors.New("y4m: unsupported format: " + string(b)) } - prod := &Producer{rd: rd, cl: r.(io.Closer)} - prod.Type = "YUV4MPEG2 producer" - prod.SDP = sdp - prod.Medias = []*core.Media{ + medias := []*core.Media{ { Kind: core.KindVideo, Direction: core.DirectionRecvonly, @@ -67,14 +38,21 @@ func Open(r io.Reader) (*Producer, error) { }, }, } - - return prod, nil + return &Producer{ + Connection: core.Connection{ + ID: core.NewID(), + FormatName: "yuv4mpegpipe", + Medias: medias, + SDP: string(b), + Transport: r, + }, + rd: rd, + }, nil } type Producer struct { - core.SuperProducer + core.Connection rd *bufio.Reader - cl io.Closer } func (c *Producer) Start() error { @@ -103,8 +81,3 @@ func (c *Producer) Start() error { c.Receivers[0].WriteRTP(pkt) } } - -func (c *Producer) Stop() error { - _ = c.SuperProducer.Close() - return c.cl.Close() -} diff --git a/pkg/y4m/y4m.go b/pkg/y4m/y4m.go index 8184ea97..4ac54da6 100644 --- a/pkg/y4m/y4m.go +++ b/pkg/y4m/y4m.go @@ -1,6 +1,7 @@ package y4m import ( + "bytes" "image" "github.com/AlexxIT/go2rtc/pkg/core" @@ -10,6 +11,34 @@ const FourCC = "YUV4" const frameHdr = "FRAME\n" +func ParseHeader(b []byte) (fmtp string) { + for b != nil { + // YUV4MPEG2 W1280 H720 F24:1 Ip A1:1 C420mpeg2 XYSCSS=420MPEG2 + // https://manned.org/yuv4mpeg.5 + // https://github.com/FFmpeg/FFmpeg/blob/master/libavformat/yuv4mpegenc.c + key := b[0] + + var value string + if i := bytes.IndexByte(b, ' '); i > 0 { + value = string(b[1:i]) + b = b[i+1:] + } else { + value = string(b[1:]) + b = nil + } + + switch key { + case 'W': + fmtp = "width=" + value + case 'H': + fmtp += ";height=" + value + case 'C': + fmtp += ";colorspace=" + value + } + } + return +} + func GetSize(fmtp string) int { w := core.Atoi(core.Between(fmtp, "width=", ";")) h := core.Atoi(core.Between(fmtp, "height=", ";")) From 734393d6385b96bf2e4184ae68f9d9d075ff3630 Mon Sep 17 00:00:00 2001 From: Alex X Date: Sun, 16 Jun 2024 06:36:24 +0300 Subject: [PATCH 026/166] Add streaming network visualisation --- internal/streams/api.go | 21 +++++ internal/streams/dot.go | 164 ++++++++++++++++++++++++++++++++++++ internal/streams/streams.go | 1 + www/index.html | 2 +- www/main.js | 1 + www/network.html | 44 ++++++++++ 6 files changed, 232 insertions(+), 1 deletion(-) create mode 100644 internal/streams/dot.go create mode 100644 www/network.html diff --git a/internal/streams/api.go b/internal/streams/api.go index 69d2276a..d64c4846 100644 --- a/internal/streams/api.go +++ b/internal/streams/api.go @@ -101,3 +101,24 @@ func apiStreams(w http.ResponseWriter, r *http.Request) { } } } + +func apiStreamsDOT(w http.ResponseWriter, r *http.Request) { + query := r.URL.Query() + + dot := make([]byte, 0, 1024) + dot = append(dot, "digraph {\n"...) + if query.Has("src") { + for _, name := range query["src"] { + if stream := streams[name]; stream != nil { + dot = AppendDOT(dot, stream) + } + } + } else { + for _, stream := range streams { + dot = AppendDOT(dot, stream) + } + } + dot = append(dot, '}') + + api.Response(w, dot, "text/vnd.graphviz") +} diff --git a/internal/streams/dot.go b/internal/streams/dot.go new file mode 100644 index 00000000..aa008c40 --- /dev/null +++ b/internal/streams/dot.go @@ -0,0 +1,164 @@ +package streams + +import ( + "encoding/json" + "fmt" + "strings" +) + +func AppendDOT(dot []byte, stream *Stream) []byte { + for _, prod := range stream.producers { + if prod.conn == nil { + continue + } + c, err := marshalConn(prod.conn) + if err != nil { + continue + } + dot = c.appendDOT(dot, "producer") + } + for _, cons := range stream.consumers { + c, err := marshalConn(cons) + if err != nil { + continue + } + dot = c.appendDOT(dot, "consumer") + } + return dot +} + +func marshalConn(v any) (*conn, error) { + b, err := json.Marshal(v) + if err != nil { + return nil, err + } + var c conn + if err = json.Unmarshal(b, &c); err != nil { + return nil, err + } + return &c, nil +} + +const bytesK = "KMGTP" + +func humanBytes(i int) string { + if i < 1000 { + return fmt.Sprintf("%d B", i) + } + + f := float64(i) / 1000 + var n uint8 + for f >= 1000 && n < 5 { + f /= 1000 + n++ + } + return fmt.Sprintf("%.2f %cB", f, bytesK[n]) +} + +type node struct { + ID uint32 `json:"id"` + Codec map[string]any `json:"codec"` + Parent uint32 `json:"parent"` + Childs []uint32 `json:"childs"` + Bytes int `json:"bytes"` + //Packets uint32 `json:"packets"` + //Drops uint32 `json:"drops"` +} + +var codecKeys = []string{"codec_name", "sample_rate", "channels", "profile", "level"} + +func (n *node) codec() []byte { + b := make([]byte, 0, 128) + for _, k := range codecKeys { + if v := n.Codec[k]; v != nil { + b = fmt.Appendf(b, "%s=%v\n", k, v) + } + } + return b[:len(b)-1] +} + +func (n *node) appendDOT(dot []byte, group string) []byte { + dot = fmt.Appendf(dot, "%d [group=%s, label=%q, title=%q];\n", n.ID, group, n.Codec["codec_name"], n.codec()) + //for _, sink := range n.Childs { + // dot = fmt.Appendf(dot, "%d -> %d;\n", n.ID, sink) + //} + return dot +} + +type conn struct { + ID uint32 `json:"id"` + FormatName string `json:"format_name"` + Protocol string `json:"protocol"` + RemoteAddr string `json:"remote_addr"` + Source string `json:"source"` + URL string `json:"url"` + UserAgent string `json:"user_agent"` + Receivers []node `json:"receivers"` + Senders []node `json:"senders"` + BytesRecv int `json:"bytes_recv"` + BytesSend int `json:"bytes_send"` +} + +func (c *conn) appendDOT(dot []byte, group string) []byte { + host := c.host() + dot = fmt.Appendf(dot, "%s [group=host];\n", host) + dot = fmt.Appendf(dot, "%d [group=%s, label=%q, title=%q];\n", c.ID, group, c.FormatName, c.label()) + if group == "producer" { + dot = fmt.Appendf(dot, "%s -> %d [label=%q];\n", host, c.ID, humanBytes(c.BytesRecv)) + } else { + dot = fmt.Appendf(dot, "%d -> %s [label=%q];\n", c.ID, host, humanBytes(c.BytesSend)) + } + + for _, recv := range c.Receivers { + dot = fmt.Appendf(dot, "%d -> %d [label=%q];\n", c.ID, recv.ID, humanBytes(recv.Bytes)) + dot = recv.appendDOT(dot, "node") + } + for _, send := range c.Senders { + dot = fmt.Appendf(dot, "%d -> %d [label=%q];\n", send.Parent, c.ID, humanBytes(send.Bytes)) + //dot = fmt.Appendf(dot, "%d -> %d [label=%q];\n", send.ID, c.ID, humanBytes(send.Bytes)) + //dot = send.appendDOT(dot, "node") + } + return dot +} + +func (c *conn) host() (s string) { + if c.Protocol == "pipe" { + return "127.0.0.1" + } + + if s = c.RemoteAddr; s == "" { + return "unknown" + } + + if i := strings.Index(s, "forwarded"); i > 0 { + s = s[i+10:] + } + + if s[0] == '[' { + if i := strings.Index(s, "]"); i > 0 { + return s[1:i] + } + } + + if i := strings.IndexAny(s, " ,:"); i > 0 { + return s[:i] + } + return +} + +func (c *conn) label() (s string) { + s = "format_name=" + c.FormatName + if c.Protocol != "" { + s += "\nprotocol=" + c.Protocol + } + if c.Source != "" { + s += "\nsource=" + c.Source + } + if c.URL != "" { + s += "\nurl=" + c.URL + } + if c.UserAgent != "" { + s += "\nuser_agent=" + c.UserAgent + } + return +} diff --git a/internal/streams/streams.go b/internal/streams/streams.go index 6d6fa773..ff0f5654 100644 --- a/internal/streams/streams.go +++ b/internal/streams/streams.go @@ -27,6 +27,7 @@ func Init() { } api.HandleFunc("api/streams", apiStreams) + api.HandleFunc("api/streams.dot", apiStreamsDOT) if cfg.Publish == nil { return diff --git a/www/index.html b/www/index.html index 6adf9f0a..63fedcec 100644 --- a/www/index.html +++ b/www/index.html @@ -139,7 +139,7 @@ const isChecked = checkboxStates[name] ? 'checked' : ''; tr.innerHTML = `` + - `${online} / info / probe` + + `${online} / info / probe / net` + `${links}`; } diff --git a/www/main.js b/www/main.js index 2c15e071..714c9127 100644 --- a/www/main.js +++ b/www/main.js @@ -138,6 +138,7 @@ body.dark-mode hr {
  • Add
  • Config
  • Log
  • +
  • Net
  • 🌙 diff --git a/www/network.html b/www/network.html new file mode 100644 index 00000000..5193b6a9 --- /dev/null +++ b/www/network.html @@ -0,0 +1,44 @@ + + + + + go2rtc - Network + + + + + +
    + + + \ No newline at end of file From 31e57c2ff81e04f585135fcf0670fc150d5f9734 Mon Sep 17 00:00:00 2001 From: Alex X Date: Sun, 16 Jun 2024 06:37:42 +0300 Subject: [PATCH 027/166] Fix errors output for webrtc client and server --- internal/webrtc/client.go | 10 ++++++++-- internal/webrtc/server.go | 1 + 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/internal/webrtc/client.go b/internal/webrtc/client.go index 4b8b1b9a..d42c51dd 100644 --- a/internal/webrtc/client.go +++ b/internal/webrtc/client.go @@ -77,10 +77,15 @@ func go2rtcClient(url string) (core.Producer, error) { // 2. Create PeerConnection pc, err := PeerConnection(true) if err != nil { - log.Error().Err(err).Caller().Send() return nil, err } + defer func() { + if err != nil { + _ = pc.Close() + } + }() + // waiter will wait PC error or WS error or nil (connection OK) var connState core.Waiter var connMu sync.Mutex @@ -133,7 +138,8 @@ func go2rtcClient(url string) (core.Producer, error) { } if msg.Type != "webrtc/answer" { - return nil, errors.New("wrong answer: " + msg.Type) + err = errors.New("wrong answer: " + msg.String()) + return nil, err } answer := msg.String() diff --git a/internal/webrtc/server.go b/internal/webrtc/server.go index 91a237db..f7365afa 100644 --- a/internal/webrtc/server.go +++ b/internal/webrtc/server.go @@ -65,6 +65,7 @@ func outputWebRTC(w http.ResponseWriter, r *http.Request) { url := r.URL.Query().Get("src") stream := streams.Get(url) if stream == nil { + http.Error(w, api.StreamNotFound, http.StatusNotFound) return } From 5d579596088f4a0694c1d12b5e4b18144ca1056b Mon Sep 17 00:00:00 2001 From: Sergey Krashevich Date: Sun, 16 Jun 2024 08:56:57 +0300 Subject: [PATCH 028/166] fix(streams): handle missing codec_name in appendDOT function --- internal/streams/dot.go | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/internal/streams/dot.go b/internal/streams/dot.go index aa008c40..b9a2b773 100644 --- a/internal/streams/dot.go +++ b/internal/streams/dot.go @@ -77,12 +77,17 @@ func (n *node) codec() []byte { return b[:len(b)-1] } -func (n *node) appendDOT(dot []byte, group string) []byte { - dot = fmt.Appendf(dot, "%d [group=%s, label=%q, title=%q];\n", n.ID, group, n.Codec["codec_name"], n.codec()) +func (n *node) appendDOT(dot []byte, group string) ([]byte, error) { + codecName, ok := n.Codec["codec_name"] + if !ok { + return nil, fmt.Errorf("codec_name not found in Codec map") + } + + dot = fmt.Appendf(dot, "%d [group=%s, label=%q, title=%q];\n", n.ID, group, codecName, n.codec()) //for _, sink := range n.Childs { // dot = fmt.Appendf(dot, "%d -> %d;\n", n.ID, sink) //} - return dot + return dot, nil } type conn struct { @@ -111,7 +116,7 @@ func (c *conn) appendDOT(dot []byte, group string) []byte { for _, recv := range c.Receivers { dot = fmt.Appendf(dot, "%d -> %d [label=%q];\n", c.ID, recv.ID, humanBytes(recv.Bytes)) - dot = recv.appendDOT(dot, "node") + dot, _ = recv.appendDOT(dot, "node") // TODO: handle error for debug purposes } for _, send := range c.Senders { dot = fmt.Appendf(dot, "%d -> %d [label=%q];\n", send.Parent, c.ID, humanBytes(send.Bytes)) From 1b411b1fed4fa33d17739542ce53f2870a161998 Mon Sep 17 00:00:00 2001 From: Sergey Krashevich Date: Sun, 16 Jun 2024 10:18:45 +0300 Subject: [PATCH 029/166] refactor(streams): optimize label generation with strings.Builder feat(network): add periodic data fetching and network update --- internal/streams/dot.go | 15 ++++---- www/network.html | 83 +++++++++++++++++++++++++++++++---------- 2 files changed, 72 insertions(+), 26 deletions(-) diff --git a/internal/streams/dot.go b/internal/streams/dot.go index aa008c40..9aacccb5 100644 --- a/internal/streams/dot.go +++ b/internal/streams/dot.go @@ -146,19 +146,20 @@ func (c *conn) host() (s string) { return } -func (c *conn) label() (s string) { - s = "format_name=" + c.FormatName +func (c *conn) label() string { + var sb strings.Builder + sb.WriteString("format_name=" + c.FormatName) if c.Protocol != "" { - s += "\nprotocol=" + c.Protocol + sb.WriteString("\nprotocol=" + c.Protocol) } if c.Source != "" { - s += "\nsource=" + c.Source + sb.WriteString("\nsource=" + c.Source) } if c.URL != "" { - s += "\nurl=" + c.URL + sb.WriteString("\nurl=" + c.URL) } if c.UserAgent != "" { - s += "\nuser_agent=" + c.UserAgent + sb.WriteString("\nuser_agent=" + c.UserAgent) } - return + return sb.String() } diff --git a/www/network.html b/www/network.html index 5193b6a9..519e0eba 100644 --- a/www/network.html +++ b/www/network.html @@ -21,24 +21,69 @@ - -
    - + +
    + + \ No newline at end of file From a69eb8a66e576cbfe369c4af985ea37be5abd096 Mon Sep 17 00:00:00 2001 From: Sergey Krashevich Date: Sun, 16 Jun 2024 14:53:08 +0300 Subject: [PATCH 030/166] style(network): add flex-grow to network div and move script tag --- www/network.html | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/www/network.html b/www/network.html index 519e0eba..b0edd31c 100644 --- a/www/network.html +++ b/www/network.html @@ -18,11 +18,15 @@ height: 100%; width: 100%; } + + #network { + flex-grow: 1; + } -
    + - \ No newline at end of file From cb44d5431a854efd2a84af52d747e98aabc862b9 Mon Sep 17 00:00:00 2001 From: Sergey Krashevich Date: Sun, 16 Jun 2024 15:01:40 +0300 Subject: [PATCH 031/166] feat(network): preserve pan and scale on data reload --- www/network.html | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/www/network.html b/www/network.html index b0edd31c..18d4a640 100644 --- a/www/network.html +++ b/www/network.html @@ -68,6 +68,8 @@ network.storePositions(); } else { const positions = network.getPositions(); + const viewState = network.getViewPosition(); + const scale = network.getScale(); network.setData(data); @@ -76,6 +78,8 @@ network.moveNode(nodeId, positions[nodeId].x, positions[nodeId].y); } } + + network.moveTo({ position: viewState, scale: scale }); } } catch (error) { From 906f554d74fa7d2cf18cbb6314a3d309342089ef Mon Sep 17 00:00:00 2001 From: Alex X Date: Sun, 16 Jun 2024 15:19:50 +0300 Subject: [PATCH 032/166] Code refactoring after #1195 --- internal/streams/dot.go | 25 +++++++++++++++---------- pkg/core/codec.go | 8 +++++++- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/internal/streams/dot.go b/internal/streams/dot.go index b9a2b773..96aa4115 100644 --- a/internal/streams/dot.go +++ b/internal/streams/dot.go @@ -67,6 +67,13 @@ type node struct { var codecKeys = []string{"codec_name", "sample_rate", "channels", "profile", "level"} +func (n *node) name() string { + if name, ok := n.Codec["codec_name"].(string); ok { + return name + } + return "unknown" +} + func (n *node) codec() []byte { b := make([]byte, 0, 128) for _, k := range codecKeys { @@ -74,20 +81,18 @@ func (n *node) codec() []byte { b = fmt.Appendf(b, "%s=%v\n", k, v) } } - return b[:len(b)-1] + if l := len(b); l > 0 { + return b[:l-1] + } + return b } -func (n *node) appendDOT(dot []byte, group string) ([]byte, error) { - codecName, ok := n.Codec["codec_name"] - if !ok { - return nil, fmt.Errorf("codec_name not found in Codec map") - } - - dot = fmt.Appendf(dot, "%d [group=%s, label=%q, title=%q];\n", n.ID, group, codecName, n.codec()) +func (n *node) appendDOT(dot []byte, group string) []byte { + dot = fmt.Appendf(dot, "%d [group=%s, label=%q, title=%q];\n", n.ID, group, n.name(), n.codec()) //for _, sink := range n.Childs { // dot = fmt.Appendf(dot, "%d -> %d;\n", n.ID, sink) //} - return dot, nil + return dot } type conn struct { @@ -116,7 +121,7 @@ func (c *conn) appendDOT(dot []byte, group string) []byte { for _, recv := range c.Receivers { dot = fmt.Appendf(dot, "%d -> %d [label=%q];\n", c.ID, recv.ID, humanBytes(recv.Bytes)) - dot, _ = recv.appendDOT(dot, "node") // TODO: handle error for debug purposes + dot = recv.appendDOT(dot, "node") } for _, send := range c.Senders { dot = fmt.Appendf(dot, "%d -> %d [label=%q];\n", send.Parent, c.ID, humanBytes(send.Bytes)) diff --git a/pkg/core/codec.go b/pkg/core/codec.go index d07b8b74..9c6c6b79 100644 --- a/pkg/core/codec.go +++ b/pkg/core/codec.go @@ -69,8 +69,14 @@ func FFmpegCodecName(name string) string { return "vp9" case CodecAV1: return "av1" + case CodecELD: + return "aac/eld" + case CodecFLAC: + return "flac" + case CodecMP3: + return "mp3" } - return "" + return name } func (c *Codec) String() (s string) { From d8aed552bc54e98804d92e74993389cf18ea1469 Mon Sep 17 00:00:00 2001 From: Sergey Krashevich Date: Sun, 16 Jun 2024 15:22:33 +0300 Subject: [PATCH 033/166] fix(network): ensure consistent node positions by storing and reusing seed --- www/network.html | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/www/network.html b/www/network.html index 18d4a640..53f1ca68 100644 --- a/www/network.html +++ b/www/network.html @@ -31,13 +31,11 @@ let network; let nodes = new vis.DataSet(); let edges = new vis.DataSet(); + let seed = ""; /* global vis */ window.addEventListener('load', () => { const url = new URL('api/streams.dot' + location.search, location.href); const options = { - layout: { - randomSeed: "0.4597730541017021:1718519934576" - }, edges: { font: { align: 'middle' }, smooth: false, @@ -66,16 +64,19 @@ edges = new vis.DataSet(data.edges); network = new vis.Network(container, { nodes, edges }, options); network.storePositions(); + seed = network.getSeed(); } else { const positions = network.getPositions(); const viewState = network.getViewPosition(); const scale = network.getScale(); - + network.setOptions({layout: { + randomSeed: seed + }}) network.setData(data); for (const nodeId in positions) { if (positions.hasOwnProperty(nodeId)) { - network.moveNode(nodeId, positions[nodeId].x, positions[nodeId].y); + network.moveNode(nodeId, Math.floor(positions[nodeId].x), Math.floor(positions[nodeId].y)); } } From a56d3353806c00d9e8203427994499d191e13942 Mon Sep 17 00:00:00 2001 From: Alex X Date: Sun, 16 Jun 2024 15:26:18 +0300 Subject: [PATCH 034/166] Fix homekit producer remote_addr --- pkg/homekit/producer.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/homekit/producer.go b/pkg/homekit/producer.go index c2781e27..451b9882 100644 --- a/pkg/homekit/producer.go +++ b/pkg/homekit/producer.go @@ -57,7 +57,8 @@ func Dial(rawURL string, server *srtp.Server) (*Client, error) { ID: core.NewID(), FormatName: "homekit", Protocol: "udp", - Source: conn.URL(), + RemoteAddr: conn.Conn.RemoteAddr().String(), + Source: rawURL, Transport: conn, }, hap: conn, From da5f060741f5939ee57c48c9d7d75e19baab5dea Mon Sep 17 00:00:00 2001 From: Alex X Date: Sun, 16 Jun 2024 19:03:57 +0300 Subject: [PATCH 035/166] Add killsignal and killtimeout to exec/rtsp --- internal/exec/closer.go | 39 ++++++++++++++++++++++++++++ internal/exec/exec.go | 53 ++++++++++++++++++++++---------------- internal/exec/pipe.go | 56 ----------------------------------------- 3 files changed, 70 insertions(+), 78 deletions(-) create mode 100644 internal/exec/closer.go delete mode 100644 internal/exec/pipe.go diff --git a/internal/exec/closer.go b/internal/exec/closer.go new file mode 100644 index 00000000..66d0e3ac --- /dev/null +++ b/internal/exec/closer.go @@ -0,0 +1,39 @@ +package exec + +import ( + "errors" + "net/url" + "os" + "os/exec" + "syscall" + "time" + + "github.com/AlexxIT/go2rtc/pkg/core" +) + +// closer support custom killsignal with custom killtimeout +type closer struct { + cmd *exec.Cmd + query url.Values +} + +func (c *closer) Close() (err error) { + sig := os.Kill + if s := c.query.Get("killsignal"); s != "" { + sig = syscall.Signal(core.Atoi(s)) + } + + log.Trace().Msgf("[exec] kill with signal=%d", sig) + err = c.cmd.Process.Signal(sig) + + if s := c.query.Get("killtimeout"); s != "" { + timeout := time.Duration(core.Atoi(s)) * time.Second + timer := time.AfterFunc(timeout, func() { + log.Trace().Msgf("[exec] kill after timeout=%s", s) + _ = c.cmd.Process.Kill() + }) + defer timer.Stop() // stop timer if Wait ends before timeout + } + + return errors.Join(err, c.cmd.Wait()) +} diff --git a/internal/exec/exec.go b/internal/exec/exec.go index ac1691d3..035317d9 100644 --- a/internal/exec/exec.go +++ b/internal/exec/exec.go @@ -1,10 +1,12 @@ package exec import ( + "bufio" "crypto/md5" "encoding/hex" "errors" "fmt" + "io" "net/url" "os" "os/exec" @@ -49,8 +51,10 @@ func Init() { } func execHandle(rawURL string) (core.Producer, error) { + rawURL, rawQuery, _ := strings.Cut(rawURL, "#") + query := streams.ParseQuery(rawQuery) + var path string - var query url.Values // RTSP flow should have `{output}` inside URL // pipe flow may have `#{params}` inside URL @@ -62,9 +66,6 @@ func execHandle(rawURL string) (core.Producer, error) { sum := md5.Sum([]byte(rawURL)) path = "/" + hex.EncodeToString(sum[:]) rawURL = rawURL[:i] + "rtsp://127.0.0.1:" + rtsp.Port + path + rawURL[i+8:] - } else if i = strings.IndexByte(rawURL, '#'); i > 0 { - query = streams.ParseQuery(rawURL[i+1:]) - rawURL = rawURL[:i] } args := shell.QuoteSplit(rawURL[5:]) // remove `exec:` @@ -74,23 +75,34 @@ func execHandle(rawURL string) (core.Producer, error) { debug: log.Debug().Enabled(), } - if path == "" { - return handlePipe(rawURL, cmd, query) - } - - return handleRTSP(rawURL, cmd, path) -} - -func handlePipe(source string, cmd *exec.Cmd, query url.Values) (core.Producer, error) { if query.Get("backchannel") == "1" { return stdin.NewClient(cmd) } - r, err := PipeCloser(cmd, query) + cl := &closer{cmd: cmd, query: query} + + if path == "" { + return handlePipe(rawURL, cmd, cl) + } + + return handleRTSP(rawURL, cmd, cl, path) +} + +func handlePipe(source string, cmd *exec.Cmd, cl io.Closer) (core.Producer, error) { + stdout, err := cmd.StdoutPipe() if err != nil { return nil, err } + rc := struct { + io.Reader + io.Closer + }{ + // add buffer for pipe reader to reduce syscall + bufio.NewReaderSize(stdout, core.BufferSize), + cl, + } + log.Debug().Strs("args", cmd.Args).Msg("[exec] run pipe") ts := time.Now() @@ -99,9 +111,9 @@ func handlePipe(source string, cmd *exec.Cmd, query url.Values) (core.Producer, return nil, err } - prod, err := magic.Open(r) + prod, err := magic.Open(rc) if err != nil { - _ = r.Close() + _ = rc.Close() return nil, fmt.Errorf("exec/pipe: %w\n%s", err, cmd.Stderr) } @@ -115,7 +127,7 @@ func handlePipe(source string, cmd *exec.Cmd, query url.Values) (core.Producer, return prod, nil } -func handleRTSP(source string, cmd *exec.Cmd, path string) (core.Producer, error) { +func handleRTSP(source string, cmd *exec.Cmd, cl io.Closer, path string) (core.Producer, error) { if log.Trace().Enabled() { cmd.Stdout = os.Stdout } @@ -147,9 +159,9 @@ func handleRTSP(source string, cmd *exec.Cmd, path string) (core.Producer, error }() select { - case <-time.After(time.Second * 60): - _ = cmd.Process.Kill() + case <-time.After(time.Minute): log.Error().Str("source", source).Msg("[exec] timeout") + _ = cl.Close() return nil, errors.New("exec: timeout") case <-done: // limit message size @@ -157,10 +169,7 @@ func handleRTSP(source string, cmd *exec.Cmd, path string) (core.Producer, error case prod := <-waiter: log.Debug().Stringer("launch", time.Since(ts)).Msg("[exec] run rtsp") setRemoteInfo(prod, source, cmd.Args) - prod.OnClose = func() error { - log.Debug().Msgf("[exec] kill rtsp") - return errors.Join(cmd.Process.Kill(), cmd.Wait()) - } + prod.OnClose = cl.Close return prod, nil } } diff --git a/internal/exec/pipe.go b/internal/exec/pipe.go deleted file mode 100644 index 12ea136b..00000000 --- a/internal/exec/pipe.go +++ /dev/null @@ -1,56 +0,0 @@ -package exec - -import ( - "bufio" - "errors" - "io" - "net/url" - "os/exec" - "syscall" - "time" - - "github.com/AlexxIT/go2rtc/pkg/core" -) - -// PipeCloser - return StdoutPipe that Kill cmd on Close call -func PipeCloser(cmd *exec.Cmd, query url.Values) (io.ReadCloser, error) { - stdout, err := cmd.StdoutPipe() - if err != nil { - return nil, err - } - - // add buffer for pipe reader to reduce syscall - return &pipeCloser{bufio.NewReaderSize(stdout, core.BufferSize), stdout, cmd, query}, nil -} - -type pipeCloser struct { - io.Reader - io.Closer - cmd *exec.Cmd - query url.Values -} - -func (p *pipeCloser) Close() error { - return errors.Join(p.Closer.Close(), p.Kill(), p.Wait()) -} - -func (p *pipeCloser) Kill() error { - if s := p.query.Get("killsignal"); s != "" { - log.Trace().Msgf("[exec] kill with custom sig=%s", s) - sig := syscall.Signal(core.Atoi(s)) - return p.cmd.Process.Signal(sig) - } - return p.cmd.Process.Kill() -} - -func (p *pipeCloser) Wait() error { - if s := p.query.Get("killtimeout"); s != "" { - timeout := time.Duration(core.Atoi(s)) * time.Second - timer := time.AfterFunc(timeout, func() { - log.Trace().Msgf("[exec] kill after timeout=%s", s) - _ = p.cmd.Process.Kill() - }) - defer timer.Stop() // stop timer if Wait ends before timeout - } - return p.cmd.Wait() -} From bdc7ff1035b0b8a427445ea31e9b3004d0748da0 Mon Sep 17 00:00:00 2001 From: Alex X Date: Sun, 16 Jun 2024 19:04:34 +0300 Subject: [PATCH 036/166] Fix forwarded remote_addr in the network --- pkg/core/connection.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/core/connection.go b/pkg/core/connection.go index 1055c381..2c3f2196 100644 --- a/pkg/core/connection.go +++ b/pkg/core/connection.go @@ -96,7 +96,7 @@ func (c *Connection) SetRemoteAddr(s string) { if c.RemoteAddr == "" { c.RemoteAddr = s } else { - c.RemoteAddr += " forward " + c.RemoteAddr + c.RemoteAddr += " forwarded " + s } } From 5b481a27c67cdf1b75b02e295ce6e172eca58878 Mon Sep 17 00:00:00 2001 From: Sergey Krashevich Date: Sun, 16 Jun 2024 21:57:48 +0300 Subject: [PATCH 037/166] fix(network): enable autoResize in network settings --- www/network.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/network.html b/www/network.html index 53f1ca68..17c3570c 100644 --- a/www/network.html +++ b/www/network.html @@ -47,7 +47,7 @@ enabled: false, } }, - autoResize: false, + autoResize: true, locale: navigator.language.toLowerCase().split('-').slice(0, 2).join('-'), }; const container = document.getElementById('network'); From e6fa97c738560dd03374994818e2bdd032096051 Mon Sep 17 00:00:00 2001 From: Alex X Date: Sun, 16 Jun 2024 22:12:52 +0300 Subject: [PATCH 038/166] Code refactoring after #1196 --- www/network.html | 99 ++++++++++++++++++++---------------------------- 1 file changed, 41 insertions(+), 58 deletions(-) diff --git a/www/network.html b/www/network.html index 17c3570c..7a4ff229 100644 --- a/www/network.html +++ b/www/network.html @@ -25,73 +25,56 @@ -
    - - + + update(); + }); + - \ No newline at end of file + From db6745e8ff034552ef87d4a45220428b87823e67 Mon Sep 17 00:00:00 2001 From: Alex X Date: Tue, 18 Jun 2024 20:35:17 +0300 Subject: [PATCH 039/166] Code refactoring after #1168 --- internal/app/app.go | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/internal/app/app.go b/internal/app/app.go index cdbb870b..eb803584 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -45,17 +45,7 @@ func Init() { os.Exit(0) } - ppid := os.Getppid() - if ppid == 1 { - daemon = false - } else { - parent, err := os.FindProcess(ppid) - if err != nil || parent.Pid < 1 { - daemon = false - } - } - - if daemon { + if daemon && os.Getppid() != 1 { if runtime.GOOS == "windows" { fmt.Println("Daemon mode is not supported on Windows") os.Exit(1) From a4885c2c3abce58074d04878bba0d72105642a9b Mon Sep 17 00:00:00 2001 From: Alex X Date: Tue, 18 Jun 2024 21:33:36 +0300 Subject: [PATCH 040/166] Update version to 1.9.4 --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index d40851cc..98bd79e3 100644 --- a/main.go +++ b/main.go @@ -36,7 +36,7 @@ import ( ) func main() { - app.Version = "1.9.3" + app.Version = "1.9.4" // 1. Core modules: app, api/ws, streams From eaae7aee39c2e4210cb466e2925cfc227ed09877 Mon Sep 17 00:00:00 2001 From: Alex X Date: Wed, 19 Jun 2024 06:52:49 +0300 Subject: [PATCH 041/166] Fix stream info for publishing RTMP --- internal/rtmp/rtmp.go | 2 +- pkg/rtmp/client.go | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/internal/rtmp/rtmp.go b/internal/rtmp/rtmp.go index afc363a9..b3d7f932 100644 --- a/internal/rtmp/rtmp.go +++ b/internal/rtmp/rtmp.go @@ -133,7 +133,7 @@ func streamsHandle(url string) (core.Producer, error) { func streamsConsumerHandle(url string) (core.Consumer, func(), error) { cons := flv.NewConsumer() run := func() { - wr, err := rtmp.DialPublish(url) + wr, err := rtmp.DialPublish(url, cons) if err != nil { return } diff --git a/pkg/rtmp/client.go b/pkg/rtmp/client.go index 138d727d..c9e9ad17 100644 --- a/pkg/rtmp/client.go +++ b/pkg/rtmp/client.go @@ -35,7 +35,7 @@ func DialPlay(rawURL string) (*flv.Producer, error) { return client.Producer() } -func DialPublish(rawURL string) (io.Writer, error) { +func DialPublish(rawURL string, cons *flv.Consumer) (io.Writer, error) { u, err := url.Parse(rawURL) if err != nil { return nil, err @@ -55,6 +55,11 @@ func DialPublish(rawURL string) (io.Writer, error) { return nil, err } + cons.FormatName = "rtmp" + cons.Protocol = "rtmp" + cons.RemoteAddr = conn.RemoteAddr().String() + cons.URL = rawURL + return client, nil } From 0e5b293b1ff3090007b8b8b961cec3c5cdef2ddf Mon Sep 17 00:00:00 2001 From: Sergey Krashevich Date: Wed, 19 Jun 2024 12:19:21 +0300 Subject: [PATCH 042/166] fix(network): preserve selected nodes and edges on data reload --- www/network.html | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/www/network.html b/www/network.html index 7a4ff229..180c9711 100644 --- a/www/network.html +++ b/www/network.html @@ -57,9 +57,14 @@ const positions = network.getPositions(); const viewPosition = network.getViewPosition(); const scale = network.getScale(); + const selectedNodes = network.getSelectedNodes(); + const selectedEdges = network.getSelectedEdges(); network.setData(data); + network.selectNodes(selectedNodes); + network.selectEdges(selectedEdges); + for (const nodeId in positions) { network.moveNode(nodeId, positions[nodeId].x, positions[nodeId].y); } From 56e2c6650dbb73c1a8c861094c550e183f0ed9d8 Mon Sep 17 00:00:00 2001 From: Rob van Oostenrijk Date: Sat, 22 Jun 2024 19:15:07 +0400 Subject: [PATCH 043/166] Update build.cmd --- scripts/build.cmd | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/scripts/build.cmd b/scripts/build.cmd index 54565b2d..8b355f76 100644 --- a/scripts/build.cmd +++ b/scripts/build.cmd @@ -56,3 +56,13 @@ go build -ldflags "-s -w" -trimpath && 7z a -mx9 -sdel %FILENAME% go2rtc @SET GOARCH=arm64 @SET FILENAME=go2rtc_mac_arm64.zip go build -ldflags "-s -w" -trimpath && 7z a -mx9 -sdel %FILENAME% go2rtc + +@SET GOOS=freebsd +@SET GOARCH=amd64 +@SET FILENAME=go2rtc_freebsd_amd64.zip +go build -ldflags "-s -w" -trimpath && 7z a -mx9 -sdel %FILENAME% go2rtc + +@SET GOOS=freebsd +@SET GOARCH=arm64 +@SET FILENAME=go2rtc_freebsd_arm64.zip +go build -ldflags "-s -w" -trimpath && 7z a -mx9 -sdel %FILENAME% go2rtc From c47427633c1c984f79180b29ebee85197a5bf11e Mon Sep 17 00:00:00 2001 From: Rob van Oostenrijk Date: Sat, 22 Jun 2024 19:20:39 +0400 Subject: [PATCH 044/166] Update build.sh --- scripts/build.sh | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/scripts/build.sh b/scripts/build.sh index 0814ba48..e365eb54 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -79,4 +79,16 @@ go build -ldflags "-s -w" -trimpath && 7z a -mx9 -bso0 -sdel $FILENAME go2rtc export GOOS=darwin export GOARCH=arm64 FILENAME="go2rtc_mac_arm64.zip" -go build -ldflags "-s -w" -trimpath && 7z a -mx9 -bso0 -sdel $FILENAME go2rtc \ No newline at end of file +go build -ldflags "-s -w" -trimpath && 7z a -mx9 -bso0 -sdel $FILENAME go2rtc + +# FreeBSD amd64 +export GOOS=freebsd +export GOARCH=amd64 +FILENAME="go2rtc_freebsd_amd64.zip" +go build -ldflags "-s -w" -trimpath && 7z a -mx9 -bso0 -sdel $FILENAME go2rtc + +# FreeBSD arm64 +export GOOS=freebsd +export GOARCH=arm64 +FILENAME="go2rtc_freebsd_arm64.zip" +go build -ldflags "-s -w" -trimpath && 7z a -mx9 -bso0 -sdel $FILENAME go2rtc From a04b7eed28fb344dd949d257e3c375ead7a4b71e Mon Sep 17 00:00:00 2001 From: Rob van Oostenrijk Date: Sat, 22 Jun 2024 19:23:14 +0400 Subject: [PATCH 045/166] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index c31ed748..33c14cca 100644 --- a/README.md +++ b/README.md @@ -126,6 +126,8 @@ Download binary for your OS from [latest release](https://github.com/AlexxIT/go2 - `go2rtc_linux_mipsel` - Linux MIPS (ex. [Xiaomi Gateway 3](https://github.com/AlexxIT/XiaomiGateway3), [Wyze cameras](https://github.com/gtxaspec/wz_mini_hacks)) - `go2rtc_mac_amd64.zip` - Mac Intel 64-bit - `go2rtc_mac_arm64.zip` - Mac ARM 64-bit +- `go2rtc_freebsd_amd64.zip` - FreeBSD Intel 64-bit +- `go2rtc_freebsd_arm64.zip` - FreeBSD ARM 64-bit Don't forget to fix the rights `chmod +x go2rtc_xxx_xxx` on Linux and Mac. From 5b0781253ffb8c690226c7dc8d044fbdd722ed6a Mon Sep 17 00:00:00 2001 From: Jamal Fanaian Date: Thu, 11 Jul 2024 17:54:04 -0700 Subject: [PATCH 046/166] Add support for Nest cameras with RTSP --- pkg/nest/api.go | 180 ++++++++++++++++++++++++++++++++++++++++++--- pkg/nest/client.go | 92 +++++++++++++++++++---- 2 files changed, 248 insertions(+), 24 deletions(-) diff --git a/pkg/nest/api.go b/pkg/nest/api.go index 5e0d3407..8aae2df2 100644 --- a/pkg/nest/api.go +++ b/pkg/nest/api.go @@ -17,9 +17,15 @@ type API struct { StreamProjectID string StreamDeviceID string - StreamSessionID string StreamExpiresAt time.Time + // WebRTC + StreamSessionID string + + // RTSP + StreamToken string + StreamExtensionToken string + extendTimer *time.Timer } @@ -112,7 +118,14 @@ func (a *API) GetDevices(projectID string) (map[string]string, error) { continue } - if device.Traits.SdmDevicesTraitsCameraLiveStream.SupportedProtocols[0] != "WEB_RTC" { + supported := false + for _, protocol := range device.Traits.SdmDevicesTraitsCameraLiveStream.SupportedProtocols { + if (protocol == "WEB_RTC" || protocol == "RTSP") { + supported = true + break + } + } + if !supported { continue } @@ -122,12 +135,44 @@ func (a *API) GetDevices(projectID string) (map[string]string, error) { } name := device.Traits.SdmDevicesTraitsInfo.CustomName + // Devices configured through the Nest app use the container/room name as opposed to the customName trait + if name == "" && len(device.ParentRelations) > 0 { + name = device.ParentRelations[0].DisplayName + } devices[name] = device.Name[i+1:] } return devices, nil } +func (a *API) GetDevice(projectID, deviceID string) (Device, error) { + uri := "https://smartdevicemanagement.googleapis.com/v1/enterprises/" + projectID + "/devices/" + deviceID + req, err := http.NewRequest("GET", uri, nil) + if err != nil { + return Device{}, err + } + + req.Header.Set("Authorization", "Bearer "+a.Token) + + client := &http.Client{Timeout: time.Second * 5000} + res, err := client.Do(req) + if err != nil { + return Device{}, err + } + + if res.StatusCode != 200 { + return Device{}, errors.New("nest: wrong status: " + res.Status) + } + + var device Device + + if err = json.NewDecoder(res.Body).Decode(&device); err != nil { + return Device{}, err + } + + return device, nil +} + func (a *API) ExchangeSDP(projectID, deviceID, offer string) (string, error) { var reqv struct { Command string `json:"command"` @@ -186,11 +231,20 @@ func (a *API) ExtendStream() error { var reqv struct { Command string `json:"command"` Params struct { - MediaSessionID string `json:"mediaSessionId"` + MediaSessionID string `json:"mediaSessionId,omitempty"` + StreamExtensionToken string `json:"streamExtensionToken,omitempty"` } `json:"params"` } - reqv.Command = "sdm.devices.commands.CameraLiveStream.ExtendWebRtcStream" - reqv.Params.MediaSessionID = a.StreamSessionID + + if a.StreamToken != "" { + // RTSP + reqv.Command = "sdm.devices.commands.CameraLiveStream.ExtendRtspStream" + reqv.Params.StreamExtensionToken = a.StreamExtensionToken + } else { + // WebRTC + reqv.Command = "sdm.devices.commands.CameraLiveStream.ExtendWebRtcStream" + reqv.Params.MediaSessionID = a.StreamSessionID + } b, err := json.Marshal(reqv) if err != nil { @@ -220,6 +274,8 @@ func (a *API) ExtendStream() error { Results struct { ExpiresAt time.Time `json:"expiresAt"` MediaSessionID string `json:"mediaSessionId"` + StreamExtensionToken string `json:"streamExtensionToken"` + StreamToken string `json:"streamToken"` } `json:"results"` } @@ -229,6 +285,111 @@ func (a *API) ExtendStream() error { a.StreamSessionID = resv.Results.MediaSessionID a.StreamExpiresAt = resv.Results.ExpiresAt + a.StreamExtensionToken = resv.Results.StreamExtensionToken + a.StreamToken = resv.Results.StreamToken + + return nil +} + +func (a *API) GenerateRtspStream(projectID, deviceID string) (string, error) { + var reqv struct { + Command string `json:"command"` + Params struct {} `json:"params"` + } + reqv.Command = "sdm.devices.commands.CameraLiveStream.GenerateRtspStream" + + b, err := json.Marshal(reqv) + if err != nil { + return "", err + } + + uri := "https://smartdevicemanagement.googleapis.com/v1/enterprises/" + + projectID + "/devices/" + deviceID + ":executeCommand" + req, err := http.NewRequest("POST", uri, bytes.NewReader(b)) + if err != nil { + return "", err + } + + req.Header.Set("Authorization", "Bearer "+a.Token) + + client := &http.Client{Timeout: time.Second * 5000} + res, err := client.Do(req) + if err != nil { + return "", err + } + + if res.StatusCode != 200 { + return "", errors.New("nest: wrong status: " + res.Status) + } + + var resv struct { + Results struct { + StreamURLs map[string]string `json:"streamUrls"` + StreamExtensionToken string `json:"streamExtensionToken"` + StreamToken string `json:"streamToken"` + ExpiresAt time.Time `json:"expiresAt"` + } `json:"results"` + } + + if err = json.NewDecoder(res.Body).Decode(&resv); err != nil { + return "", err + } + + if _, ok := resv.Results.StreamURLs["rtspUrl"]; !ok { + return "", errors.New("nest: failed to generate rtsp url") + } + + a.StreamProjectID = projectID + a.StreamDeviceID = deviceID + a.StreamToken = resv.Results.StreamToken + a.StreamExtensionToken = resv.Results.StreamExtensionToken + a.StreamExpiresAt = resv.Results.ExpiresAt + + return resv.Results.StreamURLs["rtspUrl"], nil +} + +func (a *API) StopRTSPStream() error { + if a.StreamProjectID == "" || a.StreamDeviceID == "" { + return errors.New("nest: tried to stop rtsp stream without a project or device ID") + } + + var reqv struct { + Command string `json:"command"` + Params struct { + StreamExtensionToken string `json:"streamExtensionToken"` + } `json:"params"` + } + reqv.Command = "sdm.devices.commands.CameraLiveStream.StopRtspStream" + reqv.Params.StreamExtensionToken = a.StreamExtensionToken + + b, err := json.Marshal(reqv) + if err != nil { + return err + } + + uri := "https://smartdevicemanagement.googleapis.com/v1/enterprises/" + + a.StreamProjectID + "/devices/" + a.StreamDeviceID + ":executeCommand" + req, err := http.NewRequest("POST", uri, bytes.NewReader(b)) + if err != nil { + return err + } + + req.Header.Set("Authorization", "Bearer "+a.Token) + + client := &http.Client{Timeout: time.Second * 5000} + res, err := client.Do(req) + if err != nil { + return err + } + + if res.StatusCode != 200 { + return errors.New("nest: wrong status: " + res.Status) + } + + a.StreamProjectID = "" + a.StreamDeviceID = "" + a.StreamExtensionToken = "" + a.StreamToken = "" return nil } @@ -261,10 +422,10 @@ type Device struct { //SdmDevicesTraitsCameraClipPreview struct { //} `json:"sdm.devices.traits.CameraClipPreview"` } `json:"traits"` - //ParentRelations []struct { - // Parent string `json:"parent"` - // DisplayName string `json:"displayName"` - //} `json:"parentRelations"` + ParentRelations []struct { + Parent string `json:"parent"` + DisplayName string `json:"displayName"` + } `json:"parentRelations"` } func (a *API) StartExtendStreamTimer() { @@ -277,7 +438,6 @@ func (a *API) StartExtendStreamTimer() { duration = time.Until(a.StreamExpiresAt.Add(-30 * time.Second)) a.extendTimer.Reset(duration) }) - } func (a *API) StopExtendStreamTimer() { diff --git a/pkg/nest/client.go b/pkg/nest/client.go index 0b243384..5fc589cb 100644 --- a/pkg/nest/client.go +++ b/pkg/nest/client.go @@ -5,16 +5,22 @@ import ( "net/url" "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/AlexxIT/go2rtc/pkg/rtsp" "github.com/AlexxIT/go2rtc/pkg/webrtc" pion "github.com/pion/webrtc/v3" ) -type Client struct { +type WebRTCClient struct { conn *webrtc.Conn api *API } -func Dial(rawURL string) (*Client, error) { +type RTSPClient struct { + conn *rtsp.Conn + api *API +} + +func Dial(rawURL string) (core.Producer, error) { u, err := url.Parse(rawURL) if err != nil { return nil, err @@ -36,6 +42,49 @@ func Dial(rawURL string) (*Client, error) { return nil, err } + device, err := nestAPI.GetDevice(projectID, deviceID) + if err != nil { + return nil, err + } + + for _, proto := range device.Traits.SdmDevicesTraitsCameraLiveStream.SupportedProtocols { + if proto == "WEB_RTC" { + return rtcConn(nestAPI, rawURL, projectID, deviceID) + } else if proto == "RTSP" { + return rtspConn(nestAPI, rawURL, projectID, deviceID) + } + } + + return nil, errors.New("nest: unsupported camera") +} + +func (c *WebRTCClient) GetMedias() []*core.Media { + return c.conn.GetMedias() +} + +func (c *WebRTCClient) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) { + return c.conn.GetTrack(media, codec) +} + +func (c *WebRTCClient) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error { + return c.conn.AddTrack(media, codec, track) +} + +func (c *WebRTCClient) Start() error { + c.api.StartExtendStreamTimer() + return c.conn.Start() +} + +func (c *WebRTCClient) Stop() error { + c.api.StopExtendStreamTimer() + return c.conn.Stop() +} + +func (c *WebRTCClient) MarshalJSON() ([]byte, error) { + return c.conn.MarshalJSON() +} + +func rtcConn(nestAPI *API, rawURL, projectID, deviceID string) (*WebRTCClient, error) { rtcAPI, err := webrtc.NewAPI() if err != nil { return nil, err @@ -77,31 +126,46 @@ func Dial(rawURL string) (*Client, error) { return nil, err } - return &Client{conn: conn, api: nestAPI}, nil + return &WebRTCClient{conn: conn, api: nestAPI}, nil } -func (c *Client) GetMedias() []*core.Media { - return c.conn.GetMedias() +func rtspConn(nestAPI *API, rawURL, projectID, deviceID string) (*RTSPClient, error) { + rtspURL, err := nestAPI.GenerateRtspStream(projectID, deviceID) + if err != nil { + return nil, err + } + + rtspClient := rtsp.NewClient(rtspURL) + if err := rtspClient.Dial(); err != nil { + return nil, err + } + if err := rtspClient.Describe(); err != nil { + return nil, err + } + + return &RTSPClient{conn: rtspClient, api: nestAPI}, nil } -func (c *Client) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) { +func (c *RTSPClient) GetMedias() []*core.Media { + result := c.conn.GetMedias() + return result +} + +func (c *RTSPClient) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) { return c.conn.GetTrack(media, codec) } -func (c *Client) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error { - return c.conn.AddTrack(media, codec, track) -} - -func (c *Client) Start() error { +func (c *RTSPClient) Start() error { c.api.StartExtendStreamTimer() return c.conn.Start() } -func (c *Client) Stop() error { +func (c *RTSPClient) Stop() error { + c.api.StopRTSPStream() c.api.StopExtendStreamTimer() return c.conn.Stop() } -func (c *Client) MarshalJSON() ([]byte, error) { +func (c *RTSPClient) MarshalJSON() ([]byte, error) { return c.conn.MarshalJSON() -} +} \ No newline at end of file From e1021a96af447a9448f92100f49a3a865f1d36a2 Mon Sep 17 00:00:00 2001 From: Jamal Fanaian Date: Thu, 11 Jul 2024 17:58:31 -0700 Subject: [PATCH 047/166] go fmt --- pkg/nest/api.go | 26 +++++++++++++------------- pkg/nest/client.go | 4 ++-- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/pkg/nest/api.go b/pkg/nest/api.go index 8aae2df2..80a421ba 100644 --- a/pkg/nest/api.go +++ b/pkg/nest/api.go @@ -23,7 +23,7 @@ type API struct { StreamSessionID string // RTSP - StreamToken string + StreamToken string StreamExtensionToken string extendTimer *time.Timer @@ -120,7 +120,7 @@ func (a *API) GetDevices(projectID string) (map[string]string, error) { supported := false for _, protocol := range device.Traits.SdmDevicesTraitsCameraLiveStream.SupportedProtocols { - if (protocol == "WEB_RTC" || protocol == "RTSP") { + if protocol == "WEB_RTC" || protocol == "RTSP" { supported = true break } @@ -231,7 +231,7 @@ func (a *API) ExtendStream() error { var reqv struct { Command string `json:"command"` Params struct { - MediaSessionID string `json:"mediaSessionId,omitempty"` + MediaSessionID string `json:"mediaSessionId,omitempty"` StreamExtensionToken string `json:"streamExtensionToken,omitempty"` } `json:"params"` } @@ -272,10 +272,10 @@ func (a *API) ExtendStream() error { var resv struct { Results struct { - ExpiresAt time.Time `json:"expiresAt"` - MediaSessionID string `json:"mediaSessionId"` - StreamExtensionToken string `json:"streamExtensionToken"` - StreamToken string `json:"streamToken"` + ExpiresAt time.Time `json:"expiresAt"` + MediaSessionID string `json:"mediaSessionId"` + StreamExtensionToken string `json:"streamExtensionToken"` + StreamToken string `json:"streamToken"` } `json:"results"` } @@ -293,8 +293,8 @@ func (a *API) ExtendStream() error { func (a *API) GenerateRtspStream(projectID, deviceID string) (string, error) { var reqv struct { - Command string `json:"command"` - Params struct {} `json:"params"` + Command string `json:"command"` + Params struct{} `json:"params"` } reqv.Command = "sdm.devices.commands.CameraLiveStream.GenerateRtspStream" @@ -324,10 +324,10 @@ func (a *API) GenerateRtspStream(projectID, deviceID string) (string, error) { var resv struct { Results struct { - StreamURLs map[string]string `json:"streamUrls"` - StreamExtensionToken string `json:"streamExtensionToken"` - StreamToken string `json:"streamToken"` - ExpiresAt time.Time `json:"expiresAt"` + StreamURLs map[string]string `json:"streamUrls"` + StreamExtensionToken string `json:"streamExtensionToken"` + StreamToken string `json:"streamToken"` + ExpiresAt time.Time `json:"expiresAt"` } `json:"results"` } diff --git a/pkg/nest/client.go b/pkg/nest/client.go index 5fc589cb..6d867dea 100644 --- a/pkg/nest/client.go +++ b/pkg/nest/client.go @@ -17,7 +17,7 @@ type WebRTCClient struct { type RTSPClient struct { conn *rtsp.Conn - api *API + api *API } func Dial(rawURL string) (core.Producer, error) { @@ -168,4 +168,4 @@ func (c *RTSPClient) Stop() error { func (c *RTSPClient) MarshalJSON() ([]byte, error) { return c.conn.MarshalJSON() -} \ No newline at end of file +} From 13dd3084c20b3bea53506382f0820ea2c8203415 Mon Sep 17 00:00:00 2001 From: Jamal Fanaian Date: Thu, 11 Jul 2024 18:47:05 -0700 Subject: [PATCH 048/166] Carry protocol info in stream URL --- internal/nest/init.go | 8 +++++--- pkg/nest/api.go | 41 ++++++++++------------------------------- pkg/nest/client.go | 15 +++++++++------ 3 files changed, 24 insertions(+), 40 deletions(-) diff --git a/internal/nest/init.go b/internal/nest/init.go index 01682414..8289af73 100644 --- a/internal/nest/init.go +++ b/internal/nest/init.go @@ -2,6 +2,7 @@ package nest import ( "net/http" + "strings" "github.com/AlexxIT/go2rtc/internal/api" "github.com/AlexxIT/go2rtc/internal/streams" @@ -38,11 +39,12 @@ func apiNest(w http.ResponseWriter, r *http.Request) { var items []*api.Source - for name, deviceID := range devices { - query.Set("device_id", deviceID) + for _, device := range devices { + query.Set("device_id", device.DeviceID) + query.Set("protocols", strings.Join(device.Protocols, ",")) items = append(items, &api.Source{ - Name: name, URL: "nest:?" + query.Encode(), + Name: device.Name, URL: "nest:?" + query.Encode(), }) } diff --git a/pkg/nest/api.go b/pkg/nest/api.go index 80a421ba..9e32cbcf 100644 --- a/pkg/nest/api.go +++ b/pkg/nest/api.go @@ -33,6 +33,12 @@ type Auth struct { AccessToken string } +type DeviceInfo struct { + Name string + DeviceID string + Protocols []string +} + var cache = map[string]*API{} var cacheMu sync.Mutex @@ -84,7 +90,7 @@ func NewAPI(clientID, clientSecret, refreshToken string) (*API, error) { return api, nil } -func (a *API) GetDevices(projectID string) (map[string]string, error) { +func (a *API) GetDevices(projectID string) ([]DeviceInfo, error) { uri := "https://smartdevicemanagement.googleapis.com/v1/enterprises/" + projectID + "/devices" req, err := http.NewRequest("GET", uri, nil) if err != nil { @@ -111,7 +117,7 @@ func (a *API) GetDevices(projectID string) (map[string]string, error) { return nil, err } - devices := map[string]string{} + devices := make([]DeviceInfo, 0, len(resv.Devices)) for _, device := range resv.Devices { if len(device.Traits.SdmDevicesTraitsCameraLiveStream.SupportedProtocols) == 0 { @@ -139,40 +145,13 @@ func (a *API) GetDevices(projectID string) (map[string]string, error) { if name == "" && len(device.ParentRelations) > 0 { name = device.ParentRelations[0].DisplayName } - devices[name] = device.Name[i+1:] + + devices = append(devices, DeviceInfo{Name: name, DeviceID: device.Name[i+1:], Protocols: device.Traits.SdmDevicesTraitsCameraLiveStream.SupportedProtocols}) } return devices, nil } -func (a *API) GetDevice(projectID, deviceID string) (Device, error) { - uri := "https://smartdevicemanagement.googleapis.com/v1/enterprises/" + projectID + "/devices/" + deviceID - req, err := http.NewRequest("GET", uri, nil) - if err != nil { - return Device{}, err - } - - req.Header.Set("Authorization", "Bearer "+a.Token) - - client := &http.Client{Timeout: time.Second * 5000} - res, err := client.Do(req) - if err != nil { - return Device{}, err - } - - if res.StatusCode != 200 { - return Device{}, errors.New("nest: wrong status: " + res.Status) - } - - var device Device - - if err = json.NewDecoder(res.Body).Decode(&device); err != nil { - return Device{}, err - } - - return device, nil -} - func (a *API) ExchangeSDP(projectID, deviceID, offer string) (string, error) { var reqv struct { Command string `json:"command"` diff --git a/pkg/nest/client.go b/pkg/nest/client.go index 6d867dea..e692359c 100644 --- a/pkg/nest/client.go +++ b/pkg/nest/client.go @@ -3,6 +3,7 @@ package nest import ( "errors" "net/url" + "strings" "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/rtsp" @@ -32,6 +33,12 @@ func Dial(rawURL string) (core.Producer, error) { refreshToken := query.Get("refresh_token") projectID := query.Get("project_id") deviceID := query.Get("device_id") + protocols := strings.Split(query.Get("protocols"), ",") + + // Default to WEB_RTC for backwards compataiility + if len(protocols) == 0 { + protocols = append(protocols, "WEB_RTC") + } if cliendID == "" || cliendSecret == "" || refreshToken == "" || projectID == "" || deviceID == "" { return nil, errors.New("nest: wrong query") @@ -42,12 +49,8 @@ func Dial(rawURL string) (core.Producer, error) { return nil, err } - device, err := nestAPI.GetDevice(projectID, deviceID) - if err != nil { - return nil, err - } - - for _, proto := range device.Traits.SdmDevicesTraitsCameraLiveStream.SupportedProtocols { + // Pick the first supported protocol in order of priority (WEB_RTC, RTSP) + for _, proto := range protocols { if proto == "WEB_RTC" { return rtcConn(nestAPI, rawURL, projectID, deviceID) } else if proto == "RTSP" { From c81caa4d2c7dbda9c78de2010d22b1b8e9dff39d Mon Sep 17 00:00:00 2001 From: On Freund Date: Wed, 17 Jul 2024 13:37:56 +0300 Subject: [PATCH 049/166] Install ffplay in container --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index b3888820..7a578980 100644 --- a/Dockerfile +++ b/Dockerfile @@ -44,7 +44,7 @@ FROM base # and other common tools for the echo source. # alsa-plugins-pulse for ALSA support (+0MB) # font-droid for FFmpeg drawtext filter (+2MB) -RUN apk add --no-cache tini ffmpeg bash curl jq alsa-plugins-pulse font-droid +RUN apk add --no-cache tini ffmpeg ffplay bash curl jq alsa-plugins-pulse font-droid # Hardware Acceleration for Intel CPU (+50MB) ARG TARGETARCH From 3762bdbccd599e173abb068c2d6cbfe9c424b105 Mon Sep 17 00:00:00 2001 From: Alex X Date: Thu, 18 Jul 2024 13:52:50 +0300 Subject: [PATCH 050/166] Fix mjpeg source for Foscam G2 camera #1258 --- pkg/mpjpeg/multipart.go | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/pkg/mpjpeg/multipart.go b/pkg/mpjpeg/multipart.go index abceea43..ca8924e5 100644 --- a/pkg/mpjpeg/multipart.go +++ b/pkg/mpjpeg/multipart.go @@ -18,15 +18,21 @@ func Next(rd *bufio.Reader) (http.Header, []byte, error) { return nil, nil, err } - if strings.HasPrefix(s, "--") { - break - } - if s == "\r\n" { continue } - return nil, nil, errors.New("multipart: wrong boundary: " + s) + if !strings.HasPrefix(s, "--") { + return nil, nil, errors.New("multipart: wrong boundary: " + s) + } + + // Foscam G2 has a awful implementation of MJPEG + // https://github.com/AlexxIT/go2rtc/issues/1258 + if b, _ := rd.Peek(2); string(b) == "--" { + continue + } + + break } tp := textproto.NewReader(rd) @@ -50,7 +56,5 @@ func Next(rd *bufio.Reader) (http.Header, []byte, error) { return nil, nil, err } - _, _ = rd.Discard(2) // skip "\r\n" - return http.Header(header), buf, nil } From c5bc761a52a07fb1cb2c6cd69ffacf0a01988e66 Mon Sep 17 00:00:00 2001 From: Alex X Date: Fri, 26 Jul 2024 07:55:15 +0300 Subject: [PATCH 051/166] Fix RTSP MJPEG source quality in some cases #559 --- pkg/mjpeg/mjpeg_test.go | 13 ++++++++ pkg/mjpeg/rfc2435.go | 73 ++++++++++++++++++++++++++++------------- 2 files changed, 64 insertions(+), 22 deletions(-) create mode 100644 pkg/mjpeg/mjpeg_test.go diff --git a/pkg/mjpeg/mjpeg_test.go b/pkg/mjpeg/mjpeg_test.go new file mode 100644 index 00000000..586f8c80 --- /dev/null +++ b/pkg/mjpeg/mjpeg_test.go @@ -0,0 +1,13 @@ +package mjpeg + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestRFC2435(t *testing.T) { + lqt, cqt := MakeTables(71) + require.Equal(t, byte(9), lqt[0]) + require.Equal(t, byte(10), cqt[0]) +} diff --git a/pkg/mjpeg/rfc2435.go b/pkg/mjpeg/rfc2435.go index f7e41330..44307896 100644 --- a/pkg/mjpeg/rfc2435.go +++ b/pkg/mjpeg/rfc2435.go @@ -2,21 +2,24 @@ package mjpeg // RFC 2435. Appendix A -var jpeg_luma_quantizer = []byte{ - 16, 11, 10, 16, 24, 40, 51, 61, - 12, 12, 14, 19, 26, 58, 60, 55, - 14, 13, 16, 24, 40, 57, 69, 56, - 14, 17, 22, 29, 51, 87, 80, 62, - 18, 22, 37, 56, 68, 109, 103, 77, - 24, 35, 55, 64, 81, 104, 113, 92, - 49, 64, 78, 87, 103, 121, 120, 101, - 72, 92, 95, 98, 112, 100, 103, 99, +// don't know why two tables are not respect RFC +// https://github.com/FFmpeg/FFmpeg/blob/master/libavformat/rtpdec_jpeg.c + +var jpeg_luma_quantizer = [64]byte{ + 16, 11, 12, 14, 12, 10, 16, 14, + 13, 14, 18, 17, 16, 19, 24, 40, + 26, 24, 22, 22, 24, 49, 35, 37, + 29, 40, 58, 51, 61, 60, 57, 51, + 56, 55, 64, 72, 92, 78, 64, 68, + 87, 69, 55, 56, 80, 109, 81, 87, + 95, 98, 103, 104, 103, 62, 77, 113, + 121, 112, 100, 120, 92, 101, 103, 99, } -var jpeg_chroma_quantizer = []byte{ - 17, 18, 24, 47, 99, 99, 99, 99, - 18, 21, 26, 66, 99, 99, 99, 99, - 24, 26, 56, 99, 99, 99, 99, 99, - 47, 66, 99, 99, 99, 99, 99, 99, +var jpeg_chroma_quantizer = [64]byte{ + 17, 18, 18, 24, 21, 24, 47, 26, + 26, 47, 99, 66, 56, 66, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, @@ -37,7 +40,7 @@ func MakeTables(q byte) (lqt, cqt []byte) { if q < 50 { factor = 5000 / factor - } else if q > 99 { + } else { factor = 200 - factor*2 } @@ -140,22 +143,35 @@ var chm_ac_symbols = []byte{ func MakeHeaders(p []byte, t byte, w, h uint16, lqt, cqt []byte) []byte { // Appendix A from https://www.rfc-editor.org/rfc/rfc2435 - p = append(p, 0xFF, 0xD8) + p = append(p, 0xFF, + 0xD8, // SOI + ) p = MakeQuantHeader(p, lqt, 0) p = MakeQuantHeader(p, cqt, 1) if t == 0 { - t = 0x21 + t = 0x21 // hsamp = 2, vsamp = 1 } else { - t = 0x22 + t = 0x22 // hsamp = 2, vsamp = 2 } - p = append(p, - 0xFF, 0xC0, 0, 17, 8, + p = append(p, 0xFF, + 0xC0, // SOF + 0, 17, // size + 8, // bits per component byte(h>>8), byte(h&0xFF), byte(w>>8), byte(w&0xFF), - 3, 0, t, 0, 1, 0x11, 1, 2, 0x11, 1, + 3, // number of components + 0, // comp 0 + t, + 0, // quant table 0 + 1, // comp 1 + 0x11, // hsamp = 1, vsamp = 1 + 1, // quant table 1 + 2, // comp 2 + 0x11, // hsamp = 1, vsamp = 1 + 1, // quant table 1 ) p = MakeHuffmanHeader(p, lum_dc_codelens, lum_dc_symbols, 0, 0) @@ -163,7 +179,20 @@ func MakeHeaders(p []byte, t byte, w, h uint16, lqt, cqt []byte) []byte { p = MakeHuffmanHeader(p, chm_dc_codelens, chm_dc_symbols, 1, 0) p = MakeHuffmanHeader(p, chm_ac_codelens, chm_ac_symbols, 1, 1) - return append(p, 0xFF, 0xDA, 0, 12, 3, 0, 0, 1, 0x11, 2, 0x11, 0, 63, 0) + return append(p, 0xFF, + 0xDA, // SOS + 0, 12, // size + 3, // 3 components + 0, // comp 0 + 0, // huffman table 0 + 1, // comp 1 + 0x11, // huffman table 1 + 2, // comp 2 + 0x11, // huffman table 1 + 0, // first DCT coeff + 63, // last DCT coeff + 0, // sucessive approx + ) } func MakeQuantHeader(p []byte, qt []byte, tableNo byte) []byte { From 68fa42249e12c85f4166dfb1d847010637ff6806 Mon Sep 17 00:00:00 2001 From: Alex X Date: Fri, 26 Jul 2024 14:01:43 +0300 Subject: [PATCH 052/166] Fix PCM audio from Hikvision cameras --- pkg/core/codec.go | 7 ++++++- pkg/rtsp/rtsp_test.go | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/pkg/core/codec.go b/pkg/core/codec.go index 9c6c6b79..b138df28 100644 --- a/pkg/core/codec.go +++ b/pkg/core/codec.go @@ -157,7 +157,12 @@ func UnmarshalCodec(md *sdp.MediaDescription, payloadType string) *Codec { } } - if c.Name == "" { + switch c.Name { + case "PCM": + // https://www.reddit.com/r/Hikvision/comments/17elxex/comment/k642g2r/ + // check pkg/rtsp/rtsp_test.go TestHikvisionPCM + c.Name = CodecPCML + case "": // https://en.wikipedia.org/wiki/RTP_payload_formats switch payloadType { case "0": diff --git a/pkg/rtsp/rtsp_test.go b/pkg/rtsp/rtsp_test.go index 7eb317a7..a13341b4 100644 --- a/pkg/rtsp/rtsp_test.go +++ b/pkg/rtsp/rtsp_test.go @@ -3,6 +3,7 @@ package rtsp import ( "testing" + "github.com/AlexxIT/go2rtc/pkg/core" "github.com/stretchr/testify/assert" ) @@ -159,3 +160,34 @@ a=control:trackID=2 assert.Equal(t, "recvonly", medias[0].Direction) assert.Equal(t, "recvonly", medias[1].Direction) } + +func TestHikvisionPCM(t *testing.T) { + s := `v=0 +o=- 1721969533379665 1721969533379665 IN IP4 192.168.1.12 +s=Media Presentation +e=NONE +b=AS:5100 +t=0 0 +a=control:rtsp://192.168.1.12:554/Streaming/channels/101/ +m=video 0 RTP/AVP 96 +c=IN IP4 0.0.0.0 +b=AS:5000 +a=recvonly +a=x-dimensions:3200,1800 +a=control:rtsp://192.168.1.12:554/Streaming/channels/101/trackID=1 +a=rtpmap:96 H264/90000 +a=fmtp:96 profile-level-id=420029; packetization-mode=1; sprop-parameter-sets=Z2QAM6wVFKAyAOP5f/AAEAAWyAAAH0AAB1MAIA==,aO48sA== +m=audio 0 RTP/AVP 11 +c=IN IP4 0.0.0.0 +b=AS:50 +a=recvonly +a=control:rtsp://192.168.1.12:554/Streaming/channels/101/trackID=2 +a=rtpmap:11 PCM/48000 +a=Media_header:MEDIAINFO=494D4B4801030000040000010170011080BB0000007D000000000000000000000000000000000000; +a=appversion:1.0 +` + medias, err := UnmarshalSDP([]byte(s)) + assert.Nil(t, err) + assert.Len(t, medias, 2) + assert.Equal(t, core.CodecPCML, medias[1].Codecs[0].Name) +} From 57d48f53e03e06e4af5c92f9bf8889af543e604b Mon Sep 17 00:00:00 2001 From: Alex X Date: Fri, 26 Jul 2024 14:15:53 +0300 Subject: [PATCH 053/166] Fix PCM audio quality for WebRTC --- pkg/webrtc/consumer.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pkg/webrtc/consumer.go b/pkg/webrtc/consumer.go index 2dcab436..fb90442c 100644 --- a/pkg/webrtc/consumer.go +++ b/pkg/webrtc/consumer.go @@ -64,6 +64,10 @@ func (c *Conn) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiv } case core.CodecPCMA, core.CodecPCMU, core.CodecPCM, core.CodecPCML: + // Fix audio quality https://github.com/AlexxIT/WebRTC/issues/500 + // should be before ResampleToG711, because it will be called last + sender.Handler = pcm.RepackG711(false, sender.Handler) + if codec.ClockRate == 0 { if codec.Name == core.CodecPCM || codec.Name == core.CodecPCML { codec.Name = core.CodecPCMA @@ -71,9 +75,6 @@ func (c *Conn) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiv codec.ClockRate = 8000 sender.Handler = pcm.ResampleToG711(track.Codec, 8000, sender.Handler) } - - // Fix audio quality https://github.com/AlexxIT/WebRTC/issues/500 - sender.Handler = pcm.RepackG711(false, sender.Handler) } // TODO: rewrite this dirty logic From ed99025bd6198a4ee57b0312b7c7cd837ff0fad9 Mon Sep 17 00:00:00 2001 From: Alex X Date: Fri, 26 Jul 2024 14:47:42 +0300 Subject: [PATCH 054/166] Add support S16LE (PCM-LE) for RTSP server --- pkg/core/media.go | 8 ++++++-- pkg/rtsp/consumer.go | 2 ++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/pkg/core/media.go b/pkg/core/media.go index 2284d0cd..72ab58c6 100644 --- a/pkg/core/media.go +++ b/pkg/core/media.go @@ -124,9 +124,13 @@ func MarshalSDP(name string, medias []*Media) ([]byte, error) { codec := media.Codecs[0] - name := codec.Name - if name == CodecELD { + switch codec.Name { + case CodecELD: name = CodecAAC + case CodecPCML: + name = CodecPCM // beacuse we using pcm.LittleToBig for RTSP server + default: + name = codec.Name } md := &sdp.MediaDescription{ diff --git a/pkg/rtsp/consumer.go b/pkg/rtsp/consumer.go index b6df188f..860ed113 100644 --- a/pkg/rtsp/consumer.go +++ b/pkg/rtsp/consumer.go @@ -162,6 +162,8 @@ func (c *Conn) packetWriter(codec *core.Codec, channel, payloadType uint8) core. case core.CodecJPEG: handlerFunc = mjpeg.RTPPay(handlerFunc) } + } else if codec.Name == core.CodecPCML { + handlerFunc = pcm.LittleToBig(handlerFunc) } else if c.PacketSize != 0 { switch codec.Name { case core.CodecH264: From d559ec0208ca41e4dc3cc3d94d92c46cc4d4ca39 Mon Sep 17 00:00:00 2001 From: Alex X Date: Fri, 26 Jul 2024 17:00:16 +0300 Subject: [PATCH 055/166] Fix wrong media values in SDP for some cameras #1278 --- pkg/rtsp/helpers.go | 10 ++++++++-- pkg/rtsp/rtsp_test.go | 42 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/pkg/rtsp/helpers.go b/pkg/rtsp/helpers.go index c0f02f5b..1a687c01 100644 --- a/pkg/rtsp/helpers.go +++ b/pkg/rtsp/helpers.go @@ -38,8 +38,14 @@ func UnmarshalSDP(rawSDP []byte) ([]*core.Media, error) { // Fix invalid media type (errSDPInvalidValue) caused by // some TP-LINK IP camera, e.g. TL-IPC44GW - m := regexp.MustCompile("m=application/[^ ]+") - rawSDP = m.ReplaceAll(rawSDP, []byte("m=application")) + m := regexp.MustCompile("m=[^ ]+ ") + for _, i := range m.FindAll(rawSDP, -1) { + switch string(i[2 : len(i)-1]) { + case "audio", "video", "application": + default: + rawSDP = bytes.Replace(rawSDP, i, []byte("m=application "), 1) + } + } if err == io.EOF { rawSDP = append(rawSDP, '\n') diff --git a/pkg/rtsp/rtsp_test.go b/pkg/rtsp/rtsp_test.go index a13341b4..43248ba6 100644 --- a/pkg/rtsp/rtsp_test.go +++ b/pkg/rtsp/rtsp_test.go @@ -161,6 +161,48 @@ a=control:trackID=2 assert.Equal(t, "recvonly", medias[1].Direction) } +func TestBugSDP6(t *testing.T) { + // https://github.com/AlexxIT/go2rtc/issues/1278 + s := `v=0 +o=- 3730506281693 1 IN IP4 172.20.0.215 +s=IP camera Live streaming +i=stream1 +t=0 0 +a=tool:LIVE555 Streaming Media v2014.02.04 +a=type:broadcast +a=control:* +a=range:npt=0- +a=x-qt-text-nam:IP camera Live streaming +a=x-qt-text-inf:stream1 +m=video 0 RTP/AVP 26 +c=IN IP4 172.20.0.215 +b=AS:1500 +a=x-bufferdelay:0.55000 +a=x-dimensions:1280,960 +a=control:track1 +m=audio 0 RTP/AVP 0 +c=IN IP4 172.20.0.215 +b=AS:64 +a=x-bufferdelay:0.55000 +a=control:track2 +m=application 0 RTP/AVP 107 +c=IN IP4 172.20.0.215 +b=AS:1 +a=x-bufferdelay:0.55000 +a=rtpmap:107 vnd.onvif.metadata/90000/500 +a=control:track4 +m=vana 0 RTP/AVP 108 +c=IN IP4 172.20.0.215 +b=AS:1 +a=x-bufferdelay:0.55000 +a=rtpmap:108 video.analysis/90000/500 +a=control:track5 +` + medias, err := UnmarshalSDP([]byte(s)) + assert.Nil(t, err) + assert.Len(t, medias, 4) +} + func TestHikvisionPCM(t *testing.T) { s := `v=0 o=- 1721969533379665 1721969533379665 IN IP4 192.168.1.12 From 23e8f7e0aac12825ecd74ce4ce08b76a743a82b7 Mon Sep 17 00:00:00 2001 From: Sergey Krashevich Date: Sun, 28 Jul 2024 05:34:49 +0300 Subject: [PATCH 056/166] refactor(api): move port extraction logic to Init function for prevent data race --- internal/api/api.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/internal/api/api.go b/internal/api/api.go index 86817bd0..419e2bdf 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -69,6 +69,8 @@ func Init() { } if cfg.Mod.Listen != "" { + _, port, _ := net.SplitHostPort(cfg.Mod.Listen) + Port, _ = strconv.Atoi(port) go listen("tcp", cfg.Mod.Listen) } @@ -92,10 +94,6 @@ func listen(network, address string) { log.Info().Str("addr", address).Msg("[api] listen") - if network == "tcp" { - Port = ln.Addr().(*net.TCPAddr).Port - } - server := http.Server{ Handler: Handler, ReadHeaderTimeout: 5 * time.Second, // Example: Set to 5 seconds From bd88695e591096c0b5f59abb6af6b9b7bb1be6ca Mon Sep 17 00:00:00 2001 From: Alex X Date: Sun, 4 Aug 2024 10:18:24 +0300 Subject: [PATCH 057/166] Fix AnnexB parsing in some cases --- pkg/bubble/client.go | 2 +- pkg/dvrip/producer.go | 4 +- pkg/h264/annexb/annexb.go | 84 ++++++++++++++++---------------- pkg/h264/annexb/annexb_test.go | 85 +++++++++++++++++++++++++++++++++ pkg/kasa/producer.go | 4 +- pkg/magic/bitstream/producer.go | 4 +- pkg/mpegts/demuxer.go | 2 +- 7 files changed, 133 insertions(+), 52 deletions(-) create mode 100644 pkg/h264/annexb/annexb_test.go diff --git a/pkg/bubble/client.go b/pkg/bubble/client.go index 5afba779..7a71d555 100644 --- a/pkg/bubble/client.go +++ b/pkg/bubble/client.go @@ -231,7 +231,7 @@ func (c *Client) Handle() error { Header: rtp.Header{ Timestamp: core.Now90000(), }, - Payload: annexb.EncodeToAVCC(b[6:], false), + Payload: annexb.EncodeToAVCC(b[6:]), } c.videoTrack.WriteRTP(pkt) } else { diff --git a/pkg/dvrip/producer.go b/pkg/dvrip/producer.go index c87017b4..4f49da1e 100644 --- a/pkg/dvrip/producer.go +++ b/pkg/dvrip/producer.go @@ -53,7 +53,7 @@ func (c *Producer) Start() error { packet := &rtp.Packet{ Header: rtp.Header{Timestamp: c.videoTS}, - Payload: annexb.EncodeToAVCC(payload, false), + Payload: annexb.EncodeToAVCC(payload), } //log.Printf("[AVC] %v, len: %d, ts: %10d", h265.Types(payload), len(payload), packet.Timestamp) @@ -146,7 +146,7 @@ func (c *Producer) probe() error { c.videoTS = binary.LittleEndian.Uint32(ts) c.videoDT = 90000 / uint32(fps) - payload := annexb.EncodeToAVCC(b[16:], false) + payload := annexb.EncodeToAVCC(b[16:]) c.addVideoTrack(b[4], payload) case 0xFA: // audio diff --git a/pkg/h264/annexb/annexb.go b/pkg/h264/annexb/annexb.go index 13f06622..26614a82 100644 --- a/pkg/h264/annexb/annexb.go +++ b/pkg/h264/annexb/annexb.go @@ -11,64 +11,60 @@ const startAUD = StartCode + "\x09\xF0" const startAUDstart = startAUD + StartCode // EncodeToAVCC -// will change original slice data! -// safeAppend should be used if original slice has useful data after end (part of other slice) // // FFmpeg MPEG-TS: 00000001 AUD 00000001 SPS 00000001 PPS 000001 IFrame // FFmpeg H264: 00000001 SPS 00000001 PPS 000001 IFrame 00000001 PFrame -func EncodeToAVCC(b []byte, safeAppend bool) []byte { - const minSize = len(StartCode) + 1 - - // 1. Check frist "start code" - if len(b) < len(startAUDstart) || string(b[:len(StartCode)]) != StartCode { - return nil - } - - // 2. Skip Access unit delimiter (AUD) from FFmpeg - if string(b[:len(startAUDstart)]) == startAUDstart { - b = b[6:] - } - +// Reolink: 000001 AUD 000001 VPS 00000001 SPS 00000001 PPS 00000001 IDR 00000001 IDR +func EncodeToAVCC(annexb []byte) (avc []byte) { var start int - for i, n := minSize, len(b)-minSize; i < n; { - // 3. Check "start code" (first 2 bytes) - if b[i] != 0 || b[i+1] != 0 { - i++ - continue - } + avc = make([]byte, 0, len(annexb)+4) // init memory with little overhead - // 4. Check "start code" (3 bytes size or 4 bytes size) - if b[i+2] == 1 { - if safeAppend { - // protect original slice from "damage" - b = bytes.Clone(b) - safeAppend = false + for i := 0; ; i++ { + var offset int + + if i+3 < len(annexb) { + // search next separator + if annexb[i] == 0 && annexb[i+1] == 0 { + if annexb[i+2] == 1 { + offset = 3 // 00 00 01 + } else if annexb[i+2] == 0 && annexb[i+3] == 1 { + offset = 4 // 00 00 00 01 + } else { + continue + } + } else { + continue } - - // convert start code from 3 bytes to 4 bytes - b = append(b, 0) - copy(b[i+1:], b[i:]) - n++ - } else if b[i+2] != 0 || b[i+3] != 1 { - i++ - continue + } else { + i = len(annexb) // move i to data end } - // 5. Set size for previous AU - size := uint32(i - start - len(StartCode)) - binary.BigEndian.PutUint32(b[start:], size) + if start != 0 { + size := uint32(i - start) + avc = binary.BigEndian.AppendUint32(avc, size) + avc = append(avc, annexb[start:i]...) + } - start = i + // sometimes FFmpeg put separator at the end + if i += offset; i == len(annexb) { + break + } - i += minSize + if isAUD(annexb[i]) { + start = 0 // skip this NALU + } else { + start = i // save this position + } } - // 6. Set size for last AU - size := uint32(len(b) - start - len(StartCode)) - binary.BigEndian.PutUint32(b[start:], size) + return +} - return b +func isAUD(b byte) bool { + const h264 = 9 + const h265 = 35 << 1 + return b&0b0001_1111 == h264 || b&0b0111_1110 == h265 } func DecodeAVCC(b []byte, safeClone bool) []byte { diff --git a/pkg/h264/annexb/annexb_test.go b/pkg/h264/annexb/annexb_test.go new file mode 100644 index 00000000..7220f570 --- /dev/null +++ b/pkg/h264/annexb/annexb_test.go @@ -0,0 +1,85 @@ +package annexb + +import ( + "bytes" + "encoding/binary" + "encoding/hex" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func decode(s string) []byte { + b, _ := hex.DecodeString(strings.ReplaceAll(s, " ", "")) + return b +} + +func naluTypes(avcc []byte) (types []byte) { + for { + types = append(types, avcc[4]) + + size := 4 + binary.BigEndian.Uint32(avcc) + if size < uint32(len(avcc)) { + avcc = avcc[size:] + } else { + break + } + } + return +} + +func TestFFmpegH264(t *testing.T) { + // ffmpeg -re -i bbb.mp4 -c copy -f h264 - + s := "000000016764001fac2484014016ec0440000003004000000c23c60c92 0000000168ee32c8b0 00000165888080033ffef5f8454f32cb1bb4203f854dd69bc2ca91b2bce1fb3527440000030000030000030000030050999841d1afd324aea000000300000f600011c0001b40004e40011f0003b80010800059000238000be0005e000220001100000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300004041 00000001" + b := EncodeToAVCC(decode(s)) + require.True(t, bytes.HasSuffix(b, []byte{0x40, 0x41})) + n := naluTypes(b) + require.Equal(t, []byte{0x67, 0x68, 0x65}, n) +} + +func TestFFmpegMPEGTSH264(t *testing.T) { + // ffmpeg -re -i bbb.mp4 -c copy -f mpegts - + s := "00000001 09f0 000000016764001fac2484014016ec0440000003004000000c23c60c92 0000000168ee32c8b0 00000165888080033ffef5f8454f32cb1bb4203f854dd69bc2ca91b2bce1fb3527440000030000030000030000030050999841d1afd324aea000000300000f600011c0001b40004e40011f0003b80010800059000238000be0005e000220001100000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300004041" + b := EncodeToAVCC(decode(s)) + n := naluTypes(b) + require.Equal(t, []byte{0x67, 0x68, 0x65}, n) +} + +func TestFFmpegHEVC(t *testing.T) { + // ffmpeg -re -i bbb.mp4 -c libx265 -preset superfast -tune zerolatency -f hevc - + s := "0000000140010c01ffff01600000030090000003000003005dba0240 0000000142010101600000030090000003000003005da00280802d165bab930bc05a7080000003008000000c04 000000014401c1718312 0000014e0105ffffffffffffffffff2b2ca2de09b51747dbbb55a4fe7fc2fc4e7832363520286275696c642032303829202d20332e352b3131312d6330616631386464353a5b57696e646f77735d5b4743432031332e322e305d5b3634206269745d20386269742b31306269742b3132626974202d20482e3236352f4845564320636f646563202d20436f7079726967687420323031332d3230313820286329204d756c7469636f7265776172" + b := EncodeToAVCC(decode(s)) + n := naluTypes(b) + require.Equal(t, []byte{0x40, 0x42, 0x44, 0x4e}, n) +} + +func TestFFmpegHEVC2(t *testing.T) { + // ffmpeg -re -i bbb.mp4 -c libx265 -preset superfast -tune zerolatency -f hevc - + s := "0000000140010c01ffff01600000030090000003000003005dba0240 0000000142010101600000030090000003000003005da00280802d165bab930bc05a7080000003008000000c04 000000014401c1718312 0000014e0105ffffffffffffffffff2b2ca2de09b51747dbbb55a4fe7fc2fc4e7832363520286275696c642032303829202d20332e352b3131312d6330616631386464353a5b57696e646f77735d5b4743432031332e322e305d5b3634206269745d20386269742b31306269742b3132626974202d20482e3236352f4845564320636f646563202d20436f7079726967687420323031332d3230313820286329204d756c7469636f7265776172652c20496e63202d20687474703a2f2f783236352e6f7267202d206f7074696f6e733a2063707569643d31303439303731206672616d652d746872656164733d32206e756d612d706f6f6c733d3420777070206e6f2d706d6f6465206e6f2d706d65206e6f2d70736e72206e6f2d7373696d206c6f672d6c6576656c3d322062697464657074683d3820696e7075742d6373703d31206670733d32342f3120696e7075742d7265733d313238307837323020696e7465726c6163653d3020746f74616c2d6672616d65733d30206c6576656c2d6964633d3020686967682d746965723d31207568642d62643d30207265663d31206e6f2d616c6c6f772d6e6f6e2d636f6e666f726d616e6365207265706561742d6865616465727320616e6e657862206e6f2d617564206e6f2d656f62206e6f2d656f73206e6f2d68726420696e666f20686173683d302074656d706f72616c2d6c61796572733d30206f70656e2d676f70206d696e2d6b6579696e743d3234206b6579696e743d32353020676f702d6c6f6f6b61686561643d3020626672616d65733d3020622d61646170743d30206e6f2d622d707972616d696420626672616d652d626961733d302072632d6c6f6f6b61686561643d30206c6f6f6b61686561642d736c696365733d34207363656e656375743d30206e6f2d686973742d7363656e65637574207261646c3d30206e6f2d73706c696365206e6f2d696e7472612d72656672657368206374753d3332206d696e2d63752d73697a653d38206e6f2d72656374206e6f2d616d70206d61782d74752d73697a653d33322074752d696e7465722d64657074683d312074752d696e7472612d64657074683d31206c696d69742d74753d302072646f712d6c6576656c3d302064796e616d69632d72643d302e3030206e6f2d7373696d2d7264207369676e68696465206e6f2d74736b6970206e722d696e7472613d30206e722d696e7465723d30206e6f2d636f6e73747261696e65642d696e747261207374726f6e672d696e7472612d736d6f6f7468696e67206d61782d6d657267653d32206c696d69742d726566733d30206e6f2d6c696d69742d6d6f646573206d653d31207375626d653d31206d6572616e67653d35372074656d706f72616c2d6d7670206e6f2d6672616d652d647570206e6f2d686d65206e6f2d77656967687470206e6f2d77656967687462206e6f2d616e616c797a652d7372632d70696373206465626c6f636b3d303a30206e6f2d73616f206e6f2d73616f2d6e6f6e2d6465626c6f636b2072643d322073656c6563746976652d73616f3d30206561726c792d736b69702072736b697020666173742d696e747261206e6f2d74736b69702d66617374206e6f2d63752d6c6f73736c657373206e6f2d622d696e747261206e6f2d73706c697472642d736b697020726470656e616c74793d30207073792d72643d322e3030207073792d72646f713d302e3030206e6f2d72642d726566696e65206e6f2d6c6f73736c65737320636271706f6666733d3020637271706f6666733d302072633d637266206372663d32382e302071636f6d703d302e3630207170737465703d342073746174732d77726974653d302073746174732d726561643d30206970726174696f3d312e34302061712d6d6f64653d302061712d737472656e6774683d302e3030206e6f2d637574726565207a6f6e652d636f756e743d30206e6f2d7374726963742d6362722071672d73697a653d3332206e6f2d72632d677261696e2071706d61783d36392071706d696e3d30206e6f2d636f6e73742d766276207361723d31206f7665727363616e3d3020766964656f666f726d61743d352072616e67653d3020636f6c6f727072696d3d32207472616e736665723d3220636f6c6f726d61747269783d32206368726f6d616c6f633d31206368726f6d616c6f632d746f703d30206368726f6d616c6f632d626f74746f6d3d3020646973706c61792d77696e646f773d3020636c6c3d302c30206d696e2d6c756d613d30206d61782d6c756d613d323535206c6f67322d6d61782d706f632d6c73623d38207675692d74696d696e672d696e666f207675692d6872642d696e666f20736c696365733d31206e6f2d6f70742d71702d707073206e6f2d6f70742d7265662d6c6973742d6c656e6774682d707073206e6f2d6d756c74692d706173732d6f70742d727073207363656e656375742d626961733d302e3035206e6f2d6f70742d63752d64656c74612d7170206e6f2d61712d6d6f74696f6e206e6f2d6864723130206e6f2d68647231302d6f7074206e6f2d6468647231302d6f7074206e6f2d6964722d7265636f766572792d73656920616e616c797369732d72657573652d6c6576656c3d3020616e616c797369732d736176652d72657573652d6c6576656c3d3020616e616c797369732d6c6f61642d72657573652d6c6576656c3d30207363616c652d666163746f723d3020726566696e652d696e7472613d3020726566696e652d696e7465723d3020726566696e652d6d763d3120726566696e652d6374752d646973746f7274696f6e3d30206e6f2d6c696d69742d73616f206374752d696e666f3d30206e6f2d6c6f77706173732d64637420726566696e652d616e616c797369732d747970653d3020636f70792d7069633d31206d61782d617573697a652d666163746f723d312e30206e6f2d64796e616d69632d726566696e65206e6f2d73696e676c652d736569206e6f2d686576632d6171206e6f2d737674206e6f2d6669656c642071702d61646170746174696f6e2d72616e67653d312e3030207363656e656375742d61776172652d71703d30636f6e666f726d616e63652d77696e646f772d6f6666736574732072696768743d3020626f74746f6d3d30206465636f6465722d6d61782d726174653d30206e6f2d7662762d6c6976652d6d756c74692d70617373206e6f2d6d63737466206e6f2d7362726380 0000012801adc2e5bca307b9ce6b18b5ad6a525294a6d117ffd3917322eebaeda718a0000003000003000003021207706824da718a00000300000300000300044408d5db4e31400000030000030000030012500c2725a000000300000300000300002a600e4880000003000003000003000019301180000003000003000003000007d400000300000300000300000300010b000003000003000003000003001810000003000003000003000003019100000300000300000300000d38000003000003000003000067c000000300000300000300025e000003000003000003000c58000003000003000003002b60000003000003000003007f80000003000003000003016300000300000300000303b2000003000003000006e400000300000300000e18000003000003000018d00000030000030000292000000300000300003ce00000030000030000030000030000030000bb80" + b := EncodeToAVCC(decode(s)) + n := naluTypes(b) + require.Equal(t, []byte{0x40, 0x42, 0x44, 0x4e, 0x28}, n) +} + +func TestFFmpegMPEGTSHEVC(t *testing.T) { + // ffmpeg -re -i bbb.mp4 -c libx265 -preset superfast -tune zerolatency -an -f mpegts - + s := "00000001460150 0000000140010c01ffff01600000030090000003000003005dba0240 0000000142010101600000030090000003000003005da00280802d165bab930bc05a7080000003008000000c04 000000014401c1718312 0000014e0105ffffffffffffffffff2b2ca2de09b51747dbbb55a4fe7fc2fc4e7832363520286275696c642032303829202d20332e352b3131312d6330616631386464353a5b57696e646f77735d5b4743432031332e322e305d5b3634206269745d20386269742b31306269742b3132626974202d20482e3236352f4845564320636f646563202d20436f7079726967687420323031332d3230313820286329204d756c7469636f7265776172652c20496e63202d20687474703a2f2f783236352e6f7267202d206f7074696f6e733a2063707569643d31303439303731206672616d652d746872656164733d32206e756d612d706f6f6c733d3420777070206e6f2d706d6f6465206e6f2d706d65206e6f2d70736e72206e6f2d7373696d206c6f672d6c6576656c3d322062697464657074683d3820696e7075742d6373703d31206670733d32342f3120696e7075742d7265733d313238307837323020696e7465726c6163653d3020746f74616c2d6672616d65733d30206c6576656c2d6964633d3020686967682d746965723d31207568642d62643d30207265663d31206e6f2d616c6c6f772d6e6f6e2d636f6e666f726d616e6365207265706561742d6865616465727320616e6e657862206e6f2d617564206e6f2d656f62206e6f2d656f73206e6f2d68726420696e666f20686173683d302074656d706f72616c2d6c61796572733d30206f70656e2d676f70206d696e2d6b6579696e743d3234206b6579696e743d32353020676f702d6c6f6f6b61686561643d3020626672616d65733d3020622d61646170743d30206e6f2d622d707972616d696420626672616d652d626961733d302072632d6c6f6f6b61686561643d30206c6f6f6b61686561642d736c696365733d34207363656e656375743d30206e6f2d686973742d7363656e65637574207261646c3d30206e6f2d73706c696365206e6f2d696e7472612d72656672657368206374753d3332206d696e2d63752d73697a653d38206e6f2d72656374206e6f2d616d70206d61782d74752d73697a653d33322074752d696e7465722d64657074683d312074752d696e7472612d64657074683d31206c696d69742d74753d302072646f712d6c6576656c3d302064796e616d69632d72643d302e3030206e6f2d7373696d2d7264207369676e68696465206e6f2d74736b6970206e722d696e7472613d30206e722d696e7465723d30206e6f2d636f6e73747261696e65642d696e747261207374726f6e672d696e7472612d736d6f6f7468696e67206d61782d6d657267653d32206c696d69742d726566733d30206e6f2d6c696d69742d6d6f646573206d653d31207375626d653d31206d6572616e67653d35372074656d706f72616c2d6d7670206e6f2d6672616d652d647570206e6f2d686d65206e6f2d77656967687470206e6f2d77656967687462206e6f2d616e616c797a652d7372632d70696373206465626c6f636b3d303a30206e6f2d73616f206e6f2d73616f2d6e6f6e2d6465626c6f636b2072643d322073656c6563746976652d73616f3d30206561726c792d736b69702072736b697020666173742d696e747261206e6f2d74736b69702d66617374206e6f2d63752d6c6f73736c657373206e6f2d622d696e747261206e6f2d73706c697472642d736b697020726470656e616c74793d30207073792d72643d322e3030207073792d72646f713d302e3030206e6f2d72642d726566696e65206e6f2d6c6f73736c65737320636271706f6666733d3020637271706f6666733d302072633d637266206372663d32382e302071636f6d703d302e3630207170737465703d342073746174732d77726974653d302073746174732d726561643d30206970726174696f3d312e34302061712d6d6f64653d302061712d737472656e6774683d302e3030206e6f2d637574726565207a6f6e652d636f756e743d30206e6f2d7374726963742d6362722071672d73697a653d3332206e6f2d72632d677261696e2071706d61783d36392071706d696e3d30206e6f2d636f6e73742d766276207361723d31206f7665727363616e3d3020766964656f666f726d61743d352072616e67653d3020636f6c6f727072696d3d32207472616e736665723d3220636f6c6f726d61747269783d32206368726f6d616c6f633d31206368726f6d616c6f632d746f703d30206368726f6d616c6f632d626f74746f6d3d3020646973706c61792d77696e646f773d3020636c6c3d302c30206d696e2d6c756d613d30206d61782d6c756d613d323535206c6f67322d6d61782d706f632d6c73623d38207675692d74696d696e672d696e666f207675692d6872642d696e666f20736c696365733d31206e6f2d6f70742d71702d707073206e6f2d6f70742d7265662d6c6973742d6c656e6774682d707073206e6f2d6d756c74692d706173732d6f70742d727073207363656e656375742d626961733d302e3035206e6f2d6f70742d63752d64656c74612d7170206e6f2d61712d6d6f74696f6e206e6f2d6864723130206e6f2d68647231302d6f7074206e6f2d6468647231302d6f7074206e6f2d6964722d7265636f766572792d73656920616e616c797369732d72657573652d6c6576656c3d3020616e616c797369732d736176652d72657573652d6c6576656c3d3020616e616c797369732d6c6f61642d72657573652d6c6576656c3d30207363616c652d666163746f723d3020726566696e652d696e7472613d3020726566696e652d696e7465723d3020726566696e652d6d763d3120726566696e652d6374752d646973746f7274696f6e3d30206e6f2d6c696d69742d73616f206374752d696e666f3d30206e6f2d6c6f77706173732d64637420726566696e652d616e616c797369732d747970653d3020636f70792d7069633d31206d61782d617573697a652d666163746f723d312e30206e6f2d64796e616d69632d726566696e65206e6f2d73696e676c652d736569206e6f2d686576632d6171206e6f2d737674206e6f2d6669656c642071702d61646170746174696f6e2d72616e67653d312e3030207363656e656375742d61776172652d71703d30636f6e666f726d616e63652d77696e646f772d6f6666736574732072696768743d3020626f74746f6d3d30206465636f6465722d6d61782d726174653d30206e6f2d7662762d6c6976652d6d756c74692d70617373206e6f2d6d63737466206e6f2d7362726380 0000012801adc2e5bca307b9ce6b18b5ad6a525294a6d117ffd3917322eebaeda718a0000003000003000003021207706824da718a00000300000300000300044408d5db4e31400000030000030000030012500c2725a000000300000300000300002a600e4880000003000003000003000019301180000003000003000003000007d400000300000300000300000300010b000003000003000003000003001810000003000003000003000003019100000300000300000300000d38000003000003000003000067c000000300000300000300025e000003000003000003000c58000003000003000003002b60000003000003000003007f80000003000003000003016300000300000300000303b2000003000003000006e400000300000300000e18000003000003000018d00000030000030000292000000300000300003ce00000030000030000030000030000030000bb80" + b := EncodeToAVCC(decode(s)) + n := naluTypes(b) + require.Equal(t, []byte{0x40, 0x42, 0x44, 0x4e, 0x28}, n) +} + +func TestReolink(t *testing.T) { + s := "000001460150 00000140010C01FFFF01600000030000030000030000030096AC09 0000000142010101600000030000030000030000030096A001E020021C7F8AAD3BA24BB804000013D800018CE008 000000014401C072F0941E3648 000000012601" + b := EncodeToAVCC(decode(s)) + n := naluTypes(b) + require.Equal(t, []byte{0x40, 0x42, 0x44, 0x26}, n) +} + +func TestDahua(t *testing.T) { + s := "00000001460150 00000140010c01ffff01400000030000030000030000030099ac0900 0000000142010101400000030000030000030000030099a001402005a1fe5aee46c1ae550400 000000014401c073c04c9000 000000012601" + b := EncodeToAVCC(decode(s)) + n := naluTypes(b) + require.Equal(t, []byte{0x40, 0x42, 0x44, 0x26}, n) +} diff --git a/pkg/kasa/producer.go b/pkg/kasa/producer.go index 22d10216..697c19e8 100644 --- a/pkg/kasa/producer.go +++ b/pkg/kasa/producer.go @@ -113,7 +113,7 @@ func (c *Producer) Start() error { Header: rtp.Header{ Timestamp: uint32(ts * 90000), }, - Payload: annexb.EncodeToAVCC(body, false), + Payload: annexb.EncodeToAVCC(body), } video.WriteRTP(pkt) } @@ -168,7 +168,7 @@ func (c *Producer) probe() error { } waitVideo = false - body = annexb.EncodeToAVCC(body, false) + body = annexb.EncodeToAVCC(body) codec := h264.AVCCToCodec(body) media = &core.Media{ Kind: core.KindVideo, diff --git a/pkg/magic/bitstream/producer.go b/pkg/magic/bitstream/producer.go index b84f049b..5f00f41e 100644 --- a/pkg/magic/bitstream/producer.go +++ b/pkg/magic/bitstream/producer.go @@ -25,7 +25,7 @@ func Open(r io.Reader) (*Producer, error) { return nil, err } - buf = annexb.EncodeToAVCC(buf, false) // won't break original buffer + buf = annexb.EncodeToAVCC(buf) // won't break original buffer var codec *core.Codec var format string @@ -82,7 +82,7 @@ func (c *Producer) Start() error { if len(c.Receivers) > 0 { pkt := &rtp.Packet{ Header: rtp.Header{Timestamp: core.Now90000()}, - Payload: annexb.EncodeToAVCC(buf[:i], true), + Payload: annexb.EncodeToAVCC(buf[:i]), } c.Receivers[0].WriteRTP(pkt) diff --git a/pkg/mpegts/demuxer.go b/pkg/mpegts/demuxer.go index a3efc2c9..08ccca39 100644 --- a/pkg/mpegts/demuxer.go +++ b/pkg/mpegts/demuxer.go @@ -364,7 +364,7 @@ func (p *PES) GetPacket() (pkt *rtp.Packet) { Header: rtp.Header{ PayloadType: p.StreamType, }, - Payload: annexb.EncodeToAVCC(p.Payload, false), + Payload: annexb.EncodeToAVCC(p.Payload), } if p.DTS != 0 { From 66de2f91b655b99bbb86c08f9037737eb528bb41 Mon Sep 17 00:00:00 2001 From: Chris Thach <12981621+cthach@users.noreply.github.com> Date: Tue, 6 Aug 2024 22:25:55 +0000 Subject: [PATCH 058/166] Fix resource leak in Nest source due to lack of closing HTTP response bodies --- pkg/nest/api.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pkg/nest/api.go b/pkg/nest/api.go index 5e0d3407..9d187054 100644 --- a/pkg/nest/api.go +++ b/pkg/nest/api.go @@ -53,6 +53,8 @@ func NewAPI(clientID, clientSecret, refreshToken string) (*API, error) { if err != nil { return nil, err } + defer res.Body.Close() + if res.StatusCode != 200 { return nil, errors.New("nest: wrong status: " + res.Status) } @@ -92,6 +94,7 @@ func (a *API) GetDevices(projectID string) (map[string]string, error) { if err != nil { return nil, err } + defer res.Body.Close() if res.StatusCode != 200 { return nil, errors.New("nest: wrong status: " + res.Status) @@ -157,6 +160,7 @@ func (a *API) ExchangeSDP(projectID, deviceID, offer string) (string, error) { if err != nil { return "", err } + defer res.Body.Close() if res.StatusCode != 200 { return "", errors.New("nest: wrong status: " + res.Status) @@ -211,6 +215,7 @@ func (a *API) ExtendStream() error { if err != nil { return err } + defer res.Body.Close() if res.StatusCode != 200 { return errors.New("nest: wrong status: " + res.Status) From 2311d5eabe70c9d6a493c5ff7be28614b9fd3fe4 Mon Sep 17 00:00:00 2001 From: Alex X Date: Sun, 1 Sep 2024 15:56:19 +0300 Subject: [PATCH 059/166] Change go version to 1.20 for Windows 7 support --- .github/workflows/build.yml | 4 ++-- README.md | 8 +++---- go.mod | 2 +- internal/exec/exec.go | 3 +-- internal/ffmpeg/ffmpeg.go | 3 +-- internal/webrtc/candidates.go | 6 ++--- pkg/core/slices.go | 43 +++++++++++++++++++++++++++++++++++ pkg/homekit/consumer.go | 2 +- pkg/homekit/helpers.go | 5 ++-- pkg/webrtc/api.go | 6 ++--- scripts/README.md | 8 +++++++ scripts/build.cmd | 5 ++++ 12 files changed, 74 insertions(+), 21 deletions(-) create mode 100644 pkg/core/slices.go diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8e981c19..50ba06a9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,7 +29,7 @@ jobs: with: { name: go2rtc_win64, path: go2rtc.exe } - name: Build go2rtc_win32 - env: { GOOS: windows, GOARCH: 386 } + env: { GOOS: windows, GOARCH: 386, GOTOOLCHAIN: go1.20.14 } run: go build -ldflags "-s -w" -trimpath - name: Upload go2rtc_win32 uses: actions/upload-artifact@v4 @@ -85,7 +85,7 @@ jobs: with: { name: go2rtc_linux_mipsel, path: go2rtc } - name: Build go2rtc_mac_amd64 - env: { GOOS: darwin, GOARCH: amd64 } + env: { GOOS: darwin, GOARCH: amd64, GOTOOLCHAIN: go1.20.14 } run: go build -ldflags "-s -w" -trimpath - name: Upload go2rtc_mac_amd64 uses: actions/upload-artifact@v4 diff --git a/README.md b/README.md index c31ed748..70ad4712 100644 --- a/README.md +++ b/README.md @@ -115,8 +115,8 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg Download binary for your OS from [latest release](https://github.com/AlexxIT/go2rtc/releases/): -- `go2rtc_win64.zip` - Windows 64-bit -- `go2rtc_win32.zip` - Windows 32-bit +- `go2rtc_win64.zip` - Windows 10+ 64-bit +- `go2rtc_win32.zip` - Windows 7+ 32-bit - `go2rtc_win_arm64.zip` - Windows ARM 64-bit - `go2rtc_linux_amd64` - Linux 64-bit - `go2rtc_linux_i386` - Linux 32-bit @@ -124,8 +124,8 @@ Download binary for your OS from [latest release](https://github.com/AlexxIT/go2 - `go2rtc_linux_arm` - Linux ARM 32-bit (ex. Raspberry 32-bit OS) - `go2rtc_linux_armv6` - Linux ARMv6 (for old Raspberry 1 and Zero) - `go2rtc_linux_mipsel` - Linux MIPS (ex. [Xiaomi Gateway 3](https://github.com/AlexxIT/XiaomiGateway3), [Wyze cameras](https://github.com/gtxaspec/wz_mini_hacks)) -- `go2rtc_mac_amd64.zip` - Mac Intel 64-bit -- `go2rtc_mac_arm64.zip` - Mac ARM 64-bit +- `go2rtc_mac_amd64.zip` - macOS 10.13+ Intel 64-bit +- `go2rtc_mac_arm64.zip` - macOS ARM 64-bit Don't forget to fix the rights `chmod +x go2rtc_xxx_xxx` on Linux and Mac. diff --git a/go.mod b/go.mod index d3cb791f..b038b110 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/AlexxIT/go2rtc -go 1.22 +go 1.20 require ( github.com/asticode/go-astits v1.13.0 diff --git a/internal/exec/exec.go b/internal/exec/exec.go index 035317d9..bce166e8 100644 --- a/internal/exec/exec.go +++ b/internal/exec/exec.go @@ -10,7 +10,6 @@ import ( "net/url" "os" "os/exec" - "slices" "strings" "sync" "time" @@ -230,7 +229,7 @@ func trimSpace(b []byte) []byte { func setRemoteInfo(info core.Info, source string, args []string) { info.SetSource(source) - if i := slices.Index(args, "-i"); i > 0 && i < len(args)-1 { + if i := core.Index(args, "-i"); i > 0 && i < len(args)-1 { rawURL := args[i+1] if u, err := url.Parse(rawURL); err == nil && u.Host != "" { info.SetRemoteAddr(u.Host) diff --git a/internal/ffmpeg/ffmpeg.go b/internal/ffmpeg/ffmpeg.go index 062e5aaf..12a9be83 100644 --- a/internal/ffmpeg/ffmpeg.go +++ b/internal/ffmpeg/ffmpeg.go @@ -2,7 +2,6 @@ package ffmpeg import ( "net/url" - "slices" "strings" "github.com/AlexxIT/go2rtc/internal/api" @@ -44,7 +43,7 @@ func Init() { return "", err } args := parseArgs(url[7:]) - if slices.Contains(args.Codecs, "auto") { + if core.Contains(args.Codecs, "auto") { return "", nil // force call streams.HandleFunc("ffmpeg") } return "exec:" + args.String(), nil diff --git a/internal/webrtc/candidates.go b/internal/webrtc/candidates.go index b92c4656..adbfb4a7 100644 --- a/internal/webrtc/candidates.go +++ b/internal/webrtc/candidates.go @@ -2,10 +2,10 @@ package webrtc import ( "net" - "slices" "strings" "github.com/AlexxIT/go2rtc/internal/api/ws" + "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/webrtc" pion "github.com/pion/webrtc/v3" ) @@ -75,14 +75,14 @@ func FilterCandidate(candidate *pion.ICECandidate) bool { // host candidate should be in the hosts list if candidate.Typ == pion.ICECandidateTypeHost && filters.Candidates != nil { - if !slices.Contains(filters.Candidates, candidate.Address) { + if !core.Contains(filters.Candidates, candidate.Address) { return false } } if filters.Networks != nil { networkType := NetworkType(candidate.Protocol.String(), candidate.Address) - if !slices.Contains(filters.Networks, networkType) { + if !core.Contains(filters.Networks, networkType) { return false } } diff --git a/pkg/core/slices.go b/pkg/core/slices.go new file mode 100644 index 00000000..747d813f --- /dev/null +++ b/pkg/core/slices.go @@ -0,0 +1,43 @@ +package core + +// This code copied from go1.21 for backward support in go1.20. +// We need to support go1.20 for Windows 7 + +// Index returns the index of the first occurrence of v in s, +// or -1 if not present. +func Index[S ~[]E, E comparable](s S, v E) int { + for i := range s { + if v == s[i] { + return i + } + } + return -1 +} + +// Contains reports whether v is present in s. +func Contains[S ~[]E, E comparable](s S, v E) bool { + return Index(s, v) >= 0 +} + +type Ordered interface { + ~int | ~int8 | ~int16 | ~int32 | ~int64 | + ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr | + ~float32 | ~float64 | + ~string +} + +// Max returns the maximal value in x. It panics if x is empty. +// For floating-point E, Max propagates NaNs (any NaN value in x +// forces the output to be NaN). +func Max[S ~[]E, E Ordered](x S) E { + if len(x) < 1 { + panic("slices.Max: empty list") + } + m := x[0] + for i := 1; i < len(x); i++ { + if x[i] > m { + m = x[i] + } + } + return m +} diff --git a/pkg/homekit/consumer.go b/pkg/homekit/consumer.go index 1c665233..ea83146f 100644 --- a/pkg/homekit/consumer.go +++ b/pkg/homekit/consumer.go @@ -3,7 +3,7 @@ package homekit import ( "fmt" "io" - "math/rand/v2" + "math/rand" "net" "time" diff --git a/pkg/homekit/helpers.go b/pkg/homekit/helpers.go index 89c63dc3..a1719671 100644 --- a/pkg/homekit/helpers.go +++ b/pkg/homekit/helpers.go @@ -2,7 +2,6 @@ package homekit import ( "encoding/hex" - "slices" "github.com/AlexxIT/go2rtc/pkg/aac" "github.com/AlexxIT/go2rtc/pkg/core" @@ -22,8 +21,8 @@ func videoToMedia(codecs []camera.VideoCodec) *core.Media { for _, codec := range codecs { for _, param := range codec.CodecParams { // get best profile and level - profileID := slices.Max(param.ProfileID) - level := slices.Max(param.Level) + profileID := core.Max(param.ProfileID) + level := core.Max(param.Level) profile := videoProfiles[profileID] + videoLevels[level] mediaCodec := &core.Codec{ Name: videoCodecs[codec.CodecType], diff --git a/pkg/webrtc/api.go b/pkg/webrtc/api.go index f63cabfd..0361e6b4 100644 --- a/pkg/webrtc/api.go +++ b/pkg/webrtc/api.go @@ -2,8 +2,8 @@ package webrtc import ( "net" - "slices" + "github.com/AlexxIT/go2rtc/pkg/core" "github.com/pion/interceptor" "github.com/pion/webrtc/v3" ) @@ -47,7 +47,7 @@ func NewServerAPI(network, address string, filters *Filters) (*webrtc.API, error if filters != nil && filters.Interfaces != nil { s.SetIncludeLoopbackCandidate(true) s.SetInterfaceFilter(func(name string) bool { - return slices.Contains(filters.Interfaces, name) + return core.Contains(filters.Interfaces, name) }) } else { // disable listen on Hassio docker interfaces @@ -59,7 +59,7 @@ func NewServerAPI(network, address string, filters *Filters) (*webrtc.API, error if filters != nil && filters.IPs != nil { s.SetIncludeLoopbackCandidate(true) s.SetIPFilter(func(ip net.IP) bool { - return slices.Contains(filters.IPs, ip.String()) + return core.Contains(filters.IPs, ip.String()) }) } diff --git a/scripts/README.md b/scripts/README.md index efcef154..eeb8c25d 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -1,3 +1,11 @@ +## Versions + +[Go 1.20](https://go.dev/doc/go1.20) is last version with support Windows 7 and macOS 10.13. +Go 1.21 support only Windows 10 and macOS 10.15. + +So we will set `go 1.20` (minimum version) inside `go.mod` file. And will use env `GOTOOLCHAIN=go1.20.14` for building +`win32` and `mac_amd64` binaries. All other binaries will use latest go version. + ## Build - UPX-3.96 pack broken bin for `linux_mipsel` diff --git a/scripts/build.cmd b/scripts/build.cmd index 54565b2d..4a54039d 100644 --- a/scripts/build.cmd +++ b/scripts/build.cmd @@ -1,15 +1,18 @@ @ECHO OFF +@SET GOTOOLCHAIN= @SET GOOS=windows @SET GOARCH=amd64 @SET FILENAME=go2rtc_win64.zip go build -ldflags "-s -w" -trimpath && 7z a -mx9 -sdel %FILENAME% go2rtc.exe +@SET GOTOOLCHAIN=go1.20.14 @SET GOOS=windows @SET GOARCH=386 @SET FILENAME=go2rtc_win32.zip go build -ldflags "-s -w" -trimpath && 7z a -mx9 -sdel %FILENAME% go2rtc.exe +@SET GOTOOLCHAIN= @SET GOOS=windows @SET GOARCH=arm64 @SET FILENAME=go2rtc_win_arm64.zip @@ -47,11 +50,13 @@ go build -ldflags "-s -w" -trimpath -o %FILENAME% && upx %FILENAME% @SET FILENAME=go2rtc_linux_mipsel go build -ldflags "-s -w" -trimpath -o %FILENAME% && upx %FILENAME% +@SET GOTOOLCHAIN=go1.20.14 @SET GOOS=darwin @SET GOARCH=amd64 @SET FILENAME=go2rtc_mac_amd64.zip go build -ldflags "-s -w" -trimpath && 7z a -mx9 -sdel %FILENAME% go2rtc +@SET GOTOOLCHAIN= @SET GOOS=darwin @SET GOARCH=arm64 @SET FILENAME=go2rtc_mac_arm64.zip From 8399edce6ab1e51e43009464a6383e526009c73c Mon Sep 17 00:00:00 2001 From: Alex X Date: Thu, 5 Sep 2024 11:58:05 +0300 Subject: [PATCH 060/166] Fix RTSP AAC audio from very buggy noname camera #1328 --- pkg/aac/adts.go | 1 - pkg/aac/rtp.go | 9 +++++++++ pkg/aac/rtp_test.go | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 pkg/aac/rtp_test.go diff --git a/pkg/aac/adts.go b/pkg/aac/adts.go index d5e7828e..94a13ad7 100644 --- a/pkg/aac/adts.go +++ b/pkg/aac/adts.go @@ -9,7 +9,6 @@ import ( ) func IsADTS(b []byte) bool { - _ = b[1] return len(b) > 7 && b[0] == 0xFF && b[1]&0xF6 == 0xF0 } diff --git a/pkg/aac/rtp.go b/pkg/aac/rtp.go index b5ae4a10..1faa2e27 100644 --- a/pkg/aac/rtp.go +++ b/pkg/aac/rtp.go @@ -22,6 +22,15 @@ func RTPDepay(handler core.HandlerFunc) core.HandlerFunc { //log.Printf("[RTP/AAC] units: %d, size: %4d, ts: %10d, %t", headersSize/2, len(packet.Payload), packet.Timestamp, packet.Marker) if len(packet.Payload) < int(2+headersSize) { + // In very rare cases noname cameras may send data not according to the standard + // https://github.com/AlexxIT/go2rtc/issues/1328 + if IsADTS(packet.Payload) { + clone := *packet + clone.Version = RTPPacketVersionAAC + clone.Timestamp = timestamp + clone.Payload = clone.Payload[ADTSHeaderSize:] + handler(&clone) + } return } diff --git a/pkg/aac/rtp_test.go b/pkg/aac/rtp_test.go new file mode 100644 index 00000000..c541b255 --- /dev/null +++ b/pkg/aac/rtp_test.go @@ -0,0 +1,33 @@ +package aac + +import ( + "encoding/hex" + "testing" + + "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/pion/rtp" + "github.com/stretchr/testify/require" +) + +func TestBuggy_RTSP_AAC(t *testing.T) { + // https: //github.com/AlexxIT/go2rtc/issues/1328 + payload, _ := hex.DecodeString("fff16080431ffc211ad4458aa309a1c0a8761a230502b7c74b2b5499252a010555e32e460128303c8ace4fd3260d654a424f7e7c65eddc96735fc6f1ac0edf94fdefa0e0bd6370da1c07b9c0e77a9d6e86b196a1ac7439dcafadcffcf6d89f60ac67f8884868e931383ad3e40cf5495470d1f606ef6f7624d285b951ebfa0e42641ab98f1371182b237d14f1bd16ad714fa2f1c6a7d23ebde7a0e34a2eca156a608a4caec49d9dca4b6fe2a09e9cdbf762c5b4148a3914abb7959c991228b0837b5988334b9fc18b8fac689b5ca1e4661573bbb8b253a86cae7ec14ace49969a9a76fd571ab6e650764cb59114d61dcedf07ac61b39e4ac66adebfd0d0ab45d518dd3c161049823f150864d977cf0855172ac8482e4b25fe911325d19617558c5405af74aff5492e4599bee53f2dbdf0503730af37078550f84c956b7ee89aae83c154fa2fa6e6792c5ddd5cd5cf6bb96bf055fee7f93bed59ffb039daee5ea7e5593cb194e9091e417c67d8f73026a6a6ae056e808f7c65c03d1b9197d3709ceb63bc7b979f7ba71df5e7c6395d99d6ea229000a6bc16fb4346d6b27d32f5d8d1200736d9366d59c0c9547210813b602473da9c46f9015bbb37594c1dd90cd6a36e96bd5d6a1445ab93c9e65505ec2c722bb4cc27a10600139a48c83594dde145253c386f6627d8c6e5102fe3828a590c709bc87f55b37e97d1ae72b017b09c6bb2c13299817bb45cc67318e10b6822075b97c6a03ec1c0") + packet := &rtp.Packet{ + Header: rtp.Header{ + Version: 2, + Marker: true, + SequenceNumber: 36944, + Timestamp: 4217191328, + SSRC: 12892774, + }, + Payload: payload, + } + + var size int + + RTPDepay(func(packet *core.Packet) { + size = len(packet.Payload) + })(packet) + + require.Equal(t, len(payload), size+ADTSHeaderSize) +} From eb8a13d8c2b72da5031db40335294f8b9c6dbe27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michele=20Pr=C3=A0?= Date: Mon, 16 Sep 2024 12:42:34 +0200 Subject: [PATCH 061/166] data race for streams map https://go.dev/doc/articles/race_detector --- internal/streams/streams.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/streams/streams.go b/internal/streams/streams.go index ff0f5654..7502c7d5 100644 --- a/internal/streams/streams.go +++ b/internal/streams/streams.go @@ -61,6 +61,9 @@ func New(name string, source string) *Stream { return nil } + streamsMu.Lock() + defer streamsMu.Unlock() + stream := NewStream(source) streams[name] = stream return stream From 8128edad43ae63357d336faf0bb3030163d2efb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michele=20Pr=C3=A0?= Date: Mon, 16 Sep 2024 16:42:22 +0200 Subject: [PATCH 062/166] Update streams.go --- internal/streams/streams.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/streams/streams.go b/internal/streams/streams.go index 7502c7d5..7930a645 100644 --- a/internal/streams/streams.go +++ b/internal/streams/streams.go @@ -61,10 +61,11 @@ func New(name string, source string) *Stream { return nil } + stream := NewStream(source) + streamsMu.Lock() defer streamsMu.Unlock() - stream := NewStream(source) streams[name] = stream return stream } From 6f9f1c3a3519b3c321b421abaebc0eef8766afbc Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Thu, 19 Sep 2024 16:48:37 +0200 Subject: [PATCH 063/166] Build the docker image for linux/arm/v6 --- .github/workflows/build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 50ba06a9..1bb01ca7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -159,6 +159,7 @@ jobs: platforms: | linux/amd64 linux/386 + linux/arm/v6 linux/arm/v7 linux/arm64/v8 push: ${{ github.event_name != 'pull_request' }} From 7b77e41253c9dc07eea21058746dacf3d35b0f94 Mon Sep 17 00:00:00 2001 From: Alex X Date: Sun, 22 Sep 2024 07:24:25 +0300 Subject: [PATCH 064/166] Add support arm/v6 to Dockerfile --- Dockerfile | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/Dockerfile b/Dockerfile index b3888820..7e235631 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,13 +3,18 @@ # 0. Prepare images ARG PYTHON_VERSION="3.11" ARG GO_VERSION="1.22" -ARG NGROK_VERSION="3" - -FROM python:${PYTHON_VERSION}-alpine AS base -FROM ngrok/ngrok:${NGROK_VERSION}-alpine AS ngrok -# 1. Build go2rtc binary +# 1. Download ngrok binary (for support arm/v6) +FROM alpine AS ngrok +ARG TARGETARCH +ARG TARGETOS + +ADD https://bin.equinox.io/c/bNyj1mQVY4c/ngrok-v3-stable-${TARGETOS}-${TARGETARCH}.tgz / +RUN tar -xzf /ngrok-v3-stable-${TARGETOS}-${TARGETARCH}.tgz -C /bin + + +# 2. Build go2rtc binary FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-alpine AS build ARG TARGETPLATFORM ARG TARGETOS @@ -30,15 +35,8 @@ COPY . . RUN --mount=type=cache,target=/root/.cache/go-build CGO_ENABLED=0 go build -ldflags "-s -w" -trimpath -# 2. Collect all files -FROM scratch AS rootfs - -COPY --from=build /build/go2rtc /usr/local/bin/ -COPY --from=ngrok /bin/ngrok /usr/local/bin/ - - # 3. Final image -FROM base +FROM python:${PYTHON_VERSION}-alpine AS base # Install ffmpeg, tini (for signal handling), # and other common tools for the echo source. @@ -56,7 +54,8 @@ RUN if [ "${TARGETARCH}" = "amd64" ]; then apk add --no-cache libva-intel-driver # Hardware: AMD and NVidia VDPAU (not sure about this) # RUN libva-vdpau-driver mesa-vdpau-gallium (+150MB total) -COPY --from=rootfs / / +COPY --from=build /build/go2rtc /usr/local/bin/ +COPY --from=ngrok /bin/ngrok /usr/local/bin/ ENTRYPOINT ["/sbin/tini", "--"] VOLUME /config From 388c40808020bcc4e6ea1feb7c015591d4b95b8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michele=20Pr=C3=A0?= Date: Fri, 27 Sep 2024 18:14:41 +0200 Subject: [PATCH 065/166] defer used wisely --- internal/streams/streams.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/internal/streams/streams.go b/internal/streams/streams.go index 7930a645..1bab036e 100644 --- a/internal/streams/streams.go +++ b/internal/streams/streams.go @@ -64,9 +64,8 @@ func New(name string, source string) *Stream { stream := NewStream(source) streamsMu.Lock() - defer streamsMu.Unlock() - streams[name] = stream + streamsMu.Unlock() return stream } From 95a5283c86ff8945b193101610be16433e132adc Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Tue, 22 Oct 2024 16:31:31 +0200 Subject: [PATCH 066/166] Extend streams API to allow multiple sources --- internal/homekit/api.go | 2 +- internal/streams/api.go | 32 ++++++++++++++++++++++++++++---- internal/streams/stream.go | 6 ++++++ internal/streams/streams.go | 12 +++++++----- 4 files changed, 42 insertions(+), 10 deletions(-) diff --git a/internal/homekit/api.go b/internal/homekit/api.go index abd8e97c..bd01259d 100644 --- a/internal/homekit/api.go +++ b/internal/homekit/api.go @@ -101,7 +101,7 @@ func apiPair(id, url string) error { return err } - streams.New(id, conn.URL()) + streams.New(id, []string{conn.URL()}) return app.PatchConfig(id, conn.URL(), "streams") } diff --git a/internal/streams/api.go b/internal/streams/api.go index d64c4846..7c635089 100644 --- a/internal/streams/api.go +++ b/internal/streams/api.go @@ -1,6 +1,7 @@ package streams import ( + "encoding/json" "net/http" "github.com/AlexxIT/go2rtc/internal/api" @@ -8,13 +9,18 @@ import ( "github.com/AlexxIT/go2rtc/pkg/probe" ) +func returnAllStreams(w http.ResponseWriter) { + api.ResponseJSON(w, streams) +} + func apiStreams(w http.ResponseWriter, r *http.Request) { query := r.URL.Query() src := query.Get("src") // without source - return all streams list - if src == "" && r.Method != "POST" { - api.ResponseJSON(w, streams) + // PUT checks first body for sources + if src == "" && r.Method != "POST" && r.Method != "PUT" { + returnAllStreams(w) return } @@ -47,13 +53,31 @@ func apiStreams(w http.ResponseWriter, r *http.Request) { if name == "" { name = src } + var sources []string + if src != "" { + sources = []string{src} + } else if r.Header.Get("Content-Type") == "application/json" { + var data struct { + Sources []string `json:"sources"` + } + if err := json.NewDecoder(r.Body).Decode(&data); err != nil { + log.Error().Err(err).Caller().Send() + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + sources = data.Sources + } else { + // without source(s) - return all streams list + returnAllStreams(w) + return + } - if New(name, src) == nil { + if New(name, sources) == nil { http.Error(w, "", http.StatusBadRequest) return } - if err := app.PatchConfig(name, src, "streams"); err != nil { + if err := app.PatchConfig(name, sources, "streams"); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) } diff --git a/internal/streams/stream.go b/internal/streams/stream.go index bb832694..e194e0ac 100644 --- a/internal/streams/stream.go +++ b/internal/streams/stream.go @@ -21,6 +21,12 @@ func NewStream(source any) *Stream { return &Stream{ producers: []*Producer{NewProducer(source)}, } + case []string: + s := new(Stream) + for _, str := range source { + s.producers = append(s.producers, NewProducer(str)) + } + return s case []any: s := new(Stream) for _, src := range source { diff --git a/internal/streams/streams.go b/internal/streams/streams.go index 1bab036e..91c20f40 100644 --- a/internal/streams/streams.go +++ b/internal/streams/streams.go @@ -56,12 +56,14 @@ func Validate(source string) error { return nil } -func New(name string, source string) *Stream { - if Validate(source) != nil { - return nil +func New(name string, sources []string) *Stream { + for _, source := range sources { + if Validate(source) != nil { + return nil + } } - stream := NewStream(source) + stream := NewStream(sources) streamsMu.Lock() streams[name] = stream @@ -105,7 +107,7 @@ func Patch(name string, source string) *Stream { } // create new stream with this name - return New(name, source) + return New(name, []string{source}) } func GetOrPatch(query url.Values) *Stream { From a8d394efd78b6eb7458e428c6067de436c6586bf Mon Sep 17 00:00:00 2001 From: Alex X Date: Thu, 24 Oct 2024 20:44:37 +0300 Subject: [PATCH 067/166] Update PUT /api/streams for support multiple src params --- internal/homekit/api.go | 2 +- internal/streams/api.go | 32 ++++---------------------------- internal/streams/streams.go | 4 ++-- 3 files changed, 7 insertions(+), 31 deletions(-) diff --git a/internal/homekit/api.go b/internal/homekit/api.go index bd01259d..abd8e97c 100644 --- a/internal/homekit/api.go +++ b/internal/homekit/api.go @@ -101,7 +101,7 @@ func apiPair(id, url string) error { return err } - streams.New(id, []string{conn.URL()}) + streams.New(id, conn.URL()) return app.PatchConfig(id, conn.URL(), "streams") } diff --git a/internal/streams/api.go b/internal/streams/api.go index 7c635089..d6042974 100644 --- a/internal/streams/api.go +++ b/internal/streams/api.go @@ -1,7 +1,6 @@ package streams import ( - "encoding/json" "net/http" "github.com/AlexxIT/go2rtc/internal/api" @@ -9,18 +8,13 @@ import ( "github.com/AlexxIT/go2rtc/pkg/probe" ) -func returnAllStreams(w http.ResponseWriter) { - api.ResponseJSON(w, streams) -} - func apiStreams(w http.ResponseWriter, r *http.Request) { query := r.URL.Query() src := query.Get("src") // without source - return all streams list - // PUT checks first body for sources - if src == "" && r.Method != "POST" && r.Method != "PUT" { - returnAllStreams(w) + if src == "" && r.Method != "POST" { + api.ResponseJSON(w, streams) return } @@ -53,31 +47,13 @@ func apiStreams(w http.ResponseWriter, r *http.Request) { if name == "" { name = src } - var sources []string - if src != "" { - sources = []string{src} - } else if r.Header.Get("Content-Type") == "application/json" { - var data struct { - Sources []string `json:"sources"` - } - if err := json.NewDecoder(r.Body).Decode(&data); err != nil { - log.Error().Err(err).Caller().Send() - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - sources = data.Sources - } else { - // without source(s) - return all streams list - returnAllStreams(w) - return - } - if New(name, sources) == nil { + if New(name, query["src"]...) == nil { http.Error(w, "", http.StatusBadRequest) return } - if err := app.PatchConfig(name, sources, "streams"); err != nil { + if err := app.PatchConfig(name, query["src"], "streams"); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) } diff --git a/internal/streams/streams.go b/internal/streams/streams.go index 91c20f40..ae50e6c9 100644 --- a/internal/streams/streams.go +++ b/internal/streams/streams.go @@ -56,7 +56,7 @@ func Validate(source string) error { return nil } -func New(name string, sources []string) *Stream { +func New(name string, sources ...string) *Stream { for _, source := range sources { if Validate(source) != nil { return nil @@ -107,7 +107,7 @@ func Patch(name string, source string) *Stream { } // create new stream with this name - return New(name, []string{source}) + return New(name, source) } func GetOrPatch(query url.Values) *Stream { From 16e48314990d851368e57ffbe7b1afe5b01b5b15 Mon Sep 17 00:00:00 2001 From: Alex X Date: Thu, 24 Oct 2024 23:31:21 +0300 Subject: [PATCH 068/166] Add the option to pass ICE servers with an async WebRTC offer #1408 --- internal/api/ws/ws.go | 28 +++++++++++------------ internal/webrtc/webrtc.go | 48 ++++++++++++++++++++++++++------------- 2 files changed, 46 insertions(+), 30 deletions(-) diff --git a/internal/api/ws/ws.go b/internal/api/ws/ws.go index 800a377d..0eb45aa7 100644 --- a/internal/api/ws/ws.go +++ b/internal/api/ws/ws.go @@ -1,6 +1,7 @@ package ws import ( + "encoding/json" "io" "net/http" "net/url" @@ -36,22 +37,16 @@ var log zerolog.Logger type Message struct { Type string `json:"type"` Value any `json:"value,omitempty"` + Raw []byte `json:"-"` } -func (m *Message) String() string { - if s, ok := m.Value.(string); ok { - return s - } - return "" +func (m *Message) String() (value string) { + _ = json.Unmarshal(m.Raw, &value) + return } -func (m *Message) GetString(key string) string { - if v, ok := m.Value.(map[string]any); ok { - if s, ok := v[key].(string); ok { - return s - } - } - return "" +func (m *Message) Unmarshal(v any) error { + return json.Unmarshal(m.Raw, v) } type WSHandler func(tr *Transport, msg *Message) error @@ -118,8 +113,11 @@ func apiWS(w http.ResponseWriter, r *http.Request) { }) for { - msg := new(Message) - if err = ws.ReadJSON(msg); err != nil { + var raw struct { + Type string `json:"type"` + Value json.RawMessage `json:"value"` + } + if err = ws.ReadJSON(&raw); err != nil { if !websocket.IsCloseError(err, websocket.CloseNoStatusReceived) { log.Trace().Err(err).Caller().Send() } @@ -127,6 +125,8 @@ func apiWS(w http.ResponseWriter, r *http.Request) { break } + msg := &Message{Type: raw.Type, Raw: raw.Value} + log.Trace().Str("type", msg.Type).Msg("[api] ws msg") if handler := wsHandlers[msg.Type]; handler != nil { diff --git a/internal/webrtc/webrtc.go b/internal/webrtc/webrtc.go index 8b4943c3..fe25c919 100644 --- a/internal/webrtc/webrtc.go +++ b/internal/webrtc/webrtc.go @@ -40,15 +40,17 @@ func Init() { AddCandidate(network, candidate) } + var err error + // create pionAPI with custom codecs list and custom network settings - serverAPI, err := webrtc.NewServerAPI(network, address, &filters) + serverAPI, err = webrtc.NewServerAPI(network, address, &filters) if err != nil { log.Error().Err(err).Caller().Send() return } // use same API for WebRTC server and client if no address - clientAPI := serverAPI + clientAPI = serverAPI if address != "" { log.Info().Str("addr", cfg.Mod.Listen).Msg("[webrtc] listen") @@ -81,11 +83,13 @@ func Init() { streams.HandleFunc("webrtc", streamsHandler) } +var serverAPI, clientAPI *pion.API + var log zerolog.Logger var PeerConnection func(active bool) (*pion.PeerConnection, error) -func asyncHandler(tr *ws.Transport, msg *ws.Message) error { +func asyncHandler(tr *ws.Transport, msg *ws.Message) (err error) { var stream *streams.Stream var mode core.Mode @@ -104,8 +108,30 @@ func asyncHandler(tr *ws.Transport, msg *ws.Message) error { return errors.New(api.StreamNotFound) } + var offer struct { + Type string `json:"type"` + SDP string `json:"sdp"` + ICEServers []pion.ICEServer `json:"ice_servers"` + } + + // V2 - json/object exchange, V1 - raw SDP exchange + apiV2 := msg.Type == "webrtc" + + if apiV2 { + if err = msg.Unmarshal(&offer); err != nil { + return err + } + } else { + offer.SDP = msg.String() + } + // create new PeerConnection instance - pc, err := PeerConnection(false) + var pc *pion.PeerConnection + if offer.ICEServers == nil { + pc, err = PeerConnection(false) + } else { + pc, err = serverAPI.NewPeerConnection(pion.Configuration{ICEServers: offer.ICEServers}) + } if err != nil { log.Error().Err(err).Caller().Send() return err @@ -145,20 +171,10 @@ func asyncHandler(tr *ws.Transport, msg *ws.Message) error { } }) - // V2 - json/object exchange, V1 - raw SDP exchange - apiV2 := msg.Type == "webrtc" + log.Trace().Msgf("[webrtc] offer:\n%s", offer.SDP) // 1. SetOffer, so we can get remote client codecs - var offer string - if apiV2 { - offer = msg.GetString("sdp") - } else { - offer = msg.String() - } - - log.Trace().Msgf("[webrtc] offer:\n%s", offer) - - if err = conn.SetOffer(offer); err != nil { + if err = conn.SetOffer(offer.SDP); err != nil { log.Warn().Err(err).Caller().Send() return err } From b874c17bcb147b6639f63015a6535aeb5d8598c8 Mon Sep 17 00:00:00 2001 From: Alex X Date: Mon, 28 Oct 2024 22:47:26 +0300 Subject: [PATCH 069/166] Update dependencies --- go.mod | 33 ++++++------- go.sum | 117 +++++++++++++++++----------------------------- scripts/README.md | 1 + 3 files changed, 60 insertions(+), 91 deletions(-) diff --git a/go.mod b/go.mod index b038b110..ecd32f3a 100644 --- a/go.mod +++ b/go.mod @@ -5,44 +5,45 @@ go 1.20 require ( github.com/asticode/go-astits v1.13.0 github.com/expr-lang/expr v1.16.9 - github.com/gorilla/websocket v1.5.1 + github.com/gorilla/websocket v1.5.3 github.com/mattn/go-isatty v0.0.20 - github.com/miekg/dns v1.1.59 - github.com/pion/ice/v2 v2.3.24 - github.com/pion/interceptor v0.1.29 + github.com/miekg/dns v1.1.62 + github.com/pion/ice/v2 v2.3.36 + github.com/pion/interceptor v0.1.37 github.com/pion/rtcp v1.2.14 - github.com/pion/rtp v1.8.6 + github.com/pion/rtp v1.8.9 github.com/pion/sdp/v3 v3.0.9 - github.com/pion/srtp/v2 v2.0.18 + github.com/pion/srtp/v2 v2.0.20 github.com/pion/stun v0.6.1 - github.com/pion/webrtc/v3 v3.2.40 + github.com/pion/webrtc/v3 v3.3.4 github.com/rs/zerolog v1.33.0 github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1 github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f github.com/stretchr/testify v1.9.0 github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9 - golang.org/x/crypto v0.24.0 + golang.org/x/crypto v0.28.0 gopkg.in/yaml.v3 v3.0.1 ) require ( - github.com/asticode/go-astikit v0.30.0 // indirect + github.com/asticode/go-astikit v0.45.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/kr/pretty v0.2.1 // indirect + github.com/kr/pretty v0.3.1 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/pion/datachannel v1.5.6 // indirect - github.com/pion/dtls/v2 v2.2.11 // indirect + github.com/pion/datachannel v1.5.9 // indirect + github.com/pion/dtls/v2 v2.2.12 // indirect github.com/pion/logging v0.2.2 // indirect github.com/pion/mdns v0.0.12 // indirect github.com/pion/randutil v0.1.0 // indirect - github.com/pion/sctp v1.8.16 // indirect - github.com/pion/transport/v2 v2.2.5 // indirect + github.com/pion/sctp v1.8.33 // indirect + github.com/pion/transport/v2 v2.2.10 // indirect github.com/pion/turn/v2 v2.1.6 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/wlynxg/anet v0.0.5 // indirect golang.org/x/mod v0.18.0 // indirect - golang.org/x/net v0.26.0 // indirect + golang.org/x/net v0.27.0 // indirect golang.org/x/sync v0.7.0 // indirect - golang.org/x/sys v0.21.0 // indirect + golang.org/x/sys v0.26.0 // indirect golang.org/x/tools v0.22.0 // indirect ) diff --git a/go.sum b/go.sum index 727787ac..804ecc43 100644 --- a/go.sum +++ b/go.sum @@ -1,48 +1,45 @@ -github.com/asticode/go-astikit v0.30.0 h1:DkBkRQRIxYcknlaU7W7ksNfn4gMFsB0tqMJflxkRsZA= github.com/asticode/go-astikit v0.30.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0= +github.com/asticode/go-astikit v0.45.0 h1:08to/jrbod9tchF2bJ9moW+RTDK7DBUxLdIeSE7v7Sw= +github.com/asticode/go-astikit v0.45.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0= github.com/asticode/go-astits v1.13.0 h1:XOgkaadfZODnyZRR5Y0/DWkA9vrkLLPLeeOvDwfKZ1c= github.com/asticode/go-astits v1.13.0/go.mod h1:QSHmknZ51pf6KJdHKZHJTLlMegIrhega3LPWz3ND/iI= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 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/expr-lang/expr v1.16.5 h1:m2hvtguFeVaVNTHj8L7BoAyt7O0PAIBaSVbjdHgRXMs= -github.com/expr-lang/expr v1.16.5/go.mod h1:uCkhfG+x7fcZ5A5sXHKuQ07jGZRl6J0FCAaf2k4PtVQ= github.com/expr-lang/expr v1.16.9 h1:WUAzmR0JNI9JCiF0/ewwHB1gmcGw5wW7nWt8gc6PpCI= github.com/expr-lang/expr v1.16.9/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 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/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= -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= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 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/miekg/dns v1.1.59 h1:C9EXc/UToRwKLhK5wKU/I4QVsBUc8kE6MkHBkeypWZs= -github.com/miekg/dns v1.1.59/go.mod h1:nZpewl5p6IvctfgrckopVx2OlSEHPRO/U4SYkRklrEk= -github.com/pion/datachannel v1.5.6 h1:1IxKJntfSlYkpUj8LlYRSWpYiTTC02nUrOE8T3DqGeg= -github.com/pion/datachannel v1.5.6/go.mod h1:1eKT6Q85pRnr2mHiWHxJwO50SfZRtWHTsNIVb/NfGW4= +github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ= +github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ= +github.com/pion/datachannel v1.5.9 h1:LpIWAOYPyDrXtU+BW7X0Yt/vGtYxtXQ8ql7dFfYUVZA= +github.com/pion/datachannel v1.5.9/go.mod h1:kDUuk4CU4Uxp82NH4LQZbISULkX/HtzKa4P7ldf9izE= github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= -github.com/pion/dtls/v2 v2.2.10 h1:u2Axk+FyIR1VFTPurktB+1zoEPGIW3bmyj3LEFrXjAA= -github.com/pion/dtls/v2 v2.2.10/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE= -github.com/pion/dtls/v2 v2.2.11 h1:9U/dpCYl1ySttROPWJgqWKEylUdT0fXp/xst6JwY5Ks= -github.com/pion/dtls/v2 v2.2.11/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE= -github.com/pion/ice/v2 v2.3.19 h1:1GoMRTMnB6bCP4aGy2MjxK3w4laDkk+m7svJb/eqybc= -github.com/pion/ice/v2 v2.3.19/go.mod h1:KXJJcZK7E8WzrBEYnV4UtqEZsGeWfHxsNqhVcVvgjxw= -github.com/pion/ice/v2 v2.3.24 h1:RYgzhH/u5lH0XO+ABatVKCtRd+4U1GEaCXSMjNr13tI= -github.com/pion/ice/v2 v2.3.24/go.mod h1:KXJJcZK7E8WzrBEYnV4UtqEZsGeWfHxsNqhVcVvgjxw= -github.com/pion/interceptor v0.1.29 h1:39fsnlP1U8gw2JzOFWdfCU82vHvhW9o0rZnZF56wF+M= -github.com/pion/interceptor v0.1.29/go.mod h1:ri+LGNjRUc5xUNtDEPzfdkmSqISixVTBF/z/Zms/6T4= +github.com/pion/dtls/v2 v2.2.12 h1:KP7H5/c1EiVAAKUmXyCzPiQe5+bCJrpOeKg/L05dunk= +github.com/pion/dtls/v2 v2.2.12/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE= +github.com/pion/ice/v2 v2.3.36 h1:SopeXiVbbcooUg2EIR8sq4b13RQ8gzrkkldOVg+bBsc= +github.com/pion/ice/v2 v2.3.36/go.mod h1:mBF7lnigdqgtB+YHkaY/Y6s6tsyRyo4u4rPGRuOjUBQ= +github.com/pion/interceptor v0.1.37 h1:aRA8Zpab/wE7/c0O3fh1PqY0AJI3fCSEM5lRWJVorwI= +github.com/pion/interceptor v0.1.37/go.mod h1:JzxbJ4umVTlZAf+/utHzNesY8tmRkM2lVmkS82TTj8Y= 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.12 h1:CiMYlY+O0azojWDmxdNr7ADGrnZ+V6Ilfner+6mSVK8= @@ -53,40 +50,36 @@ github.com/pion/rtcp v1.2.12/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9 github.com/pion/rtcp v1.2.14 h1:KCkGV3vJ+4DAJmvP0vaQShsb0xkRfWkO540Gy102KyE= github.com/pion/rtcp v1.2.14/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4= github.com/pion/rtp v1.8.3/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU= -github.com/pion/rtp v1.8.6 h1:MTmn/b0aWWsAzux2AmP8WGllusBVw4NPYPVFFd7jUPw= -github.com/pion/rtp v1.8.6/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU= -github.com/pion/sctp v1.8.13/go.mod h1:YKSgO/bO/6aOMP9LCie1DuD7m+GamiK2yIiPM6vH+GA= -github.com/pion/sctp v1.8.16 h1:PKrMs+o9EMLRvFfXq59WFsC+V8mN1wnKzqrv+3D/gYY= -github.com/pion/sctp v1.8.16/go.mod h1:P6PbDVA++OJMrVNg2AL3XtYHV4uD6dvfyOovCgMs0PE= +github.com/pion/rtp v1.8.9 h1:E2HX740TZKaqdcPmf4pw6ZZuG8u5RlMMt+l3dxeu6Wk= +github.com/pion/rtp v1.8.9/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU= +github.com/pion/sctp v1.8.33 h1:dSE4wX6uTJBcNm8+YlMg7lw1wqyKHggsP5uKbdj+NZw= +github.com/pion/sctp v1.8.33/go.mod h1:beTnqSzewI53KWoG3nqB282oDMGrhNxBdb+JZnkCwRM= github.com/pion/sdp/v3 v3.0.9 h1:pX++dCHoHUwq43kuwf3PyJfHlwIj4hXA7Vrifiq0IJY= github.com/pion/sdp/v3 v3.0.9/go.mod h1:B5xmvENq5IXJimIO4zfp6LAe1fD9N+kFv+V/1lOdz8M= -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/srtp/v2 v2.0.20 h1:HNNny4s+OUmG280ETrCdgFndp4ufx3/uy85EawYEhTk= +github.com/pion/srtp/v2 v2.0.20/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/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/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0= -github.com/pion/transport/v2 v2.2.5 h1:iyi25i/21gQck4hfRhomF6SktmUQjRsRW4WJdhfc3Kc= -github.com/pion/transport/v2 v2.2.5/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0= +github.com/pion/transport/v2 v2.2.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQpw6Q= +github.com/pion/transport/v2 v2.2.10/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E= github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0= -github.com/pion/transport/v3 v3.0.2 h1:r+40RJR25S9w3jbA6/5uEPTzcdn7ncyU44RWCbHkLg4= -github.com/pion/transport/v3 v3.0.2/go.mod h1:nIToODoOlb5If2jF9y2Igfx3PFYWfuXi37m0IlWa/D0= +github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0= github.com/pion/turn/v2 v2.1.3/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY= github.com/pion/turn/v2 v2.1.6 h1:Xr2niVsiPTB0FPtt+yAWKFUkU1eotQbGgpTIld4x1Gc= github.com/pion/turn/v2 v2.1.6/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY= -github.com/pion/webrtc/v3 v3.2.39 h1:Lf2SIMGdE3M9VNm48KpoX5pR8SJ6TsMnktzOkc/oB0o= -github.com/pion/webrtc/v3 v3.2.39/go.mod h1:AQ8p56OLbm3MjhYovYdgPuyX6oc+JcKx/HFoCGFcYzA= -github.com/pion/webrtc/v3 v3.2.40 h1:Wtfi6AZMQg+624cvCXUuSmrKWepSB7zfgYDOYqsSOVU= -github.com/pion/webrtc/v3 v3.2.40/go.mod h1:M1RAe3TNTD1tzyvqHrbVODfwdPGSXOUo/OgpoGGJqFY= +github.com/pion/webrtc/v3 v3.3.4 h1:v2heQVnXTSqNRXcaFQVOhIOYkLMxOu1iJG8uy1djvkk= +github.com/pion/webrtc/v3 v3.3.4/go.mod h1:liNa+E1iwyzyXqNUwvoMRNQ10x8h8FOeJKL8RkIbamE= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/profile v1.4.0/go.mod h1:NWz/XGvpEW1FyYQ7fCx4dqYBLlfTcE+A9FLAkNKqjFE= 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/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.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= -github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0= -github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1 h1:NVK+OqnavpyFmUiKfUMHrpvbCi2VFoWTrcpI7aDaJ2I= @@ -106,25 +99,19 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9 h1:aeN+ghOV0b2VCmKKO3gqnDQ8mLbpABZgRR2FVYx4ouI= github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9/go.mod h1:roo6cZ/uqpwKMuvPG0YmzI5+AmUiMWfjCBZpGXqbTxE= +github.com/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= +github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= +github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 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.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.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= -golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= -golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= -golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= -golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= -golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= -golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= -golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= 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/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= -golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -133,17 +120,10 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug 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.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= -golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= -golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= -golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= -golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= -golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= -golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= -golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= +golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= golang.org/x/sync v0.0.0-20190423024810-112230192c58/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= @@ -160,42 +140,29 @@ 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/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= -golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.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.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.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.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= -golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= 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.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 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.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.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.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY= -golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg= golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/scripts/README.md b/scripts/README.md index eeb8c25d..36f667b2 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -40,6 +40,7 @@ go list -deps .\cmd\go2rtc_rtsp\ - github.com/sigurn/crc8 - github.com/pion/ice/v2 - github.com/google/uuid + - github.com/wlynxg/anet - github.com/rs/zerolog - github.com/mattn/go-colorable - github.com/mattn/go-isatty From 780f378fb16e4b60cba764bc2c3a4b11ada3b385 Mon Sep 17 00:00:00 2001 From: Alex X Date: Mon, 28 Oct 2024 22:47:55 +0300 Subject: [PATCH 070/166] Update version to 1.9.5 --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index 98bd79e3..ee9e05f4 100644 --- a/main.go +++ b/main.go @@ -36,7 +36,7 @@ import ( ) func main() { - app.Version = "1.9.4" + app.Version = "1.9.5" // 1. Core modules: app, api/ws, streams From 3f94a754e4a9fd3be05ca80127e196867a63d1b1 Mon Sep 17 00:00:00 2001 From: Alex X Date: Tue, 29 Oct 2024 14:39:37 +0300 Subject: [PATCH 071/166] Fix WebRTC card stuck in loading #1417 --- internal/streams/streams.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/internal/streams/streams.go b/internal/streams/streams.go index ae50e6c9..b1038423 100644 --- a/internal/streams/streams.go +++ b/internal/streams/streams.go @@ -100,6 +100,10 @@ func Patch(name string, source string) *Stream { return nil } + if Validate(source) != nil { + return nil + } + // check an existing stream with this name if stream, ok := streams[name]; ok { stream.SetSource(source) @@ -107,7 +111,9 @@ func Patch(name string, source string) *Stream { } // create new stream with this name - return New(name, source) + stream := NewStream(source) + streams[name] = stream + return stream } func GetOrPatch(query url.Values) *Stream { From be5bbd3b9be13d26fd3e6224a59085cb3f9d3d1c Mon Sep 17 00:00:00 2001 From: Alex X Date: Tue, 29 Oct 2024 14:39:54 +0300 Subject: [PATCH 072/166] Fix FFmpeg tests --- internal/ffmpeg/ffmpeg_test.go | 206 +++++++++++++++++++++------------ 1 file changed, 132 insertions(+), 74 deletions(-) diff --git a/internal/ffmpeg/ffmpeg_test.go b/internal/ffmpeg/ffmpeg_test.go index 3fc5d208..2ab1170d 100644 --- a/internal/ffmpeg/ffmpeg_test.go +++ b/internal/ffmpeg/ffmpeg_test.go @@ -31,7 +31,7 @@ func TestParseArgsFile(t *testing.T) { { name: "[FILE] video will be transcoded to H265 and rotate 270º, audio will be skipped", source: "/media/bbb.mp4#video=h265#rotate=-90", - expect: `ffmpeg -hide_banner -re -i /media/bbb.mp4 -c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency -an -vf "transpose=2" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, + expect: `ffmpeg -hide_banner -re -i /media/bbb.mp4 -c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p -an -vf "transpose=2" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, }, { name: "[FILE] video will be output for MJPEG to pipe, audio will be skipped", @@ -53,85 +53,143 @@ func TestParseArgsFile(t *testing.T) { } func TestParseArgsDevice(t *testing.T) { - // [DEVICE] video will be output for MJPEG to pipe, with size 1920x1080 - args := parseArgs("device?video=0&video_size=1920x1080") - require.Equal(t, `ffmpeg -hide_banner -f dshow -video_size 1920x1080 -i "video=0" -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String()) - - // [DEVICE] video will be transcoded to H265 with framerate 20, audio will be skipped - //args = parseArgs("device?video=0&video_size=1280x720&framerate=20#video=h265#audio=pcma") - args = parseArgs("device?video=0&framerate=20#video=h265") - require.Equal(t, `ffmpeg -hide_banner -f dshow -framerate 20 -i "video=0" -c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String()) - - args = parseArgs("device?video=FaceTime HD Camera&audio=Microphone (High Definition Audio Device)") - require.Equal(t, `ffmpeg -hide_banner -f dshow -i "video=FaceTime HD Camera:audio=Microphone (High Definition Audio Device)" -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String()) + tests := []struct { + name string + source string + expect string + }{ + { + name: "[DEVICE] video will be output for MJPEG to pipe, with size 1920x1080", + source: "device?video=0&video_size=1920x1080", + expect: `ffmpeg -hide_banner -f dshow -video_size 1920x1080 -i "video=0" -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, + }, + { + name: "[DEVICE] video will be transcoded to H265 with framerate 20, audio will be skipped", + source: "device?video=0&framerate=20#video=h265", + expect: `ffmpeg -hide_banner -f dshow -framerate 20 -i "video=0" -c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, + }, + { + name: "[DEVICE] video/audio", + source: "device?video=FaceTime HD Camera&audio=Microphone (High Definition Audio Device)", + expect: `ffmpeg -hide_banner -f dshow -i "video=FaceTime HD Camera:audio=Microphone (High Definition Audio Device)" -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + args := parseArgs(test.source) + require.Equal(t, test.expect, args.String()) + }) + } } func TestParseArgsIpCam(t *testing.T) { - // [HTTP] video will be copied - args := parseArgs("http://example.com") - require.Equal(t, `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http://example.com -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String()) - - // [HTTP-MJPEG] video will be transcoded to H264 - args = parseArgs("http://example.com#video=h264") - require.Equal(t, `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http://example.com -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String()) - - // [HLS] video will be copied, audio will be skipped - args = parseArgs("https://example.com#video=copy") - require.Equal(t, `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i https://example.com -c:v copy -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String()) - - // [RTSP] video will be copied without transcoding codecs - args = parseArgs("rtsp://example.com") - require.Equal(t, `ffmpeg -hide_banner -allowed_media_types video+audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String()) - - // [RTSP] video with resize to 1280x720, should be transcoded, so select H265 - args = parseArgs("rtsp://example.com#video=h265#width=1280#height=720") - require.Equal(t, `ffmpeg -hide_banner -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency -an -vf "scale=1280:720" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String()) - - // [RTSP] video will be copied, changing RTSP transport from TCP to UDP+TCP - args = parseArgs("rtsp://example.com#input=rtsp/udp") - require.Equal(t, `ffmpeg -hide_banner -allowed_media_types video+audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -i rtsp://example.com -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String()) - - // [RTMP] video will be copied, changing RTSP transport from TCP to UDP+TCP - args = parseArgs("rtmp://example.com#input=rtsp/udp") - require.Equal(t, `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -i rtmp://example.com -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String()) + tests := []struct { + name string + source string + expect string + }{ + { + name: "[HTTP] video will be copied", + source: "http://example.com", + expect: `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http://example.com -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, + }, + { + name: "[HTTP-MJPEG] video will be transcoded to H264", + source: "http://example.com#video=h264", + expect: `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http://example.com -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, + }, + { + name: "[HLS] video will be copied, audio will be skipped", + source: "https://example.com#video=copy", + expect: `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i https://example.com -c:v copy -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, + }, + { + name: "[RTSP] video will be copied without transcoding codecs", + source: "rtsp://example.com", + expect: `ffmpeg -hide_banner -allowed_media_types video+audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, + }, + { + name: "[RTSP] video with resize to 1280x720, should be transcoded, so select H265", + source: "rtsp://example.com#video=h265#width=1280#height=720", + expect: `ffmpeg -hide_banner -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p -an -vf "scale=1280:720" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, + }, + { + name: "[RTSP] video will be copied, changing RTSP transport from TCP to UDP+TCP", + source: "rtsp://example.com#input=rtsp/udp", + expect: `ffmpeg -hide_banner -allowed_media_types video+audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -i rtsp://example.com -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, + }, + { + name: "[RTMP] video will be copied, changing RTSP transport from TCP to UDP+TCP", + source: "rtmp://example.com#input=rtsp/udp", + expect: `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -i rtmp://example.com -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + args := parseArgs(test.source) + require.Equal(t, test.expect, args.String()) + }) + } } func TestParseArgsAudio(t *testing.T) { - // [AUDIO] audio will be transcoded to AAC, video will be skipped - args := parseArgs("rtsp:///example.com#audio=aac") - require.Equal(t, `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp:///example.com -c:a aac -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String()) - - // [AUDIO] audio will be transcoded to AAC/16000, video will be skipped - args = parseArgs("rtsp:///example.com#audio=aac/16000") - require.Equal(t, `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp:///example.com -c:a aac -ar:a 16000 -ac:a 1 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String()) - - // [AUDIO] audio will be transcoded to OPUS, video will be skipped - args = parseArgs("rtsp:///example.com#audio=opus") - require.Equal(t, `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp:///example.com -c:a libopus -application:a lowdelay -min_comp 0 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String()) - - // [AUDIO] audio will be transcoded to PCMU, video will be skipped - args = parseArgs("rtsp:///example.com#audio=pcmu") - require.Equal(t, `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp:///example.com -c:a pcm_mulaw -ar:a 8000 -ac:a 1 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String()) - - // [AUDIO] audio will be transcoded to PCMU/16000, video will be skipped - args = parseArgs("rtsp:///example.com#audio=pcmu/16000") - require.Equal(t, `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp:///example.com -c:a pcm_mulaw -ar:a 16000 -ac:a 1 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String()) - - // [AUDIO] audio will be transcoded to PCMU/48000, video will be skipped - args = parseArgs("rtsp:///example.com#audio=pcmu/48000") - require.Equal(t, `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp:///example.com -c:a pcm_mulaw -ar:a 48000 -ac:a 1 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String()) - - // [AUDIO] audio will be transcoded to PCMA, video will be skipped - args = parseArgs("rtsp:///example.com#audio=pcma") - require.Equal(t, `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp:///example.com -c:a pcm_alaw -ar:a 8000 -ac:a 1 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String()) - - // [AUDIO] audio will be transcoded to PCMA/16000, video will be skipped - args = parseArgs("rtsp:///example.com#audio=pcma/16000") - require.Equal(t, `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp:///example.com -c:a pcm_alaw -ar:a 16000 -ac:a 1 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String()) - - // [AUDIO] audio will be transcoded to PCMA/48000, video will be skipped - args = parseArgs("rtsp:///example.com#audio=pcma/48000") - require.Equal(t, `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp:///example.com -c:a pcm_alaw -ar:a 48000 -ac:a 1 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String()) + tests := []struct { + name string + source string + expect string + }{ + { + name: "[AUDIO] audio will be transcoded to AAC, video will be skipped", + source: "rtsp://example.com#audio=aac", + expect: `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:a aac -vn -f adts -`, + }, + { + name: "[AUDIO] audio will be transcoded to AAC/16000, video will be skipped", + source: "rtsp://example.com#audio=aac/16000", + expect: `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:a aac -ar:a 16000 -ac:a 1 -vn -f adts -`, + }, + { + name: "[AUDIO] audio will be transcoded to OPUS, video will be skipped", + source: "rtsp://example.com#audio=opus", + expect: `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:a libopus -application:a lowdelay -min_comp 0 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, + }, + { + name: "[AUDIO] audio will be transcoded to PCMU, video will be skipped", + source: "rtsp://example.com#audio=pcmu", + expect: `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:a pcm_mulaw -ar:a 8000 -ac:a 1 -vn -f wav -`, + }, + { + name: "[AUDIO] audio will be transcoded to PCMU/16000, video will be skipped", + source: "rtsp://example.com#audio=pcmu/16000", + expect: `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:a pcm_mulaw -ar:a 16000 -ac:a 1 -vn -f wav -`, + }, + { + name: "[AUDIO] audio will be transcoded to PCMU/48000, video will be skipped", + source: "rtsp://example.com#audio=pcmu/48000", + expect: `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:a pcm_mulaw -ar:a 48000 -ac:a 1 -vn -f wav -`, + }, + { + name: "[AUDIO] audio will be transcoded to PCMA, video will be skipped", + source: "rtsp://example.com#audio=pcma", + expect: `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:a pcm_alaw -ar:a 8000 -ac:a 1 -vn -f wav -`, + }, + { + name: "[AUDIO] audio will be transcoded to PCMA/16000, video will be skipped", + source: "rtsp://example.com#audio=pcma/16000", + expect: `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:a pcm_alaw -ar:a 16000 -ac:a 1 -vn -f wav -`, + }, + { + name: "[AUDIO] audio will be transcoded to PCMA/48000, video will be skipped", + source: "rtsp://example.com#audio=pcma/48000", + expect: `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:a pcm_alaw -ar:a 48000 -ac:a 1 -vn -f wav -`, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + args := parseArgs(test.source) + require.Equal(t, test.expect, args.String()) + }) + } } func TestParseArgsHwVaapi(t *testing.T) { From 8cca8decdefdb839040ca83143f8286ae3743ecb Mon Sep 17 00:00:00 2001 From: Alex X Date: Tue, 29 Oct 2024 17:50:00 +0300 Subject: [PATCH 073/166] Update version to 1.9.6 --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index ee9e05f4..df3468f4 100644 --- a/main.go +++ b/main.go @@ -36,7 +36,7 @@ import ( ) func main() { - app.Version = "1.9.5" + app.Version = "1.9.6" // 1. Core modules: app, api/ws, streams From 3f5f1328e752d1341ea0ebb736b9981e7f31d7df Mon Sep 17 00:00:00 2001 From: Alex X Date: Thu, 31 Oct 2024 20:09:11 +0300 Subject: [PATCH 074/166] Fix webrtc:ws source after 1.9.5 #1425 --- internal/api/ws/ws.go | 20 +++++++++--------- internal/webrtc/webrtc_test.go | 38 ++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 10 deletions(-) create mode 100644 internal/webrtc/webrtc_test.go diff --git a/internal/api/ws/ws.go b/internal/api/ws/ws.go index 0eb45aa7..1d945bfe 100644 --- a/internal/api/ws/ws.go +++ b/internal/api/ws/ws.go @@ -37,16 +37,21 @@ var log zerolog.Logger type Message struct { Type string `json:"type"` Value any `json:"value,omitempty"` - Raw []byte `json:"-"` } func (m *Message) String() (value string) { - _ = json.Unmarshal(m.Raw, &value) + if s, ok := m.Value.(string); ok { + return s + } return } func (m *Message) Unmarshal(v any) error { - return json.Unmarshal(m.Raw, v) + b, err := json.Marshal(m.Value) + if err != nil { + return err + } + return json.Unmarshal(b, v) } type WSHandler func(tr *Transport, msg *Message) error @@ -113,11 +118,8 @@ func apiWS(w http.ResponseWriter, r *http.Request) { }) for { - var raw struct { - Type string `json:"type"` - Value json.RawMessage `json:"value"` - } - if err = ws.ReadJSON(&raw); err != nil { + msg := new(Message) + if err = ws.ReadJSON(msg); err != nil { if !websocket.IsCloseError(err, websocket.CloseNoStatusReceived) { log.Trace().Err(err).Caller().Send() } @@ -125,8 +127,6 @@ func apiWS(w http.ResponseWriter, r *http.Request) { break } - msg := &Message{Type: raw.Type, Raw: raw.Value} - log.Trace().Str("type", msg.Type).Msg("[api] ws msg") if handler := wsHandlers[msg.Type]; handler != nil { diff --git a/internal/webrtc/webrtc_test.go b/internal/webrtc/webrtc_test.go new file mode 100644 index 00000000..e014c31c --- /dev/null +++ b/internal/webrtc/webrtc_test.go @@ -0,0 +1,38 @@ +package webrtc + +import ( + "encoding/json" + "testing" + + "github.com/AlexxIT/go2rtc/internal/api/ws" + pion "github.com/pion/webrtc/v3" + "github.com/stretchr/testify/require" +) + +func TestWebRTCAPIv1(t *testing.T) { + raw := `{"type":"webrtc/offer","value":"v=0\n..."}` + msg := new(ws.Message) + err := json.Unmarshal([]byte(raw), msg) + require.Nil(t, err) + + require.Equal(t, "v=0\n...", msg.String()) +} + +func TestWebRTCAPIv2(t *testing.T) { + raw := `{"type":"webrtc","value":{"type":"offer","sdp":"v=0\n...","ice_servers":[{"urls":["stun:stun.l.google.com:19302"]}]}}` + msg := new(ws.Message) + err := json.Unmarshal([]byte(raw), msg) + require.Nil(t, err) + + var offer struct { + Type string `json:"type"` + SDP string `json:"sdp"` + ICEServers []pion.ICEServer `json:"ice_servers"` + } + err = msg.Unmarshal(&offer) + require.Nil(t, err) + + require.Equal(t, "offer", offer.Type) + require.Equal(t, "v=0\n...", offer.SDP) + require.Equal(t, "stun:stun.l.google.com:19302", offer.ICEServers[0].URLs[0]) +} From 1d1bcb0a63b4b319313d573d9fafa07fdb6e233d Mon Sep 17 00:00:00 2001 From: Alex X Date: Fri, 1 Nov 2024 12:08:06 +0300 Subject: [PATCH 075/166] Code refactoring for UnmarshalSDP --- pkg/rtsp/helpers.go | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/pkg/rtsp/helpers.go b/pkg/rtsp/helpers.go index 1a687c01..2d619364 100644 --- a/pkg/rtsp/helpers.go +++ b/pkg/rtsp/helpers.go @@ -28,8 +28,7 @@ func UnmarshalSDP(rawSDP []byte) ([]*core.Media, error) { sd := &sdp.SessionDescription{} if err := sd.Unmarshal(rawSDP); err != nil { // fix multiple `s=` https://github.com/AlexxIT/WebRTC/issues/417 - re, _ := regexp.Compile("\ns=[^\n]+") - rawSDP = re.ReplaceAll(rawSDP, nil) + rawSDP = regexp.MustCompile("\ns=[^\n]+").ReplaceAll(rawSDP, nil) // fix SDP header for some cameras if i := bytes.Index(rawSDP, []byte("\nm=")); i > 0 { @@ -38,12 +37,11 @@ func UnmarshalSDP(rawSDP []byte) ([]*core.Media, error) { // Fix invalid media type (errSDPInvalidValue) caused by // some TP-LINK IP camera, e.g. TL-IPC44GW - m := regexp.MustCompile("m=[^ ]+ ") - for _, i := range m.FindAll(rawSDP, -1) { - switch string(i[2 : len(i)-1]) { + for _, b := range regexp.MustCompile("m=[^ ]+ ").FindAll(rawSDP, -1) { + switch string(b[2 : len(b)-1]) { case "audio", "video", "application": default: - rawSDP = bytes.Replace(rawSDP, i, []byte("m=application "), 1) + rawSDP = bytes.Replace(rawSDP, b, []byte("m=application "), 1) } } From 6b005a666e1997643073423af292d9c2adf49d66 Mon Sep 17 00:00:00 2001 From: Alex X Date: Fri, 1 Nov 2024 12:09:44 +0300 Subject: [PATCH 076/166] Fix yet another broken SDP from CN cameras #1426 --- pkg/rtsp/helpers.go | 3 +++ pkg/rtsp/rtsp_test.go | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/pkg/rtsp/helpers.go b/pkg/rtsp/helpers.go index 2d619364..6b07342d 100644 --- a/pkg/rtsp/helpers.go +++ b/pkg/rtsp/helpers.go @@ -30,6 +30,9 @@ func UnmarshalSDP(rawSDP []byte) ([]*core.Media, error) { // fix multiple `s=` https://github.com/AlexxIT/WebRTC/issues/417 rawSDP = regexp.MustCompile("\ns=[^\n]+").ReplaceAll(rawSDP, nil) + // fix broken `c=` https://github.com/AlexxIT/go2rtc/issues/1426 + rawSDP = regexp.MustCompile("\nc=[^\n]+").ReplaceAll(rawSDP, nil) + // fix SDP header for some cameras if i := bytes.Index(rawSDP, []byte("\nm=")); i > 0 { rawSDP = append([]byte(sdpHeader), rawSDP[i:]...) diff --git a/pkg/rtsp/rtsp_test.go b/pkg/rtsp/rtsp_test.go index 43248ba6..14c99803 100644 --- a/pkg/rtsp/rtsp_test.go +++ b/pkg/rtsp/rtsp_test.go @@ -203,6 +203,40 @@ a=control:track5 assert.Len(t, medias, 4) } +func TestBugSDP7(t *testing.T) { + // https://github.com/AlexxIT/go2rtc/issues/1426 + s := `v=0 +o=- 1001 1 IN +s=VCP IPC Realtime stream +m=video 0 RTP/AVP 105 +c=IN +a=control:rtsp://1.0.1.113/media/video2/video +a=rtpmap:105 H264/90000 +a=fmtp:105 profile-level-id=640016; packetization-mode=1; sprop-parameter-sets=Z2QAFqw7UFAX/LCAAAH0AABOIEI=,aOqPLA== +a=recvonly +m=audio 0 RTP/AVP 0 +c=IN +a=fmtp:0 RTCP=0 +a=control:rtsp://1.0.1.113/media/video2/audio1 +a=recvonly +m=audio 0 RTP/AVP 0 +c=IN +a=control:rtsp://1.0.1.113/media/video2/backchannel +a=rtpmap:0 PCMA/8000 +a=rtpmap:0 PCMU/8000 +a=sendonly +m=application 0 RTP/AVP 107 +c=IN +a=control:rtsp://1.0.1.113/media/video2/metadata +a=rtpmap:107 vnd.onvif.metadata/90000 +a=fmtp:107 DecoderTag=h3c-v3 RTCP=0 +a=recvonly +` + medias, err := UnmarshalSDP([]byte(s)) + assert.Nil(t, err) + assert.Len(t, medias, 4) +} + func TestHikvisionPCM(t *testing.T) { s := `v=0 o=- 1721969533379665 1721969533379665 IN IP4 192.168.1.12 From 2c34a17d88915399f9fa9ec5191cdd4d0d5b6273 Mon Sep 17 00:00:00 2001 From: Alex X Date: Sat, 2 Nov 2024 20:50:33 +0300 Subject: [PATCH 077/166] Fix stop for webrtc stream #1428 --- pkg/webrtc/conn.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/webrtc/conn.go b/pkg/webrtc/conn.go index 3e3ecc4f..5bc16ede 100644 --- a/pkg/webrtc/conn.go +++ b/pkg/webrtc/conn.go @@ -29,6 +29,7 @@ func NewConn(pc *webrtc.PeerConnection) *Conn { Connection: core.Connection{ ID: core.NewID(), FormatName: "webrtc", + Transport: pc, }, pc: pc, } From f13aa21d0f83204f23050981f9d390523c356977 Mon Sep 17 00:00:00 2001 From: seydx Date: Sun, 3 Nov 2024 16:33:08 +0100 Subject: [PATCH 078/166] Add backchannel support for rtsp server --- internal/rtsp/rtsp.go | 18 ++++++++++++++++++ pkg/core/media.go | 4 ++++ pkg/rtsp/producer.go | 44 +++++++++++++++++++++++++++++-------------- pkg/rtsp/server.go | 29 +++++++++++++++++++++++----- 4 files changed, 76 insertions(+), 19 deletions(-) diff --git a/internal/rtsp/rtsp.go b/internal/rtsp/rtsp.go index 230bdece..377061e5 100644 --- a/internal/rtsp/rtsp.go +++ b/internal/rtsp/rtsp.go @@ -8,6 +8,7 @@ import ( "github.com/AlexxIT/go2rtc/internal/app" "github.com/AlexxIT/go2rtc/internal/streams" + "github.com/AlexxIT/go2rtc/pkg/aac" "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/rtsp" "github.com/AlexxIT/go2rtc/pkg/tcp" @@ -184,6 +185,23 @@ func tcpHandler(conn *rtsp.Conn) { } } + if query.Get("backchannel") == "1" { + conn.Medias = append(conn.Medias, &core.Media{ + Kind: core.KindAudio, + Direction: core.DirectionRecvonly, + Codecs: []*core.Codec{ + {Name: core.CodecOpus, ClockRate: 48000, Channels: 2}, + {Name: core.CodecPCM, ClockRate: 16000}, + {Name: core.CodecPCMA, ClockRate: 16000}, + {Name: core.CodecPCMU, ClockRate: 16000}, + {Name: core.CodecPCM, ClockRate: 8000}, + {Name: core.CodecPCMA, ClockRate: 8000}, + {Name: core.CodecPCMU, ClockRate: 8000}, + {Name: core.CodecAAC, ClockRate: 16000, FmtpLine: aac.FMTP + "1408"}, + }, + }) + } + if s := query.Get("pkt_size"); s != "" { conn.PacketSize = uint16(core.Atoi(s)) } diff --git a/pkg/core/media.go b/pkg/core/media.go index 72ab58c6..a700bb62 100644 --- a/pkg/core/media.go +++ b/pkg/core/media.go @@ -141,6 +141,10 @@ func MarshalSDP(name string, medias []*Media) ([]byte, error) { } md.WithCodec(codec.PayloadType, name, codec.ClockRate, codec.Channels, codec.FmtpLine) + if media.Direction != "" { + md.WithPropertyAttribute(media.Direction) + } + if media.ID != "" { md.WithValueAttribute("control", media.ID) } diff --git a/pkg/rtsp/producer.go b/pkg/rtsp/producer.go index de115808..323d9197 100644 --- a/pkg/rtsp/producer.go +++ b/pkg/rtsp/producer.go @@ -16,27 +16,43 @@ func (c *Conn) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, e } } - c.stateMu.Lock() - defer c.stateMu.Unlock() + switch c.mode { + case core.ModeActiveProducer: + c.stateMu.Lock() + defer c.stateMu.Unlock() - if c.state == StatePlay { - if err := c.Reconnect(); err != nil { + if c.state == StatePlay { + if err := c.Reconnect(); err != nil { + return nil, err + } + } + + channel, err := c.SetupMedia(media) + if err != nil { return nil, err } - } - channel, err := c.SetupMedia(media) - if err != nil { - return nil, err - } + c.state = StateSetup - c.state = StateSetup + track := core.NewReceiver(media, codec) + track.ID = channel + c.Receivers = append(c.Receivers, track) - track := core.NewReceiver(media, codec) - track.ID = channel - c.Receivers = append(c.Receivers, track) + return track, nil + case core.ModePassiveConsumer: + // Backchannel + c.stateMu.Lock() + defer c.stateMu.Unlock() - return track, nil + channel := byte(len(c.Senders)) * 2 + track := core.NewReceiver(media, codec) + track.ID = channel + c.Receivers = append(c.Receivers, track) + + return track, nil + default: + return nil, errors.New("rtsp: wrong mode for GetTrack") + } } func (c *Conn) Start() (err error) { diff --git a/pkg/rtsp/server.go b/pkg/rtsp/server.go index 7953b0dc..1cddbec5 100644 --- a/pkg/rtsp/server.go +++ b/pkg/rtsp/server.go @@ -129,6 +129,16 @@ func (c *Conn) Accept() error { medias = append(medias, media) } + for i, track := range c.Receivers { + media := &core.Media{ + Kind: core.GetKind(track.Codec.Name), + Direction: core.DirectionSendonly, + Codecs: []*core.Codec{track.Codec}, + ID: "trackID=" + strconv.Itoa(i+len(c.Senders)), + } + medias = append(medias, media) + } + res.Body, err = core.MarshalSDP(c.SessionName, medias) if err != nil { return err @@ -154,11 +164,20 @@ func (c *Conn) Accept() error { c.state = StateSetup if c.mode == core.ModePassiveConsumer { - if i := reqTrackID(req); i >= 0 && i < len(c.Senders) { - // mark sender as SETUP - c.Senders[i].Media.ID = MethodSetup - tr = fmt.Sprintf("RTP/AVP/TCP;unicast;interleaved=%d-%d", i*2, i*2+1) - res.Header.Set("Transport", tr) + trackID := reqTrackID(req) + + if trackID >= 0 { + if trackID < len(c.Senders) { + c.Senders[trackID].Media.ID = MethodSetup + tr = fmt.Sprintf("RTP/AVP/TCP;unicast;interleaved=%d-%d", trackID*2, trackID*2+1) + res.Header.Set("Transport", tr) + } else if trackID >= len(c.Senders) && trackID < len(c.Senders)+len(c.Receivers) { + c.Receivers[trackID-len(c.Senders)].Media.ID = MethodSetup + tr = fmt.Sprintf("RTP/AVP/TCP;unicast;interleaved=%d-%d", trackID*2, trackID*2+1) + res.Header.Set("Transport", tr) + } else { + res.Status = "400 Bad Request" + } } else { res.Status = "400 Bad Request" } From 340fd81778ce2e2d71c6707b733b76e5e412828b Mon Sep 17 00:00:00 2001 From: Alex X Date: Sat, 9 Nov 2024 18:17:41 +0300 Subject: [PATCH 079/166] Fix loop request, ex. `camera1: ffmpeg:camera1` --- internal/ffmpeg/ffmpeg.go | 2 ++ internal/rtsp/rtsp.go | 3 +++ internal/streams/add_consumer.go | 6 ++++++ pkg/core/connection.go | 5 +++++ 4 files changed, 16 insertions(+) diff --git a/internal/ffmpeg/ffmpeg.go b/internal/ffmpeg/ffmpeg.go index 12a9be83..b934be53 100644 --- a/internal/ffmpeg/ffmpeg.go +++ b/internal/ffmpeg/ffmpeg.go @@ -179,6 +179,7 @@ func parseArgs(s string) *ffmpeg.Args { Version: verAV, } + var source = s var query url.Values if i := strings.IndexByte(s, '#'); i >= 0 { query = streams.ParseQuery(s[i+1:]) @@ -221,6 +222,7 @@ func parseArgs(s string) *ffmpeg.Args { default: s += "?video&audio" } + s += "&source=ffmpeg:" + url.QueryEscape(source) args.Input = inputTemplate("rtsp", s, query) } else if i = strings.Index(s, "?"); i > 0 { switch s[:i] { diff --git a/internal/rtsp/rtsp.go b/internal/rtsp/rtsp.go index 230bdece..a4075f6c 100644 --- a/internal/rtsp/rtsp.go +++ b/internal/rtsp/rtsp.go @@ -188,6 +188,9 @@ func tcpHandler(conn *rtsp.Conn) { conn.PacketSize = uint16(core.Atoi(s)) } + // will help to protect looping requests to same source + conn.Connection.Source = query.Get("source") + if err := stream.AddConsumer(conn); err != nil { log.Warn().Err(err).Str("stream", name).Msg("[rtsp]") return diff --git a/internal/streams/add_consumer.go b/internal/streams/add_consumer.go index eb767691..d72e17ee 100644 --- a/internal/streams/add_consumer.go +++ b/internal/streams/add_consumer.go @@ -22,6 +22,12 @@ func (s *Stream) AddConsumer(cons core.Consumer) (err error) { producers: for prodN, prod := range s.producers { + // check for loop request, ex. `camera1: ffmpeg:camera1` + if info, ok := cons.(core.Info); ok && prod.url == info.GetSource() { + log.Trace().Msgf("[streams] skip cons=%d prod=%d", consN, prodN) + continue + } + if prodErrors[prodN] != nil { log.Trace().Msgf("[streams] skip cons=%d prod=%d", consN, prodN) continue diff --git a/pkg/core/connection.go b/pkg/core/connection.go index 2c3f2196..cc0f43e4 100644 --- a/pkg/core/connection.go +++ b/pkg/core/connection.go @@ -25,6 +25,7 @@ type Info interface { SetSource(string) SetURL(string) WithRequest(*http.Request) + GetSource() string } // Connection just like webrtc.PeerConnection @@ -123,6 +124,10 @@ func (c *Connection) WithRequest(r *http.Request) { c.UserAgent = r.UserAgent() } +func (c *Connection) GetSource() string { + return c.Source +} + // Create like os.Create, init Consumer with existing Transport func Create(w io.Writer) (*Connection, error) { return &Connection{Transport: w}, nil From e982257271e15135c80be6f102b38d1e9394a9b1 Mon Sep 17 00:00:00 2001 From: Ikko Eltociear Ashimine Date: Sun, 10 Nov 2024 09:00:37 +0900 Subject: [PATCH 080/166] docs: update README.md shapshot -> snapshot --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 70ad4712..c37bf2f6 100644 --- a/README.md +++ b/README.md @@ -170,7 +170,7 @@ Available modules: - [api](#module-api) - HTTP API (important for WebRTC support) - [rtsp](#module-rtsp) - RTSP Server (important for FFmpeg support) - [webrtc](#module-webrtc) - WebRTC Server -- [mp4](#module-mp4) - MSE, MP4 stream and MP4 shapshot Server +- [mp4](#module-mp4) - MSE, MP4 stream and MP4 snapshot Server - [hls](#module-hls) - HLS TS or fMP4 stream Server - [mjpeg](#module-mjpeg) - MJPEG Server - [ffmpeg](#source-ffmpeg) - FFmpeg integration From 2348d12e9d701b1506db76cddf59af30a1acaf4e Mon Sep 17 00:00:00 2001 From: Jerome Date: Sun, 10 Nov 2024 13:13:31 +0100 Subject: [PATCH 081/166] Update README.md --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c37bf2f6..e87e35a0 100644 --- a/README.md +++ b/README.md @@ -648,10 +648,11 @@ This source type support Roborock vacuums with cameras. Known working models: - Roborock S6 MaxV - only video (the vacuum has no microphone) - Roborock S7 MaxV - video and two way audio +- Roborock Qrevo MaxV - video and two way audio -Source support load Roborock credentials from Home Assistant [custom integration](https://github.com/humbertogontijo/homeassistant-roborock). Otherwise, you need to log in to your Roborock account (MiHome account is not supported). Go to: go2rtc WebUI > Add webpage. Copy `roborock://...` source for your vacuum and paste it to `go2rtc.yaml` config. +Source support load Roborock credentials from Home Assistant [custom integration](https://github.com/humbertogontijo/homeassistant-roborock) or the [core integration](https://www.home-assistant.io/integrations/roborock). Otherwise, you need to log in to your Roborock account (MiHome account is not supported). Go to: go2rtc WebUI > Add webpage. Copy `roborock://...` source for your vacuum and paste it to `go2rtc.yaml` config. -If you have graphic pin for your vacuum - add it as numeric pin (lines: 123, 456, 678) to the end of the roborock-link. +If you have graphic pin for your vacuum - add it as numeric pin (lines: 123, 456, 789) to the end of the roborock-link. #### Source: WebRTC From fde04bd62512cfc4eb998354381a31a7f9ed21cc Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Sun, 10 Nov 2024 19:27:59 +0100 Subject: [PATCH 082/166] Improve codec not matched error by including kind --- internal/streams/add_consumer.go | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/internal/streams/add_consumer.go b/internal/streams/add_consumer.go index d72e17ee..f1c9aebc 100644 --- a/internal/streams/add_consumer.go +++ b/internal/streams/add_consumer.go @@ -130,12 +130,15 @@ func formatError(consMedias, prodMedias []*core.Media, prodErrors []error) error // 2. Return "codecs not matched" if prodMedias != nil { - var prod, cons string + var prod, cons map[string]string = make(map[string]string), make(map[string]string) for _, media := range prodMedias { if media.Direction == core.DirectionRecvonly { for _, codec := range media.Codecs { - prod = appendString(prod, codec.PrintName()) + if _, ok := prod[codec.Name]; !ok { + prod[media.Kind] = "" + } + prod[media.Kind] = appendString(prod[media.Kind], codec.PrintName()) } } } @@ -143,18 +146,29 @@ func formatError(consMedias, prodMedias []*core.Media, prodErrors []error) error for _, media := range consMedias { if media.Direction == core.DirectionSendonly { for _, codec := range media.Codecs { - cons = appendString(cons, codec.PrintName()) + if _, ok := cons[codec.Name]; !ok { + cons[media.Kind] = "" + } + cons[media.Kind] = appendString(cons[media.Kind], codec.PrintName()) } } } - return errors.New("streams: codecs not matched: " + prod + " => " + cons) + return errors.New("streams: codecs not matched: " + mapToString(prod) + " => " + mapToString(cons)) } // 3. Return unknown error return errors.New("streams: unknown error") } +func mapToString(m map[string]string) string { + var s string + for k, v := range m { + s = appendString(s, "("+k+": "+v+")") + } + return s +} + func appendString(s, elem string) string { if strings.Contains(s, elem) { return s From 7640a42bfcdc6e6cc50ffd0fc2dab9864d8d2f4e Mon Sep 17 00:00:00 2001 From: Andrew Marshall Date: Sun, 10 Nov 2024 15:42:40 -0500 Subject: [PATCH 083/166] Read from credential files See https://systemd.io/CREDENTIALS/. This will also work for Docker Secrets by setting `CREDENTIALS_DIRECTORY=/run/secrets`. --- internal/app/README.md | 6 +++--- pkg/shell/shell.go | 8 ++++++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/internal/app/README.md b/internal/app/README.md index 2460daa2..9ec3d9fc 100644 --- a/internal/app/README.md +++ b/internal/app/README.md @@ -19,15 +19,15 @@ go2rtc -c log.format=text -c /config/go2rtc.yaml -c rtsp.listen='' -c /usr/local ## Environment variables -Also go2rtc support templates for using environment variables in any part of config: +There is support for loading external variables into the config. First, they will be attempted to be loaded from [credential files](https://systemd.io/CREDENTIALS). If `CREDENTIALS_DIRECTORY` is not set, then the key will be loaded from an environment variable. If no environment variable is set, then the string will be left as-is. ```yaml streams: camera1: rtsp://rtsp:${CAMERA_PASSWORD}@192.168.1.123/av_stream/ch0 rtsp: - username: ${RTSP_USER:admin} # "admin" if env "RTSP_USER" not set - password: ${RTSP_PASS:secret} # "secret" if env "RTSP_PASS" not set + username: ${RTSP_USER:admin} # "admin" if "RTSP_USER" not set + password: ${RTSP_PASS:secret} # "secret" if "RTSP_PASS" not set ``` ## JSON Schema diff --git a/pkg/shell/shell.go b/pkg/shell/shell.go index d538b961..75df671f 100644 --- a/pkg/shell/shell.go +++ b/pkg/shell/shell.go @@ -3,6 +3,7 @@ package shell import ( "os" "os/signal" + "path/filepath" "regexp" "strings" "syscall" @@ -51,6 +52,13 @@ func ReplaceEnvVars(text string) string { dok = true } + if dir, vok := os.LookupEnv("CREDENTIALS_DIRECTORY"); vok { + value, err := os.ReadFile(filepath.Join(dir, key)) + if err == nil { + return strings.TrimSpace(string(value)) + } + } + if value, vok := os.LookupEnv(key); vok { return value } From d372597bdbbb0618093d0b2cec72b2d1180f52ea Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Mon, 11 Nov 2024 09:27:21 +0100 Subject: [PATCH 084/166] Lower codec not matched error for ffmpeg to debug --- internal/rtsp/rtsp.go | 9 +++++- internal/streams/add_consumer.go | 48 ++++++++++++++++++++------------ 2 files changed, 38 insertions(+), 19 deletions(-) diff --git a/internal/rtsp/rtsp.go b/internal/rtsp/rtsp.go index a4075f6c..cc6d5727 100644 --- a/internal/rtsp/rtsp.go +++ b/internal/rtsp/rtsp.go @@ -192,7 +192,14 @@ func tcpHandler(conn *rtsp.Conn) { conn.Connection.Source = query.Get("source") if err := stream.AddConsumer(conn); err != nil { - log.Warn().Err(err).Str("stream", name).Msg("[rtsp]") + logEvent := log.Warn() + + if _, ok := err.(*streams.CodecNotMatchedError); ok && strings.HasPrefix(query.Get("source"), "ffmpeg") { + // lower codec not matched error for ffmpeg to debug + logEvent = log.Debug() + } + + logEvent.Err(err).Str("stream", name).Msg("[rtsp]") return } diff --git a/internal/streams/add_consumer.go b/internal/streams/add_consumer.go index d72e17ee..0b8f6cf0 100644 --- a/internal/streams/add_consumer.go +++ b/internal/streams/add_consumer.go @@ -130,25 +130,10 @@ func formatError(consMedias, prodMedias []*core.Media, prodErrors []error) error // 2. Return "codecs not matched" if prodMedias != nil { - var prod, cons string - - for _, media := range prodMedias { - if media.Direction == core.DirectionRecvonly { - for _, codec := range media.Codecs { - prod = appendString(prod, codec.PrintName()) - } - } + return &CodecNotMatchedError{ + producerMedias: prodMedias, + consumerMedias: consMedias, } - - for _, media := range consMedias { - if media.Direction == core.DirectionSendonly { - for _, codec := range media.Codecs { - cons = appendString(cons, codec.PrintName()) - } - } - } - - return errors.New("streams: codecs not matched: " + prod + " => " + cons) } // 3. Return unknown error @@ -164,3 +149,30 @@ func appendString(s, elem string) string { } return s + ", " + elem } + +type CodecNotMatchedError struct { + producerMedias []*core.Media + consumerMedias []*core.Media +} + +func (e *CodecNotMatchedError) Error() string { + var prod, cons string + + for _, media := range e.producerMedias { + if media.Direction == core.DirectionRecvonly { + for _, codec := range media.Codecs { + prod = appendString(prod, codec.PrintName()) + } + } + } + + for _, media := range e.consumerMedias { + if media.Direction == core.DirectionSendonly { + for _, codec := range media.Codecs { + cons = appendString(cons, codec.PrintName()) + } + } + } + + return "streams: codecs not matched: " + prod + " => " + cons +} From 831aa03c9f184b0be20b624569bcd5dda8510e50 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Mon, 11 Nov 2024 11:16:12 +0100 Subject: [PATCH 085/166] Implement suggestion --- internal/ffmpeg/ffmpeg.go | 2 + internal/rtsp/rtsp.go | 8 ++-- internal/streams/add_consumer.go | 66 ++++++++++++++++++-------------- 3 files changed, 44 insertions(+), 32 deletions(-) diff --git a/internal/ffmpeg/ffmpeg.go b/internal/ffmpeg/ffmpeg.go index b934be53..b57dcc70 100644 --- a/internal/ffmpeg/ffmpeg.go +++ b/internal/ffmpeg/ffmpeg.go @@ -223,6 +223,8 @@ func parseArgs(s string) *ffmpeg.Args { s += "?video&audio" } s += "&source=ffmpeg:" + url.QueryEscape(source) + // change codec not matched error level to debug + s += "&" + string(streams.CodecNotMatchedErrorCode) + "=" + zerolog.DebugLevel.String() args.Input = inputTemplate("rtsp", s, query) } else if i = strings.Index(s, "?"); i > 0 { switch s[:i] { diff --git a/internal/rtsp/rtsp.go b/internal/rtsp/rtsp.go index cc6d5727..1bb41d83 100644 --- a/internal/rtsp/rtsp.go +++ b/internal/rtsp/rtsp.go @@ -194,9 +194,11 @@ func tcpHandler(conn *rtsp.Conn) { if err := stream.AddConsumer(conn); err != nil { logEvent := log.Warn() - if _, ok := err.(*streams.CodecNotMatchedError); ok && strings.HasPrefix(query.Get("source"), "ffmpeg") { - // lower codec not matched error for ffmpeg to debug - logEvent = log.Debug() + if err, ok := err.(*streams.ErrorWithErrorCode); ok { + level, parseErr := zerolog.ParseLevel(query.Get(err.Code())) + if parseErr == nil { + logEvent = log.WithLevel(level) + } } logEvent.Err(err).Str("stream", name).Msg("[rtsp]") diff --git a/internal/streams/add_consumer.go b/internal/streams/add_consumer.go index 0b8f6cf0..efe8542b 100644 --- a/internal/streams/add_consumer.go +++ b/internal/streams/add_consumer.go @@ -1,7 +1,6 @@ package streams import ( - "errors" "strings" "github.com/AlexxIT/go2rtc/pkg/core" @@ -125,19 +124,34 @@ func formatError(consMedias, prodMedias []*core.Media, prodErrors []error) error } if len(text) != 0 { - return errors.New("streams: " + text) + return &ErrorWithErrorCode{MultipleErrorCode, "streams: " + text} } // 2. Return "codecs not matched" if prodMedias != nil { - return &CodecNotMatchedError{ - producerMedias: prodMedias, - consumerMedias: consMedias, + var prod, cons string + + for _, media := range prodMedias { + if media.Direction == core.DirectionRecvonly { + for _, codec := range media.Codecs { + prod = appendString(prod, codec.PrintName()) + } + } } + + for _, media := range consMedias { + if media.Direction == core.DirectionSendonly { + for _, codec := range media.Codecs { + cons = appendString(cons, codec.PrintName()) + } + } + } + + return &ErrorWithErrorCode{CodecNotMatchedErrorCode, "streams: codecs not matched: " + prod + " => " + cons} } // 3. Return unknown error - return errors.New("streams: unknown error") + return &ErrorWithErrorCode{UnknownErrorCode, "streams: unknown error"} } func appendString(s, elem string) string { @@ -150,29 +164,23 @@ func appendString(s, elem string) string { return s + ", " + elem } -type CodecNotMatchedError struct { - producerMedias []*core.Media - consumerMedias []*core.Media +type ErrorCode string + +const ( + CodecNotMatchedErrorCode ErrorCode = "codecNotMatched" + MultipleErrorCode ErrorCode = "multiple" + UnknownErrorCode ErrorCode = "unknown" +) + +type ErrorWithErrorCode struct { + code ErrorCode + message string } -func (e *CodecNotMatchedError) Error() string { - var prod, cons string - - for _, media := range e.producerMedias { - if media.Direction == core.DirectionRecvonly { - for _, codec := range media.Codecs { - prod = appendString(prod, codec.PrintName()) - } - } - } - - for _, media := range e.consumerMedias { - if media.Direction == core.DirectionSendonly { - for _, codec := range media.Codecs { - cons = appendString(cons, codec.PrintName()) - } - } - } - - return "streams: codecs not matched: " + prod + " => " + cons +func (e *ErrorWithErrorCode) Error() string { + return e.message +} + +func (e *ErrorWithErrorCode) Code() string { + return string(e.code) } From 9ee8174d5f12382831f1f2b36c094f416738c153 Mon Sep 17 00:00:00 2001 From: Alex X Date: Mon, 11 Nov 2024 16:36:51 +0300 Subject: [PATCH 086/166] Code refactoring for #1448 --- internal/streams/add_consumer.go | 22 ++++------------------ 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/internal/streams/add_consumer.go b/internal/streams/add_consumer.go index f1c9aebc..7400ce6e 100644 --- a/internal/streams/add_consumer.go +++ b/internal/streams/add_consumer.go @@ -130,15 +130,12 @@ func formatError(consMedias, prodMedias []*core.Media, prodErrors []error) error // 2. Return "codecs not matched" if prodMedias != nil { - var prod, cons map[string]string = make(map[string]string), make(map[string]string) + var prod, cons string for _, media := range prodMedias { if media.Direction == core.DirectionRecvonly { for _, codec := range media.Codecs { - if _, ok := prod[codec.Name]; !ok { - prod[media.Kind] = "" - } - prod[media.Kind] = appendString(prod[media.Kind], codec.PrintName()) + prod = appendString(prod, media.Kind+":"+codec.PrintName()) } } } @@ -146,29 +143,18 @@ func formatError(consMedias, prodMedias []*core.Media, prodErrors []error) error for _, media := range consMedias { if media.Direction == core.DirectionSendonly { for _, codec := range media.Codecs { - if _, ok := cons[codec.Name]; !ok { - cons[media.Kind] = "" - } - cons[media.Kind] = appendString(cons[media.Kind], codec.PrintName()) + cons = appendString(cons, media.Kind+":"+codec.PrintName()) } } } - return errors.New("streams: codecs not matched: " + mapToString(prod) + " => " + mapToString(cons)) + return errors.New("streams: codecs not matched: " + prod + " => " + cons) } // 3. Return unknown error return errors.New("streams: unknown error") } -func mapToString(m map[string]string) string { - var s string - for k, v := range m { - s = appendString(s, "("+k+": "+v+")") - } - return s -} - func appendString(s, elem string) string { if strings.Contains(s, elem) { return s From 570b7d0d97ea222b9db719802c4dce0eea0ef7b9 Mon Sep 17 00:00:00 2001 From: Alex X Date: Mon, 11 Nov 2024 17:45:55 +0300 Subject: [PATCH 087/166] Code refactoring for #1450 --- internal/ffmpeg/ffmpeg.go | 5 +++-- internal/rtsp/rtsp.go | 21 ++++++++++----------- internal/streams/add_consumer.go | 28 ++++------------------------ 3 files changed, 17 insertions(+), 37 deletions(-) diff --git a/internal/ffmpeg/ffmpeg.go b/internal/ffmpeg/ffmpeg.go index b57dcc70..25d61e4b 100644 --- a/internal/ffmpeg/ffmpeg.go +++ b/internal/ffmpeg/ffmpeg.go @@ -223,8 +223,9 @@ func parseArgs(s string) *ffmpeg.Args { s += "?video&audio" } s += "&source=ffmpeg:" + url.QueryEscape(source) - // change codec not matched error level to debug - s += "&" + string(streams.CodecNotMatchedErrorCode) + "=" + zerolog.DebugLevel.String() + for _, v := range query["query"] { + s += "&" + v + } args.Input = inputTemplate("rtsp", s, query) } else if i = strings.Index(s, "?"); i > 0 { switch s[:i] { diff --git a/internal/rtsp/rtsp.go b/internal/rtsp/rtsp.go index 1bb41d83..0fe135f8 100644 --- a/internal/rtsp/rtsp.go +++ b/internal/rtsp/rtsp.go @@ -147,6 +147,7 @@ func tcpHandler(conn *rtsp.Conn) { var closer func() trace := log.Trace().Enabled() + level := zerolog.WarnLevel conn.Listen(func(msg any) { if trace { @@ -188,20 +189,18 @@ func tcpHandler(conn *rtsp.Conn) { conn.PacketSize = uint16(core.Atoi(s)) } + // param name like ffmpeg style https://ffmpeg.org/ffmpeg-protocols.html + if s := query.Get("log_level"); s != "" { + if lvl, err := zerolog.ParseLevel(s); err == nil { + level = lvl + } + } + // will help to protect looping requests to same source conn.Connection.Source = query.Get("source") if err := stream.AddConsumer(conn); err != nil { - logEvent := log.Warn() - - if err, ok := err.(*streams.ErrorWithErrorCode); ok { - level, parseErr := zerolog.ParseLevel(query.Get(err.Code())) - if parseErr == nil { - logEvent = log.WithLevel(level) - } - } - - logEvent.Err(err).Str("stream", name).Msg("[rtsp]") + log.WithLevel(level).Err(err).Str("stream", name).Msg("[rtsp]") return } @@ -239,7 +238,7 @@ func tcpHandler(conn *rtsp.Conn) { if err := conn.Accept(); err != nil { if err != io.EOF { - log.Warn().Err(err).Caller().Send() + log.WithLevel(level).Err(err).Caller().Send() } if closer != nil { closer() diff --git a/internal/streams/add_consumer.go b/internal/streams/add_consumer.go index efe8542b..d72e17ee 100644 --- a/internal/streams/add_consumer.go +++ b/internal/streams/add_consumer.go @@ -1,6 +1,7 @@ package streams import ( + "errors" "strings" "github.com/AlexxIT/go2rtc/pkg/core" @@ -124,7 +125,7 @@ func formatError(consMedias, prodMedias []*core.Media, prodErrors []error) error } if len(text) != 0 { - return &ErrorWithErrorCode{MultipleErrorCode, "streams: " + text} + return errors.New("streams: " + text) } // 2. Return "codecs not matched" @@ -147,11 +148,11 @@ func formatError(consMedias, prodMedias []*core.Media, prodErrors []error) error } } - return &ErrorWithErrorCode{CodecNotMatchedErrorCode, "streams: codecs not matched: " + prod + " => " + cons} + return errors.New("streams: codecs not matched: " + prod + " => " + cons) } // 3. Return unknown error - return &ErrorWithErrorCode{UnknownErrorCode, "streams: unknown error"} + return errors.New("streams: unknown error") } func appendString(s, elem string) string { @@ -163,24 +164,3 @@ func appendString(s, elem string) string { } return s + ", " + elem } - -type ErrorCode string - -const ( - CodecNotMatchedErrorCode ErrorCode = "codecNotMatched" - MultipleErrorCode ErrorCode = "multiple" - UnknownErrorCode ErrorCode = "unknown" -) - -type ErrorWithErrorCode struct { - code ErrorCode - message string -} - -func (e *ErrorWithErrorCode) Error() string { - return e.message -} - -func (e *ErrorWithErrorCode) Code() string { - return string(e.code) -} From dbe9e4aadeeae306b2e90a4668f077d405448eff Mon Sep 17 00:00:00 2001 From: Alex X Date: Mon, 11 Nov 2024 20:20:53 +0300 Subject: [PATCH 088/166] Update version to 1.9.7 --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index df3468f4..7f94b930 100644 --- a/main.go +++ b/main.go @@ -36,7 +36,7 @@ import ( ) func main() { - app.Version = "1.9.6" + app.Version = "1.9.7" // 1. Core modules: app, api/ws, streams From 25145f72e56560666e6718740442d16a8c5127bb Mon Sep 17 00:00:00 2001 From: Alex X Date: Thu, 14 Nov 2024 19:39:26 +0300 Subject: [PATCH 089/166] Fix broken incoming sources after v1.9.7 #1458 --- internal/streams/play.go | 2 +- internal/streams/stream.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/streams/play.go b/internal/streams/play.go index 7ada66e6..9bec7258 100644 --- a/internal/streams/play.go +++ b/internal/streams/play.go @@ -103,7 +103,7 @@ func (s *Stream) Play(source string) error { } func (s *Stream) AddInternalProducer(conn core.Producer) { - producer := &Producer{conn: conn, state: stateInternal} + producer := &Producer{conn: conn, state: stateInternal, url: "internal"} s.mu.Lock() s.producers = append(s.producers, producer) s.mu.Unlock() diff --git a/internal/streams/stream.go b/internal/streams/stream.go index e194e0ac..569e63ee 100644 --- a/internal/streams/stream.go +++ b/internal/streams/stream.go @@ -76,7 +76,7 @@ func (s *Stream) RemoveConsumer(cons core.Consumer) { } func (s *Stream) AddProducer(prod core.Producer) { - producer := &Producer{conn: prod, state: stateExternal} + producer := &Producer{conn: prod, state: stateExternal, url: "external"} s.mu.Lock() s.producers = append(s.producers, producer) s.mu.Unlock() From a8edaedc8b7518507eb1a5720d9963639a056b13 Mon Sep 17 00:00:00 2001 From: Alex X Date: Thu, 14 Nov 2024 19:39:26 +0300 Subject: [PATCH 090/166] Fix broken incoming sources after v1.9.7 #1458 --- internal/streams/play.go | 2 +- internal/streams/stream.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/streams/play.go b/internal/streams/play.go index 7ada66e6..9bec7258 100644 --- a/internal/streams/play.go +++ b/internal/streams/play.go @@ -103,7 +103,7 @@ func (s *Stream) Play(source string) error { } func (s *Stream) AddInternalProducer(conn core.Producer) { - producer := &Producer{conn: conn, state: stateInternal} + producer := &Producer{conn: conn, state: stateInternal, url: "internal"} s.mu.Lock() s.producers = append(s.producers, producer) s.mu.Unlock() diff --git a/internal/streams/stream.go b/internal/streams/stream.go index e194e0ac..569e63ee 100644 --- a/internal/streams/stream.go +++ b/internal/streams/stream.go @@ -76,7 +76,7 @@ func (s *Stream) RemoveConsumer(cons core.Consumer) { } func (s *Stream) AddProducer(prod core.Producer) { - producer := &Producer{conn: prod, state: stateExternal} + producer := &Producer{conn: prod, state: stateExternal, url: "external"} s.mu.Lock() s.producers = append(s.producers, producer) s.mu.Unlock() From 194d1dae51ef547f368d8c467dbee782527ef11f Mon Sep 17 00:00:00 2001 From: Alex X Date: Sun, 24 Nov 2024 13:09:13 +0300 Subject: [PATCH 091/166] Add support doorbird source #1060 --- internal/doorbird/doorbird.go | 36 ++++++++++++ internal/http/http.go | 4 ++ main.go | 2 + pkg/doorbird/backchannel.go | 100 ++++++++++++++++++++++++++++++++++ pkg/pcm/producer.go | 55 +++++++++++++++++++ 5 files changed, 197 insertions(+) create mode 100644 internal/doorbird/doorbird.go create mode 100644 pkg/doorbird/backchannel.go create mode 100644 pkg/pcm/producer.go diff --git a/internal/doorbird/doorbird.go b/internal/doorbird/doorbird.go new file mode 100644 index 00000000..c56fc0f9 --- /dev/null +++ b/internal/doorbird/doorbird.go @@ -0,0 +1,36 @@ +package doorbird + +import ( + "net/url" + + "github.com/AlexxIT/go2rtc/internal/streams" + "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/AlexxIT/go2rtc/pkg/doorbird" +) + +func Init() { + streams.RedirectFunc("doorbird", func(rawURL string) (string, error) { + u, err := url.Parse(rawURL) + if err != nil { + return "", err + } + + // https://www.doorbird.com/downloads/api_lan.pdf + switch u.Query().Get("media") { + case "video": + u.Path = "/bha-api/video.cgi" + case "audio": + u.Path = "/bha-api/audio-receive.cgi" + default: + return "", nil + } + + u.Scheme = "http" + + return u.String(), nil + }) + + streams.HandleFunc("doorbird", func(source string) (core.Producer, error) { + return doorbird.Dial(source) + }) +} diff --git a/internal/http/http.go b/internal/http/http.go index a35439d5..4b0560c1 100644 --- a/internal/http/http.go +++ b/internal/http/http.go @@ -14,6 +14,7 @@ import ( "github.com/AlexxIT/go2rtc/pkg/image" "github.com/AlexxIT/go2rtc/pkg/magic" "github.com/AlexxIT/go2rtc/pkg/mpjpeg" + "github.com/AlexxIT/go2rtc/pkg/pcm" "github.com/AlexxIT/go2rtc/pkg/tcp" ) @@ -87,6 +88,9 @@ func do(req *http.Request) (core.Producer, error) { return image.Open(res) case ct == "multipart/x-mixed-replace": return mpjpeg.Open(res.Body) + //https://www.iana.org/assignments/media-types/audio/basic + case ct == "audio/basic": + return pcm.Open(res.Body) } return magic.Open(res.Body) diff --git a/main.go b/main.go index 7f94b930..d5c59ffc 100644 --- a/main.go +++ b/main.go @@ -6,6 +6,7 @@ import ( "github.com/AlexxIT/go2rtc/internal/app" "github.com/AlexxIT/go2rtc/internal/bubble" "github.com/AlexxIT/go2rtc/internal/debug" + "github.com/AlexxIT/go2rtc/internal/doorbird" "github.com/AlexxIT/go2rtc/internal/dvrip" "github.com/AlexxIT/go2rtc/internal/echo" "github.com/AlexxIT/go2rtc/internal/exec" @@ -82,6 +83,7 @@ func main() { bubble.Init() // bubble source expr.Init() // expr source gopro.Init() // gopro source + doorbird.Init() // doorbird source // 6. Helper modules diff --git a/pkg/doorbird/backchannel.go b/pkg/doorbird/backchannel.go new file mode 100644 index 00000000..c6a7dec1 --- /dev/null +++ b/pkg/doorbird/backchannel.go @@ -0,0 +1,100 @@ +package doorbird + +import ( + "fmt" + "net" + "net/http" + "net/url" + "time" + + "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/pion/rtp" +) + +type Client struct { + core.Connection + conn net.Conn +} + +func Dial(rawURL string) (*Client, error) { + u, err := url.Parse(rawURL) + if err != nil { + return nil, err + } + + user := u.User.Username() + pass, _ := u.User.Password() + + rawURL = fmt.Sprintf("http://%s/bha-api/audio-transmit.cgi?http-user=%s&&http-password=%s", u.Host, user, pass) + + req, err := http.NewRequest("POST", rawURL, nil) + if err != nil { + return nil, err + } + req.Header = http.Header{ + "Content-Type": []string{"audio/basic"}, + "Content-Length": []string{"9999999"}, + "Connection": []string{"Keep-Alive"}, + "Cache-Control": []string{"no-cache"}, + } + + if u.Port() == "" { + u.Host += ":80" + } + + conn, err := net.DialTimeout("tcp", u.Host, core.ConnDialTimeout) + if err != nil { + return nil, err + } + + _ = conn.SetWriteDeadline(time.Now().Add(core.ConnDeadline)) + if err = req.Write(conn); err != nil { + return nil, err + } + + medias := []*core.Media{ + { + Kind: core.KindAudio, + Direction: core.DirectionSendonly, + Codecs: []*core.Codec{ + {Name: core.CodecPCMU, ClockRate: 8000}, + }, + }, + } + + return &Client{ + core.Connection{ + ID: core.NewID(), + FormatName: "doorbird", + Protocol: "http", + URL: rawURL, + Medias: medias, + Transport: conn, + }, + conn, + }, nil +} + +func (c *Client) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) { + return nil, core.ErrCantGetTrack +} + +func (c *Client) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error { + sender := core.NewSender(media, track.Codec) + + sender.Handler = func(pkt *rtp.Packet) { + _ = c.conn.SetWriteDeadline(time.Now().Add(core.ConnDeadline)) + if n, err := c.conn.Write(pkt.Payload); err == nil { + c.Send += n + } + } + + sender.HandleRTP(track) + c.Senders = append(c.Senders, sender) + return nil +} + +func (c *Client) Start() (err error) { + _, err = c.conn.Read(nil) + return +} diff --git a/pkg/pcm/producer.go b/pkg/pcm/producer.go new file mode 100644 index 00000000..8a957f6d --- /dev/null +++ b/pkg/pcm/producer.go @@ -0,0 +1,55 @@ +package pcm + +import ( + "io" + + "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/pion/rtp" +) + +type Producer struct { + core.Connection + rd io.Reader +} + +func Open(rd io.Reader) (*Producer, error) { + medias := []*core.Media{ + { + Kind: core.KindAudio, + Direction: core.DirectionRecvonly, + Codecs: []*core.Codec{ + {Name: core.CodecPCMU, ClockRate: 8000}, + }, + }, + } + return &Producer{ + core.Connection{ + ID: core.NewID(), + FormatName: "pcm", + Medias: medias, + Transport: rd, + }, + rd, + }, nil +} + +func (c *Producer) Start() error { + for { + payload := make([]byte, 1024) + if _, err := io.ReadFull(c.rd, payload); err != nil { + return err + } + + c.Recv += 1024 + + if len(c.Receivers) == 0 { + continue + } + + pkt := &rtp.Packet{ + Header: rtp.Header{Timestamp: core.Now90000()}, + Payload: payload, + } + c.Receivers[0].WriteRTP(pkt) + } +} From 5b53ca7cf1df134923a31dca00aaa84c460a55a7 Mon Sep 17 00:00:00 2001 From: oeiber <46045177+oeiber@users.noreply.github.com> Date: Sun, 24 Nov 2024 16:19:58 +0100 Subject: [PATCH 092/166] Removing double additional '&' in rawURL --- pkg/doorbird/backchannel.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/doorbird/backchannel.go b/pkg/doorbird/backchannel.go index c6a7dec1..e5f3257c 100644 --- a/pkg/doorbird/backchannel.go +++ b/pkg/doorbird/backchannel.go @@ -25,7 +25,7 @@ func Dial(rawURL string) (*Client, error) { user := u.User.Username() pass, _ := u.User.Password() - rawURL = fmt.Sprintf("http://%s/bha-api/audio-transmit.cgi?http-user=%s&&http-password=%s", u.Host, user, pass) + rawURL = fmt.Sprintf("http://%s/bha-api/audio-transmit.cgi?http-user=%s&http-password=%s", u.Host, user, pass) req, err := http.NewRequest("POST", rawURL, nil) if err != nil { From d8c0f9d1d931fe33947dd25d2af9c2c5a976579d Mon Sep 17 00:00:00 2001 From: Alex X Date: Thu, 5 Dec 2024 10:54:46 +0300 Subject: [PATCH 093/166] Update support doorbird source #1060 --- pkg/doorbird/backchannel.go | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/pkg/doorbird/backchannel.go b/pkg/doorbird/backchannel.go index e5f3257c..82379383 100644 --- a/pkg/doorbird/backchannel.go +++ b/pkg/doorbird/backchannel.go @@ -3,7 +3,6 @@ package doorbird import ( "fmt" "net" - "net/http" "net/url" "time" @@ -25,19 +24,6 @@ func Dial(rawURL string) (*Client, error) { user := u.User.Username() pass, _ := u.User.Password() - rawURL = fmt.Sprintf("http://%s/bha-api/audio-transmit.cgi?http-user=%s&http-password=%s", u.Host, user, pass) - - req, err := http.NewRequest("POST", rawURL, nil) - if err != nil { - return nil, err - } - req.Header = http.Header{ - "Content-Type": []string{"audio/basic"}, - "Content-Length": []string{"9999999"}, - "Connection": []string{"Keep-Alive"}, - "Cache-Control": []string{"no-cache"}, - } - if u.Port() == "" { u.Host += ":80" } @@ -47,8 +33,15 @@ func Dial(rawURL string) (*Client, error) { return nil, err } + s := fmt.Sprintf("POST /bha-api/audio-transmit.cgi?http-user=%s&http-password=%s HTTP/1.0\r\n", user, pass) + + "Content-Type: audio/basic\r\n" + + "Content-Length: 9999999\r\n" + + "Connection: Keep-Alive\r\n" + + "Cache-Control: no-cache\r\n" + + "\r\n" + _ = conn.SetWriteDeadline(time.Now().Add(core.ConnDeadline)) - if err = req.Write(conn); err != nil { + if _, err = conn.Write([]byte(s)); err != nil { return nil, err } From f1ba5e95ec21f3e03c158a372ca25d53193c6992 Mon Sep 17 00:00:00 2001 From: Alex X Date: Fri, 6 Dec 2024 12:34:31 +0300 Subject: [PATCH 094/166] Fix parsing RTSP Transport header #1235 --- pkg/rtsp/server.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/rtsp/server.go b/pkg/rtsp/server.go index 7953b0dc..c96125a2 100644 --- a/pkg/rtsp/server.go +++ b/pkg/rtsp/server.go @@ -149,7 +149,7 @@ func (c *Conn) Accept() error { } const transport = "RTP/AVP/TCP;unicast;interleaved=" - if strings.HasPrefix(tr, transport) { + if tr = core.Between(tr, "interleaved=", ";"); tr != "" { c.session = core.RandString(8, 10) c.state = StateSetup @@ -157,13 +157,13 @@ func (c *Conn) Accept() error { if i := reqTrackID(req); i >= 0 && i < len(c.Senders) { // mark sender as SETUP c.Senders[i].Media.ID = MethodSetup - tr = fmt.Sprintf("RTP/AVP/TCP;unicast;interleaved=%d-%d", i*2, i*2+1) - res.Header.Set("Transport", tr) + tr = fmt.Sprintf("%d-%d", i*2, i*2+1) + res.Header.Set("Transport", transport+tr) } else { res.Status = "400 Bad Request" } } else { - res.Header.Set("Transport", tr[:len(transport)+3]) + res.Header.Set("Transport", transport+tr) } } else { res.Status = "461 Unsupported transport" From 8ecaabfce9b563f3fb6a0025531add7786865338 Mon Sep 17 00:00:00 2001 From: Alex X Date: Mon, 16 Dec 2024 20:24:45 +0300 Subject: [PATCH 095/166] Add support VIGI cameras #1470 --- internal/tapo/tapo.go | 4 ++ pkg/tapo/client.go | 106 +++++++++++++++++++++++++++++------------- pkg/tapo/producer.go | 2 +- 3 files changed, 79 insertions(+), 33 deletions(-) diff --git a/internal/tapo/tapo.go b/internal/tapo/tapo.go index 724c9e86..88eff5c4 100644 --- a/internal/tapo/tapo.go +++ b/internal/tapo/tapo.go @@ -15,4 +15,8 @@ func Init() { streams.HandleFunc("tapo", func(source string) (core.Producer, error) { return tapo.Dial(source) }) + + streams.HandleFunc("vigi", func(source string) (core.Producer, error) { + return tapo.Dial(source) + }) } diff --git a/pkg/tapo/client.go b/pkg/tapo/client.go index 3585011c..6ccafe4e 100644 --- a/pkg/tapo/client.go +++ b/pkg/tapo/client.go @@ -27,7 +27,7 @@ import ( type Client struct { core.Listener - url string + url *url.URL medias []*core.Media receivers []*core.Receiver @@ -52,17 +52,15 @@ type cbcMode interface { SetIV([]byte) } -func Dial(url string) (*Client, error) { - var err error - c := &Client{url: url} - if c.conn1, err = c.newConn(); err != nil { - return nil, err - } - return c, nil -} - -func (c *Client) newConn() (net.Conn, error) { - u, err := url.Parse(c.url) +// Dial support different urls: +// - tapo://{cloud-password}@192.168.1.123 - auth to Tapo cameras +// with cloud password (autodetect hash method) +// - tapo://admin:{hashed-cloud-password}@192.168.1.123 - auth to Tapo cameras +// with pre-hashed cloud password +// - vigi://admin:{password}@192.168.1.123 - auth to Vigi cameras with password +// for admin account (other not supported) +func Dial(rawURL string) (*Client, error) { + u, err := url.Parse(rawURL) if err != nil { return nil, err } @@ -71,21 +69,31 @@ func (c *Client) newConn() (net.Conn, error) { u.Host += ":8800" } - req, err := http.NewRequest("POST", "http://"+u.Host+"/stream", nil) + c := &Client{url: u} + if c.conn1, err = c.newConn(); err != nil { + return nil, err + } + return c, nil +} + +func (c *Client) newConn() (net.Conn, error) { + req, err := http.NewRequest("POST", "http://"+c.url.Host+"/stream", nil) if err != nil { return nil, err } - query := u.Query() + query := c.url.Query() if deviceId := query.Get("deviceId"); deviceId != "" { req.URL.RawQuery = "deviceId=" + deviceId } - req.URL.User = u.User req.Header.Set("Content-Type", "multipart/mixed; boundary=--client-stream-boundary--") - conn, res, err := dial(req) + username := c.url.User.Username() + password, _ := c.url.User.Password() + + conn, res, err := dial(req, c.url.Scheme, username, password) if err != nil { return nil, err } @@ -95,7 +103,7 @@ func (c *Client) newConn() (net.Conn, error) { } if c.decrypt == nil { - c.newDectypter(res) + c.newDectypter(res, c.url.Scheme, username, password) } channel := query.Get("channel") @@ -119,14 +127,18 @@ func (c *Client) newConn() (net.Conn, error) { return conn, nil } -func (c *Client) newDectypter(res *http.Response) { - username := res.Request.URL.User.Username() - password, _ := res.Request.URL.User.Password() +func (c *Client) newDectypter(res *http.Response, brand, username, password string) { + exchange := res.Header.Get("Key-Exchange") + nonce := core.Between(exchange, `nonce="`, `"`) - // extract nonce from response - // cipher="AES_128_CBC" username="admin" padding="PKCS7_16" algorithm="MD5" nonce="***" - nonce := res.Header.Get("Key-Exchange") - nonce = core.Between(nonce, `nonce="`, `"`) + if brand == "tapo" && password == "" { + if strings.Contains(exchange, `encrypt_type="3"`) { + password = fmt.Sprintf("%32X", sha256.Sum256([]byte(username))) + } else { + password = fmt.Sprintf("%16X", md5.Sum([]byte(username))) + } + username = "admin" + } key := md5.Sum([]byte(nonce + ":" + password)) iv := md5.Sum([]byte(username + ":" + nonce)) @@ -263,16 +275,12 @@ func (c *Client) Request(conn net.Conn, body []byte) (string, error) { } } -func dial(req *http.Request) (net.Conn, *http.Response, error) { +func dial(req *http.Request, brand, username, password string) (net.Conn, *http.Response, error) { conn, err := net.DialTimeout("tcp", req.URL.Host, core.ConnDialTimeout) if err != nil { return nil, nil, err } - username := req.URL.User.Username() - password, _ := req.URL.User.Password() - req.URL.User = nil - if err = req.Write(conn); err != nil { return nil, nil, err } @@ -291,7 +299,7 @@ func dial(req *http.Request) (net.Conn, *http.Response, error) { return nil, nil, fmt.Errorf("Expected StatusCode to be %d, received %d", http.StatusUnauthorized, res.StatusCode) } - if password == "" { + if brand == "tapo" && password == "" { // support cloud password in place of username if strings.Contains(auth, `encrypt_type="3"`) { password = fmt.Sprintf("%32X", sha256.Sum256([]byte(username))) @@ -299,6 +307,8 @@ func dial(req *http.Request) (net.Conn, *http.Response, error) { password = fmt.Sprintf("%16X", md5.Sum([]byte(username))) } username = "admin" + } else if brand == "vigi" && username == "admin" { + password = securityEncode(password) } realm := tcp.Between(auth, `realm="`, `"`) @@ -331,7 +341,39 @@ func dial(req *http.Request) (net.Conn, *http.Response, error) { return nil, nil, err } - req.URL.User = url.UserPassword(username, password) - return conn, res, nil } + +const ( + keyShort = "RDpbLfCPsJZ7fiv" + keyLong = "yLwVl0zKqws7LgKPRQ84Mdt708T1qQ3Ha7xv3H7NyU84p21BriUWBU43odz3iP4rBL3cD02KZciXTysVXiV8ngg6vL48rPJyAUw0HurW20xqxv9aYb4M9wK1Ae0wlro510qXeU07kV57fQMc8L6aLgMLwygtc0F10a0Dg70TOoouyFhdysuRMO51yY5ZlOZZLEal1h0t9YQW0Ko7oBwmCAHoic4HYbUyVeU3sfQ1xtXcPcf1aT303wAQhv66qzW" +) + +func securityEncode(s string) string { + size := len(s) + + var n int // max + if size > len(keyShort) { + n = size + } else { + n = len(keyShort) + } + + b := make([]byte, n) + + for i := 0; i < n; i++ { + c1 := 187 + c2 := 187 + if i >= size { + c1 = int(keyShort[i]) + } else if i >= len(keyShort) { + c2 = int(s[i]) + } else { + c1 = int(keyShort[i]) + c2 = int(s[i]) + } + b[i] = keyLong[(c1^c2)%len(keyLong)] + } + + return string(b) +} diff --git a/pkg/tapo/producer.go b/pkg/tapo/producer.go index 7d66d907..87a91ff5 100644 --- a/pkg/tapo/producer.go +++ b/pkg/tapo/producer.go @@ -77,7 +77,7 @@ func (c *Client) Stop() error { func (c *Client) MarshalJSON() ([]byte, error) { info := &core.Connection{ ID: core.ID(c), - FormatName: "tapo", + FormatName: c.url.Scheme, Protocol: "http", Medias: c.medias, Recv: c.recv, From 29f7f1a57d8b4366d68c20eec090a7a5c641ff38 Mon Sep 17 00:00:00 2001 From: fmcloudconsulting <170678386+fmcloudconsulting@users.noreply.github.com> Date: Mon, 16 Dec 2024 22:50:35 +0100 Subject: [PATCH 096/166] feat: accept rtsp client without interleaved parameter --- pkg/rtsp/server.go | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/pkg/rtsp/server.go b/pkg/rtsp/server.go index c96125a2..b59d9abf 100644 --- a/pkg/rtsp/server.go +++ b/pkg/rtsp/server.go @@ -148,25 +148,30 @@ func (c *Conn) Accept() error { Request: req, } - const transport = "RTP/AVP/TCP;unicast;interleaved=" - if tr = core.Between(tr, "interleaved=", ";"); tr != "" { - c.session = core.RandString(8, 10) - c.state = StateSetup + const transport = "RTP/AVP/TCP;unicast" - if c.mode == core.ModePassiveConsumer { - if i := reqTrackID(req); i >= 0 && i < len(c.Senders) { - // mark sender as SETUP - c.Senders[i].Media.ID = MethodSetup - tr = fmt.Sprintf("%d-%d", i*2, i*2+1) - res.Header.Set("Transport", transport+tr) + c.session = core.RandString(8, 10) + c.state = StateSetup + + if c.mode == core.ModePassiveConsumer { + if i := reqTrackID(req); i >= 0 && i < len(c.Senders) { + // mark sender as SETUP + c.Senders[i].Media.ID = MethodSetup + interleaved := fmt.Sprintf("%d-%d", i*2, i*2+1) + + // Check if transport already contains the 'interleaved' parameter + if strings.Contains(transport, "interleaved=") { + // If so, just update the interleaved value + res.Header.Set("Transport", strings.Replace(transport, "interleaved=[^;]*", "interleaved="+interleaved, 1)) } else { - res.Status = "400 Bad Request" + // Otherwise, append the interleaved parameter + res.Header.Set("Transport", transport+";interleaved="+interleaved) } } else { - res.Header.Set("Transport", transport+tr) + res.Status = "400 Bad Request" } } else { - res.Status = "461 Unsupported transport" + res.Header.Set("Transport", tr) } if err = c.WriteResponse(res); err != nil { From fd125ecc683daf8f526673aca3ead91e53540e37 Mon Sep 17 00:00:00 2001 From: fmcloudconsulting <170678386+fmcloudconsulting@users.noreply.github.com> Date: Tue, 17 Dec 2024 17:28:13 +0100 Subject: [PATCH 097/166] fix: return 461 if client requested an invalid transport method --- pkg/rtsp/server.go | 41 ++++++++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/pkg/rtsp/server.go b/pkg/rtsp/server.go index b59d9abf..f5967c5a 100644 --- a/pkg/rtsp/server.go +++ b/pkg/rtsp/server.go @@ -150,28 +150,35 @@ func (c *Conn) Accept() error { const transport = "RTP/AVP/TCP;unicast" - c.session = core.RandString(8, 10) - c.state = StateSetup + // Test if client requests unicast with TCP transport, otherwise return 461 Transport not supported + // This allows smart clients who initially requested UDP to fall back on TCP transport. + if strings.HasPrefix(tr, transport) { + + c.session = core.RandString(8, 10) + c.state = StateSetup - if c.mode == core.ModePassiveConsumer { - if i := reqTrackID(req); i >= 0 && i < len(c.Senders) { - // mark sender as SETUP - c.Senders[i].Media.ID = MethodSetup - interleaved := fmt.Sprintf("%d-%d", i*2, i*2+1) - - // Check if transport already contains the 'interleaved' parameter - if strings.Contains(transport, "interleaved=") { - // If so, just update the interleaved value - res.Header.Set("Transport", strings.Replace(transport, "interleaved=[^;]*", "interleaved="+interleaved, 1)) + if c.mode == core.ModePassiveConsumer { + if i := reqTrackID(req); i >= 0 && i < len(c.Senders) { + // mark sender as SETUP + c.Senders[i].Media.ID = MethodSetup + interleaved := fmt.Sprintf("%d-%d", i*2, i*2+1) + + // Check if transport already contains the 'interleaved' parameter + if strings.Contains(transport, "interleaved=") { + // If so, just update the interleaved value + res.Header.Set("Transport", strings.Replace(transport, "interleaved=[^;]*", "interleaved="+interleaved, 1)) + } else { + // Otherwise, append the interleaved parameter + res.Header.Set("Transport", transport+";interleaved="+interleaved) + } } else { - // Otherwise, append the interleaved parameter - res.Header.Set("Transport", transport+";interleaved="+interleaved) + res.Status = "400 Bad Request" } } else { - res.Status = "400 Bad Request" + res.Header.Set("Transport", tr) } - } else { - res.Header.Set("Transport", tr) + else { + res.Status = "461 Unsupported transport" } if err = c.WriteResponse(res); err != nil { From d881755503b4c30b338fd3924fd51cdeb1b460c7 Mon Sep 17 00:00:00 2001 From: fmcloudconsulting <170678386+fmcloudconsulting@users.noreply.github.com> Date: Tue, 17 Dec 2024 17:30:10 +0100 Subject: [PATCH 098/166] chore: lint --- pkg/rtsp/server.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/rtsp/server.go b/pkg/rtsp/server.go index f5967c5a..d9a40881 100644 --- a/pkg/rtsp/server.go +++ b/pkg/rtsp/server.go @@ -156,7 +156,7 @@ func (c *Conn) Accept() error { c.session = core.RandString(8, 10) c.state = StateSetup - + if c.mode == core.ModePassiveConsumer { if i := reqTrackID(req); i >= 0 && i < len(c.Senders) { // mark sender as SETUP From 4b80b2c233be089fb8ced21bc05f2c493cc9a3d9 Mon Sep 17 00:00:00 2001 From: fmcloudconsulting <170678386+fmcloudconsulting@users.noreply.github.com> Date: Tue, 17 Dec 2024 17:36:18 +0100 Subject: [PATCH 099/166] fix: typo --- pkg/rtsp/server.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/rtsp/server.go b/pkg/rtsp/server.go index d9a40881..88d24e27 100644 --- a/pkg/rtsp/server.go +++ b/pkg/rtsp/server.go @@ -177,7 +177,7 @@ func (c *Conn) Accept() error { } else { res.Header.Set("Transport", tr) } - else { + } else { res.Status = "461 Unsupported transport" } From 6fa352f407c3466edafeffab1e56b0e84740f2d3 Mon Sep 17 00:00:00 2001 From: fmcloudconsulting <170678386+fmcloudconsulting@users.noreply.github.com> Date: Tue, 17 Dec 2024 19:06:15 +0100 Subject: [PATCH 100/166] fix: don't require unicast param and fix typo (tr instead of transport) --- pkg/rtsp/server.go | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/pkg/rtsp/server.go b/pkg/rtsp/server.go index 88d24e27..cefaef1d 100644 --- a/pkg/rtsp/server.go +++ b/pkg/rtsp/server.go @@ -148,11 +148,9 @@ func (c *Conn) Accept() error { Request: req, } - const transport = "RTP/AVP/TCP;unicast" - - // Test if client requests unicast with TCP transport, otherwise return 461 Transport not supported + // Test if client requests TCP transport, otherwise return 461 Transport not supported // This allows smart clients who initially requested UDP to fall back on TCP transport. - if strings.HasPrefix(tr, transport) { + if strings.HasPrefix(tr, "RTP/AVP/TCP") { c.session = core.RandString(8, 10) c.state = StateSetup @@ -163,13 +161,13 @@ func (c *Conn) Accept() error { c.Senders[i].Media.ID = MethodSetup interleaved := fmt.Sprintf("%d-%d", i*2, i*2+1) - // Check if transport already contains the 'interleaved' parameter - if strings.Contains(transport, "interleaved=") { + // Check if tr already contains the 'interleaved' parameter + if strings.Contains(tr, "interleaved=") { // If so, just update the interleaved value - res.Header.Set("Transport", strings.Replace(transport, "interleaved=[^;]*", "interleaved="+interleaved, 1)) + res.Header.Set("Transport", strings.Replace(tr, "interleaved=[^;]*", "interleaved="+interleaved, 1)) } else { // Otherwise, append the interleaved parameter - res.Header.Set("Transport", transport+";interleaved="+interleaved) + res.Header.Set("Transport", tr+";interleaved="+interleaved) } } else { res.Status = "400 Bad Request" From 3a50b3678d132f301fe53de1be7ba8054665c4e4 Mon Sep 17 00:00:00 2001 From: Alex Cortelyou <1689668+acortelyou@users.noreply.github.com> Date: Mon, 23 Dec 2024 23:43:39 -0800 Subject: [PATCH 101/166] Extend onvif server to support Unifi Protect --- internal/onvif/init.go | 13 ++++++ pkg/onvif/server.go | 100 ++++++++++++++++++++++++++++++++++++----- 2 files changed, 103 insertions(+), 10 deletions(-) diff --git a/internal/onvif/init.go b/internal/onvif/init.go index 014c5e18..b8b4fca6 100644 --- a/internal/onvif/init.go +++ b/internal/onvif/init.go @@ -70,6 +70,10 @@ func onvifDeviceService(w http.ResponseWriter, r *http.Request) { // important for Hass: Media section res = onvif.GetCapabilitiesResponse(r.Host) + case onvif.ActionGetServices: + // important for Unifi: Media section + res = onvif.GetServicesResponse(r.Host) + case onvif.ActionGetSystemDateAndTime: // important for Hass res = onvif.GetSystemDateAndTimeResponse() @@ -95,8 +99,13 @@ func onvifDeviceService(w http.ResponseWriter, r *http.Request) { case onvif.ActionGetProfiles: // important for Hass: H264 codec, width, height + // important for Unifi: framerate, bitrate, quality res = onvif.GetProfilesResponse(streams.GetAll()) + case onvif.ActionGetVideoSources: + // important for Unifi: framerate, resolution + res = onvif.GetVideoSourcesResponse(streams.GetAll()) + case onvif.ActionGetStreamUri: host, _, err := net.SplitHostPort(r.Host) if err != nil { @@ -107,6 +116,10 @@ func onvifDeviceService(w http.ResponseWriter, r *http.Request) { uri := "rtsp://" + host + ":" + rtsp.Port + "/" + onvif.FindTagValue(b, "ProfileToken") res = onvif.GetStreamUriResponse(uri) + case onvif.ActionGetSnapshotUri: + uri := "http://" + r.Host + "/api/frame.jpeg?src=" + onvif.FindTagValue(b, "ProfileToken") + res = onvif.GetSnapshotUriResponse(uri) + default: http.Error(w, "unsupported action", http.StatusBadRequest) log.Debug().Msgf("[onvif] unsupported request:\n%s", b) diff --git a/pkg/onvif/server.go b/pkg/onvif/server.go index f8f2883c..df53dfab 100644 --- a/pkg/onvif/server.go +++ b/pkg/onvif/server.go @@ -16,6 +16,7 @@ const ( ActionGetServiceCapabilities = "GetServiceCapabilities" ActionGetProfiles = "GetProfiles" ActionGetStreamUri = "GetStreamUri" + ActionGetSnapshotUri = "GetSnapshotUri" ActionSystemReboot = "SystemReboot" ActionGetServices = "GetServices" @@ -65,6 +66,32 @@ func GetCapabilitiesResponse(host string) string { ` } +func GetServicesResponse(host string) string { + return ` + + + + + http://www.onvif.org/ver10/device/wsdl + http://` + host + `/onvif/device_service + + 2 + 5 + + + + http://www.onvif.org/ver10/media/wsdl + http://` + host + `/onvif/media_service + + 2 + 5 + + + + +` +} + func GetSystemDateAndTimeResponse() string { loc := time.Now() utc := loc.UTC() @@ -142,7 +169,7 @@ func GetServiceCapabilitiesResponse() string { - + @@ -171,14 +198,27 @@ func GetProfilesResponse(names []string) string { for i, name := range names { buf.WriteString(` - ` + name + ` - - H264 - - 1920 - 1080 - - + ` + name + ` + + ` + name + ` + H264 + + 1920 + 1080 + + + 29.97003 + 1 + 5000 + + 4 + PT1000S + + + ` + name + ` + ` + strconv.Itoa(i) + ` + + `) } @@ -190,15 +230,55 @@ func GetProfilesResponse(names []string) string { return buf.String() } + +func GetVideoSourcesResponse(names []string) string { + buf := bytes.NewBuffer(nil) + buf.WriteString(` + + + `) + + for i, _ := range names { + buf.WriteString(` + + 29.97003 + + 1920 + 1080 + + `) + } + + buf.WriteString(` + + +`) + + return buf.String() +} + func GetStreamUriResponse(uri string) string { return ` - ` + uri + ` + ` + uri + ` ` } + +func GetSnapshotUriResponse(uri string) string { + return ` + + + + + ` + uri + ` + + + +` +} From 159d9425a732eedef06a9dd797ec6a284339a1b6 Mon Sep 17 00:00:00 2001 From: Alex Cortelyou <1689668+acortelyou@users.noreply.github.com> Date: Tue, 24 Dec 2024 11:08:18 -0800 Subject: [PATCH 102/166] Remove non-essential fields --- pkg/onvif/server.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pkg/onvif/server.go b/pkg/onvif/server.go index df53dfab..e2d56556 100644 --- a/pkg/onvif/server.go +++ b/pkg/onvif/server.go @@ -208,11 +208,8 @@ func GetProfilesResponse(names []string) string { 29.97003 - 1 5000 - 4 - PT1000S ` + name + ` @@ -241,7 +238,6 @@ func GetVideoSourcesResponse(names []string) string { for i, _ := range names { buf.WriteString(` - 29.97003 1920 1080 From 261a936bb84a0e3ec82e94cb87c029a3edd0364d Mon Sep 17 00:00:00 2001 From: Xiaokui Shu Date: Wed, 25 Dec 2024 17:09:23 -0500 Subject: [PATCH 103/166] Add rtsp server failed auth logging --- internal/rtsp/rtsp.go | 6 +++++- pkg/rtsp/server.go | 10 +++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/internal/rtsp/rtsp.go b/internal/rtsp/rtsp.go index 0fe135f8..0777219d 100644 --- a/internal/rtsp/rtsp.go +++ b/internal/rtsp/rtsp.go @@ -1,6 +1,7 @@ package rtsp import ( + "fmt" "io" "net" "net/url" @@ -237,7 +238,10 @@ func tcpHandler(conn *rtsp.Conn) { }) if err := conn.Accept(); err != nil { - if err != io.EOF { + if err == rtsp.FailedAuth { + rAddr := conn.Connection.RemoteAddr + log.Warn().Msg(fmt.Sprintf("[rtsp] failed authentication from %s", rAddr)) + } else if err != io.EOF { log.WithLevel(level).Err(err).Caller().Send() } if closer != nil { diff --git a/pkg/rtsp/server.go b/pkg/rtsp/server.go index c96125a2..9527e155 100644 --- a/pkg/rtsp/server.go +++ b/pkg/rtsp/server.go @@ -13,6 +13,8 @@ import ( "github.com/AlexxIT/go2rtc/pkg/tcp" ) +var FailedAuth = errors.New("failed authentication") + func NewServer(conn net.Conn) *Conn { return &Conn{ Connection: core.Connection{ @@ -54,7 +56,13 @@ func (c *Conn) Accept() error { if err = c.WriteResponse(res); err != nil { return err } - continue + if req.Header.Get("Authorization") != "" { + // eliminate false positive: ffmpeg sends first request without + // authorization header even if the user provides credentials + return FailedAuth + } else { + continue + } } // Receiver: OPTIONS > DESCRIBE > SETUP... > PLAY > TEARDOWN From 0d6b8fc6fc207c5c89c3cf0923b0cbd38f04ad1b Mon Sep 17 00:00:00 2001 From: Alex X Date: Sun, 29 Dec 2024 11:44:56 +0300 Subject: [PATCH 104/166] Fix OPUS/48000/1 for RTSP from some cameras #1506 --- pkg/rtsp/helpers.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/pkg/rtsp/helpers.go b/pkg/rtsp/helpers.go index 6b07342d..346ecf73 100644 --- a/pkg/rtsp/helpers.go +++ b/pkg/rtsp/helpers.go @@ -70,8 +70,15 @@ func UnmarshalSDP(rawSDP []byte) ([]*core.Media, error) { // Check buggy SDP with fmtp for H264 on another track // https://github.com/AlexxIT/WebRTC/issues/419 for _, codec := range media.Codecs { - if codec.Name == core.CodecH264 && codec.FmtpLine == "" { - codec.FmtpLine = findFmtpLine(codec.PayloadType, sd.MediaDescriptions) + switch codec.Name { + case core.CodecH264: + if codec.FmtpLine == "" { + codec.FmtpLine = findFmtpLine(codec.PayloadType, sd.MediaDescriptions) + } + case core.CodecOpus: + // fix OPUS for some cameras https://datatracker.ietf.org/doc/html/rfc7587 + codec.ClockRate = 48000 + codec.Channels = 2 } } From a3f084dcde33b9fd341fb3ae00c0d7f6ca7210e9 Mon Sep 17 00:00:00 2001 From: Alex X Date: Sun, 29 Dec 2024 22:37:04 +0300 Subject: [PATCH 105/166] RTMP server enhancement to support OpenIPC cameras --- pkg/flv/producer.go | 42 +++++++++++++++++++++++++++--------------- pkg/rtmp/server.go | 13 +++++++------ 2 files changed, 34 insertions(+), 21 deletions(-) diff --git a/pkg/flv/producer.go b/pkg/flv/producer.go index 66755217..7535a8a4 100644 --- a/pkg/flv/producer.go +++ b/pkg/flv/producer.go @@ -140,23 +140,29 @@ func (c *Producer) probe() error { // 1. Empty video/audio flag // 2. MedaData without stereo key for AAC // 3. Audio header after Video keyframe tag - waitType := []byte{TagData} - timeout := time.Now().Add(core.ProbeTimeout) - for len(waitType) != 0 && time.Now().Before(timeout) { + // OpenIPC camera sends: + // 1. Empty video/audio flag + // 2. No MetaData packet + // 3. Sends a video packet in more than 3 seconds + waitVideo := true + waitAudio := true + timeout := time.Now().Add(time.Second * 5) + + for (waitVideo || waitAudio) && time.Now().Before(timeout) { pkt, err := c.readPacket() if err != nil { return err } - if i := bytes.IndexByte(waitType, pkt.PayloadType); i < 0 { - continue - } else { - waitType = append(waitType[:i], waitType[i+1:]...) - } + //log.Printf("%d %0.20s", pkt.PayloadType, pkt.Payload) switch pkt.PayloadType { case TagAudio: + if !waitAudio { + continue + } + _ = pkt.Payload[1] // bounds codecID := pkt.Payload[0] >> 4 // SoundFormat @@ -179,8 +185,13 @@ func (c *Producer) probe() error { Codecs: []*core.Codec{codec}, } c.Medias = append(c.Medias, media) + waitAudio = false case TagVideo: + if !waitVideo { + continue + } + var codec *core.Codec if isExHeader(pkt.Payload) { @@ -213,19 +224,20 @@ func (c *Producer) probe() error { Codecs: []*core.Codec{codec}, } c.Medias = append(c.Medias, media) + waitVideo = false case TagData: if !bytes.Contains(pkt.Payload, []byte("onMetaData")) { - waitType = append(waitType, TagData) + continue } // Dahua cameras doesn't send videocodecid - if bytes.Contains(pkt.Payload, []byte("videocodecid")) || - bytes.Contains(pkt.Payload, []byte("width")) || - bytes.Contains(pkt.Payload, []byte("framerate")) { - waitType = append(waitType, TagVideo) + if !bytes.Contains(pkt.Payload, []byte("videocodecid")) && + !bytes.Contains(pkt.Payload, []byte("width")) && + !bytes.Contains(pkt.Payload, []byte("framerate")) { + waitVideo = false } - if bytes.Contains(pkt.Payload, []byte("audiocodecid")) { - waitType = append(waitType, TagAudio) + if !bytes.Contains(pkt.Payload, []byte("audiocodecid")) { + waitAudio = false } } } diff --git a/pkg/rtmp/server.go b/pkg/rtmp/server.go index ed727b98..3dcd4048 100644 --- a/pkg/rtmp/server.go +++ b/pkg/rtmp/server.go @@ -117,10 +117,6 @@ func (c *Conn) acceptCommand(b []byte) error { } } - if c.App == "" { - return fmt.Errorf("rtmp: read command %x", b) - } - payload := amf.EncodeItems( "_result", tID, map[string]any{"fmsVer": "FMS/3,0,1,123"}, @@ -129,9 +125,16 @@ func (c *Conn) acceptCommand(b []byte) error { return c.writeMessage(3, TypeCommand, 0, payload) case CommandReleaseStream: + // if app is empty - will use key as app + if c.App == "" && len(items) == 4 { + c.App, _ = items[3].(string) + } + payload := amf.EncodeItems("_result", tID, nil) return c.writeMessage(3, TypeCommand, 0, payload) + case CommandFCPublish: // no response + case CommandCreateStream: payload := amf.EncodeItems("_result", tID, nil, 1) return c.writeMessage(3, TypeCommand, 0, payload) @@ -140,8 +143,6 @@ func (c *Conn) acceptCommand(b []byte) error { c.Intent = cmd c.streamID = 1 - case CommandFCPublish: // no response - default: println("rtmp: unknown command: " + cmd) } From b8303b9a22e1727b9a4db8979b6177f5e13dd35c Mon Sep 17 00:00:00 2001 From: Alex Cortelyou <1689668+acortelyou@users.noreply.github.com> Date: Sun, 29 Dec 2024 16:16:49 -0800 Subject: [PATCH 106/166] Remove optional fields, normalize indentation --- pkg/onvif/server.go | 66 ++++++++++++++++++++++----------------------- 1 file changed, 32 insertions(+), 34 deletions(-) diff --git a/pkg/onvif/server.go b/pkg/onvif/server.go index e2d56556..bc3f8ffe 100644 --- a/pkg/onvif/server.go +++ b/pkg/onvif/server.go @@ -46,23 +46,23 @@ func GetRequestAction(b []byte) string { func GetCapabilitiesResponse(host string) string { return ` - - - - - http://` + host + `/onvif/device_service - - - http://` + host + `/onvif/media_service - - false - false - true - - - - - + + + + + http://` + host + `/onvif/device_service + + + http://` + host + `/onvif/media_service + + false + false + true + + + + + ` } @@ -197,31 +197,29 @@ func GetProfilesResponse(names []string) string { for i, name := range names { buf.WriteString(` - - ` + name + ` - + + ` + name + ` + ` + name + ` - H264 - - 1920 + H264 + + 1920 1080 - - 29.97003 - 5000 + - + ` + name + ` ` + strconv.Itoa(i) + ` - `) + `) } buf.WriteString(` - - + + `) return buf.String() @@ -233,11 +231,11 @@ func GetVideoSourcesResponse(names []string) string { buf.WriteString(` - `) + `) for i, _ := range names { buf.WriteString(` - + 1920 1080 @@ -246,8 +244,8 @@ func GetVideoSourcesResponse(names []string) string { } buf.WriteString(` - - + + `) return buf.String() From cf88bf9c23e7196cec60dc62644f12b2f20d8083 Mon Sep 17 00:00:00 2001 From: Alex Cortelyou <1689668+acortelyou@users.noreply.github.com> Date: Sun, 29 Dec 2024 16:22:49 -0800 Subject: [PATCH 107/166] Remove inaccurate comments --- internal/onvif/init.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/internal/onvif/init.go b/internal/onvif/init.go index b8b4fca6..e5ed9a7c 100644 --- a/internal/onvif/init.go +++ b/internal/onvif/init.go @@ -71,7 +71,6 @@ func onvifDeviceService(w http.ResponseWriter, r *http.Request) { res = onvif.GetCapabilitiesResponse(r.Host) case onvif.ActionGetServices: - // important for Unifi: Media section res = onvif.GetServicesResponse(r.Host) case onvif.ActionGetSystemDateAndTime: @@ -99,11 +98,9 @@ func onvifDeviceService(w http.ResponseWriter, r *http.Request) { case onvif.ActionGetProfiles: // important for Hass: H264 codec, width, height - // important for Unifi: framerate, bitrate, quality res = onvif.GetProfilesResponse(streams.GetAll()) case onvif.ActionGetVideoSources: - // important for Unifi: framerate, resolution res = onvif.GetVideoSourcesResponse(streams.GetAll()) case onvif.ActionGetStreamUri: From f601c4721833eb9fbb45dba7874890cab01cf974 Mon Sep 17 00:00:00 2001 From: Alex X Date: Mon, 30 Dec 2024 22:34:08 +0300 Subject: [PATCH 108/166] Improve ONVIF server --- examples/onvif_client/main.go | 72 +++++ internal/onvif/README.md | 25 ++ internal/onvif/{init.go => onvif.go} | 80 ++--- pkg/onvif/README.md | 38 +++ pkg/onvif/client.go | 113 ++----- pkg/onvif/envelope.go | 79 +++++ pkg/onvif/helpers.go | 23 ++ pkg/onvif/server.go | 422 +++++++++++++-------------- 8 files changed, 506 insertions(+), 346 deletions(-) create mode 100644 examples/onvif_client/main.go create mode 100644 internal/onvif/README.md rename internal/onvif/{init.go => onvif.go} (67%) create mode 100644 pkg/onvif/README.md create mode 100644 pkg/onvif/envelope.go diff --git a/examples/onvif_client/main.go b/examples/onvif_client/main.go new file mode 100644 index 00000000..03dd12ba --- /dev/null +++ b/examples/onvif_client/main.go @@ -0,0 +1,72 @@ +package main + +import ( + "log" + "net" + "net/url" + "os" + + "github.com/AlexxIT/go2rtc/pkg/onvif" +) + +func main() { + var rawURL = os.Args[1] + var operation = os.Args[2] + var token string + if len(os.Args) > 3 { + token = os.Args[3] + } + + client, err := onvif.NewClient(rawURL) + if err != nil { + log.Panic(err) + } + + var b []byte + + switch operation { + case onvif.ServiceGetServiceCapabilities: + b, err = client.MediaRequest(operation) + case onvif.DeviceGetCapabilities, + onvif.DeviceGetDeviceInformation, + onvif.DeviceGetDiscoveryMode, + onvif.DeviceGetDNS, + onvif.DeviceGetHostname, + onvif.DeviceGetNetworkDefaultGateway, + onvif.DeviceGetNetworkInterfaces, + onvif.DeviceGetNetworkProtocols, + onvif.DeviceGetNTP, + onvif.DeviceGetScopes, + onvif.DeviceGetServices, + onvif.DeviceGetSystemDateAndTime, + onvif.DeviceSystemReboot: + b, err = client.DeviceRequest(operation) + case onvif.MediaGetProfiles, onvif.MediaGetVideoSources: + b, err = client.MediaRequest(operation) + case onvif.MediaGetProfile: + b, err = client.GetProfile(token) + case onvif.MediaGetVideoSourceConfiguration: + b, err = client.GetVideoSourceConfiguration(token) + case onvif.MediaGetStreamUri: + b, err = client.GetStreamUri(token) + case onvif.MediaGetSnapshotUri: + b, err = client.GetSnapshotUri(token) + default: + log.Printf("unknown action\n") + } + + if err != nil { + log.Printf("%s\n", err) + } + + u, err := url.Parse(rawURL) + if err != nil { + log.Fatal(err) + } + + host, _, _ := net.SplitHostPort(u.Host) + + if err = os.WriteFile(host+"_"+operation+".xml", b, 0644); err != nil { + log.Printf("%s\n", err) + } +} diff --git a/internal/onvif/README.md b/internal/onvif/README.md new file mode 100644 index 00000000..ee922fbf --- /dev/null +++ b/internal/onvif/README.md @@ -0,0 +1,25 @@ +# ONVIF + +A regular camera has a single video source (`GetVideoSources`) and two profiles (`GetProfiles`). + +Go2rtc has one video source and one profile per stream. + +## Tested clients + +Go2rtc works as ONVIF server: + +- Happytime onvif client (windows) +- Home Assistant ONVIF integration (linux) +- Onvier (android) +- ONVIF Device Manager (windows) + +PS. Support only TCP transport for RTSP protocol. UDP and HTTP transports - unsupported yet. + +## Tested cameras + +Go2rtc works as ONVIF client: + +- Dahua IPC-K42 +- OpenIPC +- Reolink RLC-520A +- TP-Link Tapo TC60 diff --git a/internal/onvif/init.go b/internal/onvif/onvif.go similarity index 67% rename from internal/onvif/init.go rename to internal/onvif/onvif.go index e5ed9a7c..d332ca38 100644 --- a/internal/onvif/init.go +++ b/internal/onvif/onvif.go @@ -55,55 +55,65 @@ func onvifDeviceService(w http.ResponseWriter, r *http.Request) { return } - action := onvif.GetRequestAction(b) - if action == "" { + operation := onvif.GetRequestAction(b) + if operation == "" { http.Error(w, "malformed request body", http.StatusBadRequest) return } - log.Trace().Msgf("[onvif] %s", action) + log.Trace().Msgf("[onvif] server request %s %s:\n%s", r.Method, r.RequestURI, b) - var res string + switch operation { + case onvif.DeviceGetNetworkInterfaces, // important for Hass + onvif.DeviceGetSystemDateAndTime, // important for Hass + onvif.DeviceGetDiscoveryMode, + onvif.DeviceGetDNS, + onvif.DeviceGetHostname, + onvif.DeviceGetNetworkDefaultGateway, + onvif.DeviceGetNetworkProtocols, + onvif.DeviceGetNTP, + onvif.DeviceGetScopes: + b = onvif.StaticResponse(operation) - switch action { - case onvif.ActionGetCapabilities: + case onvif.DeviceGetCapabilities: // important for Hass: Media section - res = onvif.GetCapabilitiesResponse(r.Host) + b = onvif.GetCapabilitiesResponse(r.Host) - case onvif.ActionGetServices: - res = onvif.GetServicesResponse(r.Host) + case onvif.DeviceGetServices: + b = onvif.GetServicesResponse(r.Host) - case onvif.ActionGetSystemDateAndTime: - // important for Hass - res = onvif.GetSystemDateAndTimeResponse() - - case onvif.ActionGetNetworkInterfaces: - // important for Hass: none - res = onvif.GetNetworkInterfacesResponse() - - case onvif.ActionGetDeviceInformation: + case onvif.DeviceGetDeviceInformation: // important for Hass: SerialNumber (unique server ID) - res = onvif.GetDeviceInformationResponse("", "go2rtc", app.Version, r.Host) + b = onvif.GetDeviceInformationResponse("", "go2rtc", app.Version, r.Host) - case onvif.ActionGetServiceCapabilities: + case onvif.ServiceGetServiceCapabilities: // important for Hass - res = onvif.GetServiceCapabilitiesResponse() + // TODO: check path links to media + b = onvif.GetMediaServiceCapabilitiesResponse() - case onvif.ActionSystemReboot: - res = onvif.SystemRebootResponse() + case onvif.DeviceSystemReboot: + b = onvif.StaticResponse(operation) time.AfterFunc(time.Second, func() { os.Exit(0) }) - case onvif.ActionGetProfiles: + case onvif.MediaGetVideoSources: + b = onvif.GetVideoSourcesResponse(streams.GetAll()) + + case onvif.MediaGetProfiles: // important for Hass: H264 codec, width, height - res = onvif.GetProfilesResponse(streams.GetAll()) + b = onvif.GetProfilesResponse(streams.GetAll()) - case onvif.ActionGetVideoSources: - res = onvif.GetVideoSourcesResponse(streams.GetAll()) + case onvif.MediaGetProfile: + token := onvif.FindTagValue(b, "ProfileToken") + b = onvif.GetProfileResponse(token) - case onvif.ActionGetStreamUri: + case onvif.MediaGetVideoSourceConfiguration: + token := onvif.FindTagValue(b, "ConfigurationToken") + b = onvif.GetVideoSourceConfigurationResponse(token) + + case onvif.MediaGetStreamUri: host, _, err := net.SplitHostPort(r.Host) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) @@ -111,20 +121,22 @@ func onvifDeviceService(w http.ResponseWriter, r *http.Request) { } uri := "rtsp://" + host + ":" + rtsp.Port + "/" + onvif.FindTagValue(b, "ProfileToken") - res = onvif.GetStreamUriResponse(uri) + b = onvif.GetStreamUriResponse(uri) - case onvif.ActionGetSnapshotUri: + case onvif.MediaGetSnapshotUri: uri := "http://" + r.Host + "/api/frame.jpeg?src=" + onvif.FindTagValue(b, "ProfileToken") - res = onvif.GetSnapshotUriResponse(uri) + b = onvif.GetSnapshotUriResponse(uri) default: - http.Error(w, "unsupported action", http.StatusBadRequest) + http.Error(w, "unsupported operation", http.StatusBadRequest) log.Debug().Msgf("[onvif] unsupported request:\n%s", b) return } + log.Trace().Msgf("[onvif] server response:\n%s", b) + w.Header().Set("Content-Type", "application/soap+xml; charset=utf-8") - if _, err = w.Write([]byte(res)); err != nil { + if _, err = w.Write(b); err != nil { log.Error().Err(err).Caller().Send() } } @@ -170,7 +182,7 @@ func apiOnvif(w http.ResponseWriter, r *http.Request) { } if l := log.Trace(); l.Enabled() { - b, _ := client.GetProfiles() + b, _ := client.MediaRequest(onvif.MediaGetProfiles) l.Msgf("[onvif] src=%s profiles:\n%s", src, b) } diff --git a/pkg/onvif/README.md b/pkg/onvif/README.md new file mode 100644 index 00000000..73267379 --- /dev/null +++ b/pkg/onvif/README.md @@ -0,0 +1,38 @@ +## Profiles + +- Profile A - For access control configuration +- Profile C - For door control and event management +- Profile S - For basic video streaming + - Video streaming and configuration +- Profile T - For advanced video streaming + - H.264 / H.265 video compression + - Imaging settings + - Motion alarm and tampering events + - Metadata streaming + - Bi-directional audio + +## Services + +https://www.onvif.org/profiles/specifications/ + +- https://www.onvif.org/ver10/device/wsdl/devicemgmt.wsdl +- https://www.onvif.org/ver20/imaging/wsdl/imaging.wsdl +- https://www.onvif.org/ver10/media/wsdl/media.wsdl + +## TMP + +| | Dahua | Reolink | TP-Link | +|------------------------|---------|---------|---------| +| GetCapabilities | no auth | no auth | no auth | +| GetServices | no auth | no auth | no auth | +| GetServiceCapabilities | no auth | no auth | auth | +| GetSystemDateAndTime | no auth | no auth | no auth | +| GetNetworkInterfaces | auth | auth | auth | +| GetDeviceInformation | auth | auth | auth | +| GetProfiles | auth | auth | auth | +| GetScopes | auth | auth | auth | + +- Dahua - onvif://192.168.10.90:80 +- Reolink - onvif://192.168.10.92:8000 +- TP-Link - onvif://192.168.10.91:2020/onvif/device_service +- \ No newline at end of file diff --git a/pkg/onvif/client.go b/pkg/onvif/client.go index 97bfd8dc..cb6221e1 100644 --- a/pkg/onvif/client.go +++ b/pkg/onvif/client.go @@ -2,8 +2,6 @@ package onvif import ( "bytes" - "crypto/sha1" - "encoding/base64" "errors" "html" "io" @@ -12,8 +10,6 @@ import ( "regexp" "strings" "time" - - "github.com/AlexxIT/go2rtc/pkg/core" ) const PathDevice = "/onvif/device_service" @@ -41,7 +37,7 @@ func NewClient(rawURL string) (*Client, error) { client.deviceURL = baseURL + u.Path } - b, err := client.GetCapabilities() + b, err := client.DeviceRequest(DeviceGetCapabilities) if err != nil { return nil, err } @@ -95,7 +91,7 @@ func (c *Client) GetURI() (string, error) { } func (c *Client) GetName() (string, error) { - b, err := c.GetDeviceInformation() + b, err := c.DeviceRequest(DeviceGetDeviceInformation) if err != nil { return "", err } @@ -104,7 +100,7 @@ func (c *Client) GetName() (string, error) { } func (c *Client) GetProfilesTokens() ([]string, error) { - b, err := c.GetProfiles() + b, err := c.MediaRequest(MediaGetProfiles) if err != nil { return nil, err } @@ -127,86 +123,53 @@ func (c *Client) HasSnapshots() bool { return strings.Contains(string(b), `SnapshotUri="true"`) } -func (c *Client) GetCapabilities() ([]byte, error) { +func (c *Client) GetProfile(token string) ([]byte, error) { return c.Request( - c.deviceURL, - ` - All -`, + c.mediaURL, ``+token+``, ) } -func (c *Client) GetNetworkInterfaces() ([]byte, error) { - return c.Request( - c.deviceURL, ``, - ) -} - -func (c *Client) GetDeviceInformation() ([]byte, error) { - return c.Request( - c.deviceURL, ``, - ) -} - -func (c *Client) GetProfiles() ([]byte, error) { - return c.Request( - c.mediaURL, ``, - ) +func (c *Client) GetVideoSourceConfiguration(token string) ([]byte, error) { + return c.Request(c.mediaURL, ` + `+token+` +`) } func (c *Client) GetStreamUri(token string) ([]byte, error) { - return c.Request( - c.mediaURL, - ` + return c.Request(c.mediaURL, ` RTP-Unicast RTSP `+token+` -`, - ) +`) } func (c *Client) GetSnapshotUri(token string) ([]byte, error) { return c.Request( - c.imaginURL, - ` - `+token+` -`, - ) -} - -func (c *Client) GetSystemDateAndTime() ([]byte, error) { - return c.Request( - c.deviceURL, ``, + c.imaginURL, ``+token+``, ) } func (c *Client) GetServiceCapabilities() ([]byte, error) { // some cameras answer GetServiceCapabilities for media only for path = "/onvif/media" return c.Request( - c.mediaURL, ``, + c.mediaURL, ``, ) } -func (c *Client) SystemReboot() ([]byte, error) { - return c.Request( - c.deviceURL, ``, - ) +func (c *Client) DeviceRequest(operation string) ([]byte, error) { + if operation == DeviceGetServices { + operation = `true` + } else { + operation = `` + } + return c.Request(c.deviceURL, operation) } -func (c *Client) GetServices() ([]byte, error) { - return c.Request( - c.deviceURL, ` - true -`, - ) -} - -func (c *Client) GetScopes() ([]byte, error) { - return c.Request( - c.deviceURL, ``, - ) +func (c *Client) MediaRequest(operation string) ([]byte, error) { + operation = `` + return c.Request(c.mediaURL, operation) } func (c *Client) Request(url, body string) ([]byte, error) { @@ -214,35 +177,11 @@ func (c *Client) Request(url, body string) ([]byte, error) { return nil, errors.New("onvif: unsupported service") } - buf := bytes.NewBuffer(nil) - buf.WriteString( - ``, - ) - - if user := c.url.User; user != nil { - nonce := core.RandString(16, 36) - created := time.Now().UTC().Format(time.RFC3339Nano) - pass, _ := user.Password() - - h := sha1.New() - h.Write([]byte(nonce + created + pass)) - - buf.WriteString(` - - -` + user.Username() + ` -` + base64.StdEncoding.EncodeToString(h.Sum(nil)) + ` -` + base64.StdEncoding.EncodeToString([]byte(nonce)) + ` -` + created + ` - - -`) - } - - buf.WriteString(`` + body + ``) + e := NewEnvelopeWithUser(c.url.User) + e.Append(body) client := &http.Client{Timeout: time.Second * 5000} - res, err := client.Post(url, `application/soap+xml;charset=utf-8`, buf) + res, err := client.Post(url, `application/soap+xml;charset=utf-8`, bytes.NewReader(e.Bytes())) if err != nil { return nil, err } diff --git a/pkg/onvif/envelope.go b/pkg/onvif/envelope.go new file mode 100644 index 00000000..f0e1b29c --- /dev/null +++ b/pkg/onvif/envelope.go @@ -0,0 +1,79 @@ +package onvif + +import ( + "crypto/sha1" + "encoding/base64" + "fmt" + "net/url" + "time" + + "github.com/AlexxIT/go2rtc/pkg/core" +) + +type Envelope struct { + buf []byte +} + +const ( + prefix1 = ` + +` + prefix2 = ` +` + suffix = ` + +` +) + +func NewEnvelope() *Envelope { + e := &Envelope{buf: make([]byte, 0, 1024)} + e.Append(prefix1, prefix2) + return e +} + +func NewEnvelopeWithUser(user *url.Userinfo) *Envelope { + if user == nil { + return NewEnvelope() + } + + nonce := core.RandString(16, 36) + created := time.Now().UTC().Format(time.RFC3339Nano) + pass, _ := user.Password() + + h := sha1.New() + h.Write([]byte(nonce + created + pass)) + + e := &Envelope{buf: make([]byte, 0, 1024)} + e.Append(prefix1) + e.Appendf(` + + + %s + %s + %s + %s + + + +`, + user.Username(), + base64.StdEncoding.EncodeToString(h.Sum(nil)), + base64.StdEncoding.EncodeToString([]byte(nonce)), + created) + e.Append(prefix2) + return e +} + +func (e *Envelope) Append(args ...string) { + for _, s := range args { + e.buf = append(e.buf, s...) + } +} + +func (e *Envelope) Appendf(format string, args ...any) { + e.buf = fmt.Appendf(e.buf, format, args...) +} + +func (e *Envelope) Bytes() []byte { + return append(e.buf, suffix...) +} diff --git a/pkg/onvif/helpers.go b/pkg/onvif/helpers.go index fc9c8392..251f4579 100644 --- a/pkg/onvif/helpers.go +++ b/pkg/onvif/helpers.go @@ -1,6 +1,7 @@ package onvif import ( + "fmt" "net" "regexp" "strconv" @@ -106,3 +107,25 @@ func atoi(s string) int { } return i } + +func GetPosixTZ(current time.Time) string { + // Thanks to https://github.com/Path-Variable/go-posix-time + _, offset := current.Zone() + + if current.IsDST() { + _, end := current.ZoneBounds() + endPlus1 := end.Add(time.Hour * 25) + _, offset = endPlus1.Zone() + } + + var prefix string + if offset < 0 { + prefix = "GMT+" + offset = -offset / 60 + } else { + prefix = "GMT-" + offset = offset / 60 + } + + return prefix + fmt.Sprintf("%02d:%02d", offset/60, offset%60) +} diff --git a/pkg/onvif/server.go b/pkg/onvif/server.go index bc3f8ffe..42343d37 100644 --- a/pkg/onvif/server.go +++ b/pkg/onvif/server.go @@ -2,31 +2,40 @@ package onvif import ( "bytes" - "fmt" "regexp" - "strconv" "time" ) -const ( - ActionGetCapabilities = "GetCapabilities" - ActionGetSystemDateAndTime = "GetSystemDateAndTime" - ActionGetNetworkInterfaces = "GetNetworkInterfaces" - ActionGetDeviceInformation = "GetDeviceInformation" - ActionGetServiceCapabilities = "GetServiceCapabilities" - ActionGetProfiles = "GetProfiles" - ActionGetStreamUri = "GetStreamUri" - ActionGetSnapshotUri = "GetSnapshotUri" - ActionSystemReboot = "SystemReboot" +const ServiceGetServiceCapabilities = "GetServiceCapabilities" - ActionGetServices = "GetServices" - ActionGetScopes = "GetScopes" - ActionGetVideoSources = "GetVideoSources" - ActionGetAudioSources = "GetAudioSources" - ActionGetVideoSourceConfigurations = "GetVideoSourceConfigurations" - ActionGetAudioSourceConfigurations = "GetAudioSourceConfigurations" - ActionGetVideoEncoderConfigurations = "GetVideoEncoderConfigurations" - ActionGetAudioEncoderConfigurations = "GetAudioEncoderConfigurations" +const ( + DeviceGetCapabilities = "GetCapabilities" + DeviceGetDeviceInformation = "GetDeviceInformation" + DeviceGetDiscoveryMode = "GetDiscoveryMode" + DeviceGetDNS = "GetDNS" + DeviceGetHostname = "GetHostname" + DeviceGetNetworkDefaultGateway = "GetNetworkDefaultGateway" + DeviceGetNetworkInterfaces = "GetNetworkInterfaces" + DeviceGetNetworkProtocols = "GetNetworkProtocols" + DeviceGetNTP = "GetNTP" + DeviceGetScopes = "GetScopes" + DeviceGetServices = "GetServices" + DeviceGetSystemDateAndTime = "GetSystemDateAndTime" + DeviceSystemReboot = "SystemReboot" +) + +const ( + MediaGetAudioEncoderConfigurations = "GetAudioEncoderConfigurations" + MediaGetAudioSources = "GetAudioSources" + MediaGetAudioSourceConfigurations = "GetAudioSourceConfigurations" + MediaGetProfile = "GetProfile" + MediaGetProfiles = "GetProfiles" + MediaGetSnapshotUri = "GetSnapshotUri" + MediaGetStreamUri = "GetStreamUri" + MediaGetVideoEncoderConfigurations = "GetVideoEncoderConfigurations" + MediaGetVideoSources = "GetVideoSources" + MediaGetVideoSourceConfiguration = "GetVideoSourceConfiguration" + MediaGetVideoSourceConfigurations = "GetVideoSourceConfigurations" ) func GetRequestAction(b []byte) string { @@ -43,236 +52,199 @@ func GetRequestAction(b []byte) string { return string(m[1]) } -func GetCapabilitiesResponse(host string) string { - return ` - - - - - - http://` + host + `/onvif/device_service - - - http://` + host + `/onvif/media_service - - false - false - true - - - - - -` +func GetCapabilitiesResponse(host string) []byte { + e := NewEnvelope() + e.Append(` + + + http://`, host, `/onvif/device_service + + + http://`, host, `/onvif/media_service + + false + false + true + + + +`) + return e.Bytes() } -func GetServicesResponse(host string) string { - return ` - - - - - http://www.onvif.org/ver10/device/wsdl - http://` + host + `/onvif/device_service - - 2 - 5 - - - - http://www.onvif.org/ver10/media/wsdl - http://` + host + `/onvif/media_service - - 2 - 5 - - - - -` +func GetServicesResponse(host string) []byte { + e := NewEnvelope() + e.Append(` + + http://www.onvif.org/ver10/device/wsdl + http://`, host, `/onvif/device_service + 25 + + + http://www.onvif.org/ver10/media/wsdl + http://`, host, `/onvif/media_service + 25 + +`) + return e.Bytes() } -func GetSystemDateAndTimeResponse() string { +func GetSystemDateAndTimeResponse() []byte { loc := time.Now() utc := loc.UTC() - return fmt.Sprintf(` - - - - - NTP - false - - GMT%s - - - - %d - %d - %d - - - %d - %d - %d - - - - - %d - %d - %d - - - %d - %d - %d - - - - - -`, - loc.Format("-07:00"), + e := NewEnvelope() + e.Appendf(` + + NTP + true + + %s + + + %d%d%d + %d%d%d + + + %d%d%d + %d%d%d + + +`, + GetPosixTZ(loc), utc.Hour(), utc.Minute(), utc.Second(), utc.Year(), utc.Month(), utc.Day(), loc.Hour(), loc.Minute(), loc.Second(), loc.Year(), loc.Month(), loc.Day(), ) + return e.Bytes() } -func GetNetworkInterfacesResponse() string { - return ` - - - - -` +func GetDeviceInformationResponse(manuf, model, firmware, serial string) []byte { + e := NewEnvelope() + e.Append(` + `, manuf, ` + `, model, ` + `, firmware, ` + `, serial, ` + 1.00 +`) + return e.Bytes() } -func GetDeviceInformationResponse(manuf, model, firmware, serial string) string { - return ` - - - - ` + manuf + ` - ` + model + ` - ` + firmware + ` - ` + serial + ` - 1.00 - - -` +func GetMediaServiceCapabilitiesResponse() []byte { + e := NewEnvelope() + e.Append(` + + + +`) + return e.Bytes() } -func GetServiceCapabilitiesResponse() string { - return ` - - - - - - - - -` +func GetProfilesResponse(names []string) []byte { + e := NewEnvelope() + e.Append(` +`) + for _, name := range names { + appendProfile(e, "Profiles", name) + } + e.Append(``) + return e.Bytes() } -func SystemRebootResponse() string { - return ` - - - - system reboot in 1 second... - - -` +func GetProfileResponse(name string) []byte { + e := NewEnvelope() + e.Append(` +`) + appendProfile(e, "Profile", name) + e.Append(``) + return e.Bytes() } -func GetProfilesResponse(names []string) string { - buf := bytes.NewBuffer(nil) - buf.WriteString(` - - - `) +func appendProfile(e *Envelope, tag, name string) { + e.Append(` + `, name, ` + + VSC + `, name, ` + + + + VEC + H264 + 19201080 + + +`) +} - for i, name := range names { - buf.WriteString(` - - ` + name + ` - - ` + name + ` - H264 - - 1920 - 1080 - - - - - - ` + name + ` - ` + strconv.Itoa(i) + ` - - - `) +func GetVideoSourceConfigurationResponse(name string) []byte { + e := NewEnvelope() + e.Append(` + + VSC + `, name, ` + + +`) + return e.Bytes() +} + +func GetVideoSourcesResponse(names []string) []byte { + e := NewEnvelope() + e.Append(` +`) + for _, name := range names { + e.Append(` + 30.000000 + 19201080 + +`) + } + e.Append(``) + return e.Bytes() +} + +func GetStreamUriResponse(uri string) []byte { + e := NewEnvelope() + e.Append(``, uri, ``) + return e.Bytes() +} + +func GetSnapshotUriResponse(uri string) []byte { + e := NewEnvelope() + e.Append(``, uri, ``) + return e.Bytes() +} + +func StaticResponse(operation string) []byte { + switch operation { + case DeviceGetSystemDateAndTime: + return GetSystemDateAndTimeResponse() } - buf.WriteString(` - - -`) - - return buf.String() -} - - -func GetVideoSourcesResponse(names []string) string { - buf := bytes.NewBuffer(nil) - buf.WriteString(` - - - `) - - for i, _ := range names { - buf.WriteString(` - - - 1920 - 1080 - - `) + e := NewEnvelope() + e.Append(responses[operation]) + b := e.Bytes() + if operation == DeviceGetNetworkInterfaces { + println() } - - buf.WriteString(` - - -`) - - return buf.String() + return b } -func GetStreamUriResponse(uri string) string { - return ` - - - - - ` + uri + ` - - - -` -} +var responses = map[string]string{ + DeviceGetDiscoveryMode: `Discoverable`, + DeviceGetDNS: ``, + DeviceGetHostname: ``, + DeviceGetNetworkDefaultGateway: ``, + DeviceGetNTP: ``, + DeviceSystemReboot: `OK`, -func GetSnapshotUriResponse(uri string) string { - return ` - - - - - ` + uri + ` - - - -` + DeviceGetNetworkInterfaces: ``, + DeviceGetNetworkProtocols: ``, + DeviceGetScopes: ` + Fixedonvif://www.onvif.org/name/go2rtc + Fixedonvif://www.onvif.org/location/github + Fixedonvif://www.onvif.org/Profile/Streaming + Fixedonvif://www.onvif.org/type/Network_Video_Transmitter +`, } From bc9194d74092fb9c4de4d9162bf7f9558fdec996 Mon Sep 17 00:00:00 2001 From: Alex X Date: Fri, 3 Jan 2025 13:33:12 +0300 Subject: [PATCH 109/166] Update go dependencies --- go.mod | 27 ++++++++++++++------------- go.sum | 55 ++++++++++++++++++++++++++++++++----------------------- 2 files changed, 46 insertions(+), 36 deletions(-) diff --git a/go.mod b/go.mod index ecd32f3a..5f0a193b 100644 --- a/go.mod +++ b/go.mod @@ -8,20 +8,20 @@ require ( github.com/gorilla/websocket v1.5.3 github.com/mattn/go-isatty v0.0.20 github.com/miekg/dns v1.1.62 - github.com/pion/ice/v2 v2.3.36 + github.com/pion/ice/v2 v2.3.37 github.com/pion/interceptor v0.1.37 - github.com/pion/rtcp v1.2.14 - github.com/pion/rtp v1.8.9 + github.com/pion/rtcp v1.2.15 + github.com/pion/rtp v1.8.10 github.com/pion/sdp/v3 v3.0.9 github.com/pion/srtp/v2 v2.0.20 github.com/pion/stun v0.6.1 - github.com/pion/webrtc/v3 v3.3.4 + github.com/pion/webrtc/v3 v3.3.5 github.com/rs/zerolog v1.33.0 github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1 github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f - github.com/stretchr/testify v1.9.0 + github.com/stretchr/testify v1.10.0 github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9 - golang.org/x/crypto v0.28.0 + golang.org/x/crypto v0.31.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -31,19 +31,20 @@ require ( github.com/google/uuid v1.6.0 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/pion/datachannel v1.5.9 // indirect + github.com/pion/datachannel v1.5.10 // indirect github.com/pion/dtls/v2 v2.2.12 // indirect github.com/pion/logging v0.2.2 // indirect github.com/pion/mdns v0.0.12 // indirect github.com/pion/randutil v0.1.0 // indirect - github.com/pion/sctp v1.8.33 // indirect + github.com/pion/sctp v1.8.35 // indirect github.com/pion/transport/v2 v2.2.10 // indirect + github.com/pion/transport/v3 v3.0.7 // indirect github.com/pion/turn/v2 v2.1.6 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/wlynxg/anet v0.0.5 // indirect - golang.org/x/mod v0.18.0 // indirect - golang.org/x/net v0.27.0 // indirect - golang.org/x/sync v0.7.0 // indirect - golang.org/x/sys v0.26.0 // indirect - golang.org/x/tools v0.22.0 // indirect + golang.org/x/mod v0.20.0 // indirect + golang.org/x/net v0.33.0 // indirect + golang.org/x/sync v0.10.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/tools v0.24.0 // indirect ) diff --git a/go.sum b/go.sum index 804ecc43..c75ffced 100644 --- a/go.sum +++ b/go.sum @@ -31,13 +31,13 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ= github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ= -github.com/pion/datachannel v1.5.9 h1:LpIWAOYPyDrXtU+BW7X0Yt/vGtYxtXQ8ql7dFfYUVZA= -github.com/pion/datachannel v1.5.9/go.mod h1:kDUuk4CU4Uxp82NH4LQZbISULkX/HtzKa4P7ldf9izE= +github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o= +github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M= github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= github.com/pion/dtls/v2 v2.2.12 h1:KP7H5/c1EiVAAKUmXyCzPiQe5+bCJrpOeKg/L05dunk= github.com/pion/dtls/v2 v2.2.12/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE= -github.com/pion/ice/v2 v2.3.36 h1:SopeXiVbbcooUg2EIR8sq4b13RQ8gzrkkldOVg+bBsc= -github.com/pion/ice/v2 v2.3.36/go.mod h1:mBF7lnigdqgtB+YHkaY/Y6s6tsyRyo4u4rPGRuOjUBQ= +github.com/pion/ice/v2 v2.3.37 h1:ObIdaNDu1rCo7hObhs34YSBcO7fjslJMZV0ux+uZWh0= +github.com/pion/ice/v2 v2.3.37/go.mod h1:mBF7lnigdqgtB+YHkaY/Y6s6tsyRyo4u4rPGRuOjUBQ= github.com/pion/interceptor v0.1.37 h1:aRA8Zpab/wE7/c0O3fh1PqY0AJI3fCSEM5lRWJVorwI= github.com/pion/interceptor v0.1.37/go.mod h1:JzxbJ4umVTlZAf+/utHzNesY8tmRkM2lVmkS82TTj8Y= github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= @@ -47,13 +47,13 @@ github.com/pion/mdns v0.0.12/go.mod h1:VExJjv8to/6Wqm1FXK+Ii/Z9tsVk/F5sD/N70cnYF 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.12/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4= -github.com/pion/rtcp v1.2.14 h1:KCkGV3vJ+4DAJmvP0vaQShsb0xkRfWkO540Gy102KyE= -github.com/pion/rtcp v1.2.14/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4= +github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo= +github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0= github.com/pion/rtp v1.8.3/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU= -github.com/pion/rtp v1.8.9 h1:E2HX740TZKaqdcPmf4pw6ZZuG8u5RlMMt+l3dxeu6Wk= -github.com/pion/rtp v1.8.9/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU= -github.com/pion/sctp v1.8.33 h1:dSE4wX6uTJBcNm8+YlMg7lw1wqyKHggsP5uKbdj+NZw= -github.com/pion/sctp v1.8.33/go.mod h1:beTnqSzewI53KWoG3nqB282oDMGrhNxBdb+JZnkCwRM= +github.com/pion/rtp v1.8.10 h1:puphjdbjPB+L+NFaVuZ5h6bt1g5q4kFIoI+r5q/g0CU= +github.com/pion/rtp v1.8.10/go.mod h1:8uMBJj32Pa1wwx8Fuv/AsFhn8jsgw+3rUC2PfoBZ8p4= +github.com/pion/sctp v1.8.35 h1:qwtKvNK1Wc5tHMIYgTDJhfZk7vATGVHhXbUDfHbYwzA= +github.com/pion/sctp v1.8.35/go.mod h1:EcXP8zCYVTRy3W9xtOF7wJm1L1aXfKRQzaM33SjQlzg= github.com/pion/sdp/v3 v3.0.9 h1:pX++dCHoHUwq43kuwf3PyJfHlwIj4hXA7Vrifiq0IJY= github.com/pion/sdp/v3 v3.0.9/go.mod h1:B5xmvENq5IXJimIO4zfp6LAe1fD9N+kFv+V/1lOdz8M= github.com/pion/srtp/v2 v2.0.20 h1:HNNny4s+OUmG280ETrCdgFndp4ufx3/uy85EawYEhTk= @@ -67,11 +67,12 @@ github.com/pion/transport/v2 v2.2.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQp github.com/pion/transport/v2 v2.2.10/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E= github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0= github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0= +github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo= github.com/pion/turn/v2 v2.1.3/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY= github.com/pion/turn/v2 v2.1.6 h1:Xr2niVsiPTB0FPtt+yAWKFUkU1eotQbGgpTIld4x1Gc= github.com/pion/turn/v2 v2.1.6/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY= -github.com/pion/webrtc/v3 v3.3.4 h1:v2heQVnXTSqNRXcaFQVOhIOYkLMxOu1iJG8uy1djvkk= -github.com/pion/webrtc/v3 v3.3.4/go.mod h1:liNa+E1iwyzyXqNUwvoMRNQ10x8h8FOeJKL8RkIbamE= +github.com/pion/webrtc/v3 v3.3.5 h1:ZsSzaMz/i9nblPdiAkZoP+E6Kmjw+jnyq3bEmU3EtRg= +github.com/pion/webrtc/v3 v3.3.5/go.mod h1:liNa+E1iwyzyXqNUwvoMRNQ10x8h8FOeJKL8RkIbamE= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/profile v1.4.0/go.mod h1:NWz/XGvpEW1FyYQ7fCx4dqYBLlfTcE+A9FLAkNKqjFE= @@ -95,8 +96,9 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9 h1:aeN+ghOV0b2VCmKKO3gqnDQ8mLbpABZgRR2FVYx4ouI= github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9/go.mod h1:roo6cZ/uqpwKMuvPG0YmzI5+AmUiMWfjCBZpGXqbTxE= github.com/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= @@ -108,12 +110,13 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= -golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= -golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= 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/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= -golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0= +golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= @@ -122,13 +125,13 @@ 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.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= -golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= -golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/sync v0.0.0-20190423024810-112230192c58/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/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -143,8 +146,8 @@ golang.org/x/sys v0.9.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/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= -golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.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.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -165,6 +168,12 @@ 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/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= +golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg= +golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI= +golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= +golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= +golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE= +golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= From 4035e916723e1bbde5711b88f84e01faa5b1e60c Mon Sep 17 00:00:00 2001 From: Alex X Date: Fri, 3 Jan 2025 15:08:38 +0300 Subject: [PATCH 110/166] Fix ONVIF XML tag parsing in some cases --- pkg/onvif/helpers.go | 2 +- pkg/onvif/onvif_test.go | 28 ++++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/pkg/onvif/helpers.go b/pkg/onvif/helpers.go index 251f4579..f240f2ec 100644 --- a/pkg/onvif/helpers.go +++ b/pkg/onvif/helpers.go @@ -12,7 +12,7 @@ import ( ) func FindTagValue(b []byte, tag string) string { - re := regexp.MustCompile(`(?s)[:<]` + tag + `>([^<]+)`) + re := regexp.MustCompile(`(?s)<(?:\w+:)?` + tag + `\b[^>]*>([^<]+)`) m := re.FindSubmatch(b) if len(m) != 2 { return "" diff --git a/pkg/onvif/onvif_test.go b/pkg/onvif/onvif_test.go index cd57d60b..e9ffab04 100644 --- a/pkg/onvif/onvif_test.go +++ b/pkg/onvif/onvif_test.go @@ -84,6 +84,34 @@ func TestGetStreamUri(t *testing.T) { `, url: "rtsp://192.168.5.53:8090/profile1=r", }, + { + name: "go2rtc 1.9.4", + xml: ` + + + + rtsp://192.168.1.123:8554/rtsp-dahua1 + + + +`, + url: "rtsp://192.168.1.123:8554/rtsp-dahua1", + }, + { + name: "go2rtc 1.9.8", + xml: ` + + + + + rtsp://192.168.1.123:8554/rtsp-dahua2 + + + + +`, + url: "rtsp://192.168.1.123:8554/rtsp-dahua2", + }, } for _, test := range tests { From 199fdd6728eb932d3a34e5cebdfc00326a44ced8 Mon Sep 17 00:00:00 2001 From: Alex X Date: Fri, 3 Jan 2025 16:24:31 +0300 Subject: [PATCH 111/166] Update version to 1.9.8 --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index d5c59ffc..b8d58b27 100644 --- a/main.go +++ b/main.go @@ -37,7 +37,7 @@ import ( ) func main() { - app.Version = "1.9.7" + app.Version = "1.9.8" // 1. Core modules: app, api/ws, streams From 55af09a3502ad72b3909df637b03740321d7c404 Mon Sep 17 00:00:00 2001 From: Alex X Date: Sun, 5 Jan 2025 11:03:44 +0300 Subject: [PATCH 112/166] Add support fix JPEG from some MJPEG sources --- pkg/mjpeg/helpers.go | 63 +++++++++++++++++++++++++++++++++++--------- pkg/mjpeg/jpeg.go | 10 +++++++ pkg/mjpeg/rfc2435.go | 30 ++++++++++----------- 3 files changed, 75 insertions(+), 28 deletions(-) create mode 100644 pkg/mjpeg/jpeg.go diff --git a/pkg/mjpeg/helpers.go b/pkg/mjpeg/helpers.go index 08b4408b..d1acbd45 100644 --- a/pkg/mjpeg/helpers.go +++ b/pkg/mjpeg/helpers.go @@ -9,24 +9,38 @@ import ( "github.com/pion/rtp" ) -// FixJPEG - reencode JPEG if it has wrong header -// -// for example, this app produce "bad" images: -// https://github.com/jacksonliam/mjpg-streamer -// -// and they can't be uploaded to the Telegram servers: -// {"ok":false,"error_code":400,"description":"Bad Request: IMAGE_PROCESS_FAILED"} func FixJPEG(b []byte) []byte { // skip non-JPEG - if len(b) < 10 || b[0] != 0xFF || b[1] != 0xD8 { - return b - } - // skip if header OK for imghdr library - // https://docs.python.org/3/library/imghdr.html - if string(b[2:4]) == "\xFF\xDB" || string(b[6:10]) == "JFIF" || string(b[6:10]) == "Exif" { + if len(b) < 10 || b[0] != 0xFF || b[1] != markerSOI { return b } + // skip JPEG without app marker + if b[2] == 0xFF && b[3] == markerDQT { + return b + } + + switch string(b[6:10]) { + case "JFIF", "Exif": + // skip if header OK for imghdr library + // - https://docs.python.org/3/library/imghdr.html + return b + case "AVI1": + // adds DHT tables to JPEG file before SOS marker + // useful when you want to save a JPEG frame from an MJPEG stream + // - https://github.com/image-rs/jpeg-decoder/issues/76 + // - https://github.com/pion/mediadevices/pull/493 + // - https://bugzilla.mozilla.org/show_bug.cgi?id=963907#c18 + return InjectDHT(b) + } + + // reencode JPEG if it has wrong header + // + // for example, this app produce "bad" images: + // https://github.com/jacksonliam/mjpg-streamer + // + // and they can't be uploaded to the Telegram servers: + // {"ok":false,"error_code":400,"description":"Bad Request: IMAGE_PROCESS_FAILED"} img, err := jpeg.Decode(bytes.NewReader(b)) if err != nil { return b @@ -54,3 +68,26 @@ func Encoder(codec *core.Codec, handler core.HandlerFunc) core.HandlerFunc { handler(&clone) } } + +const dhtSize = 432 // known size for 4 default tables + +func InjectDHT(b []byte) []byte { + if bytes.Index(b, []byte{0xFF, markerDHT}) > 0 { + return b // already exist + } + + i := bytes.Index(b, []byte{0xFF, markerSOS}) + if i < 0 { + return b + } + + dht := make([]byte, 0, dhtSize) + dht = MakeHuffmanHeaders(dht) + + tmp := make([]byte, len(b)+dhtSize) + copy(tmp, b[:i]) + copy(tmp[i:], dht) + copy(tmp[i+dhtSize:], b[i:]) + + return tmp +} diff --git a/pkg/mjpeg/jpeg.go b/pkg/mjpeg/jpeg.go new file mode 100644 index 00000000..8d6d13d1 --- /dev/null +++ b/pkg/mjpeg/jpeg.go @@ -0,0 +1,10 @@ +package mjpeg + +const ( + markerSOF = 0xC0 // Start Of Frame (Baseline Sequential) + markerSOI = 0xD8 // Start Of Image + markerEOI = 0xD9 // End Of Image + markerSOS = 0xDA // Start Of Scan + markerDQT = 0xDB // Define Quantization Table + markerDHT = 0xC4 // Define Huffman Table +) diff --git a/pkg/mjpeg/rfc2435.go b/pkg/mjpeg/rfc2435.go index 44307896..aa34c2f1 100644 --- a/pkg/mjpeg/rfc2435.go +++ b/pkg/mjpeg/rfc2435.go @@ -143,9 +143,7 @@ var chm_ac_symbols = []byte{ func MakeHeaders(p []byte, t byte, w, h uint16, lqt, cqt []byte) []byte { // Appendix A from https://www.rfc-editor.org/rfc/rfc2435 - p = append(p, 0xFF, - 0xD8, // SOI - ) + p = append(p, 0xFF, markerSOI) p = MakeQuantHeader(p, lqt, 0) p = MakeQuantHeader(p, cqt, 1) @@ -156,8 +154,7 @@ func MakeHeaders(p []byte, t byte, w, h uint16, lqt, cqt []byte) []byte { t = 0x22 // hsamp = 2, vsamp = 2 } - p = append(p, 0xFF, - 0xC0, // SOF + p = append(p, 0xFF, markerSOF, 0, 17, // size 8, // bits per component byte(h>>8), byte(h&0xFF), @@ -174,13 +171,9 @@ func MakeHeaders(p []byte, t byte, w, h uint16, lqt, cqt []byte) []byte { 1, // quant table 1 ) - p = MakeHuffmanHeader(p, lum_dc_codelens, lum_dc_symbols, 0, 0) - p = MakeHuffmanHeader(p, lum_ac_codelens, lum_ac_symbols, 0, 1) - p = MakeHuffmanHeader(p, chm_dc_codelens, chm_dc_symbols, 1, 0) - p = MakeHuffmanHeader(p, chm_ac_codelens, chm_ac_symbols, 1, 1) + p = MakeHuffmanHeaders(p) - return append(p, 0xFF, - 0xDA, // SOS + return append(p, 0xFF, markerSOS, 0, 12, // size 3, // 3 components 0, // comp 0 @@ -196,16 +189,23 @@ func MakeHeaders(p []byte, t byte, w, h uint16, lqt, cqt []byte) []byte { } func MakeQuantHeader(p []byte, qt []byte, tableNo byte) []byte { - p = append(p, 0xFF, 0xDB, 0, 67, tableNo) + p = append(p, 0xFF, markerDQT, 0, 67, tableNo) return append(p, qt...) } func MakeHuffmanHeader(p, codelens, symbols []byte, tableNo, tableClass byte) []byte { - p = append(p, - 0xFF, 0xC4, 0, - byte(3+len(codelens)+len(symbols)), + p = append(p, 0xFF, markerDHT, + 0, byte(3+len(codelens)+len(symbols)), // size (tableClass<<4)|tableNo, ) p = append(p, codelens...) return append(p, symbols...) } + +func MakeHuffmanHeaders(p []byte) []byte { + p = MakeHuffmanHeader(p, lum_dc_codelens, lum_dc_symbols, 0, 0) + p = MakeHuffmanHeader(p, lum_ac_codelens, lum_ac_symbols, 0, 1) + p = MakeHuffmanHeader(p, chm_dc_codelens, chm_dc_symbols, 1, 0) + p = MakeHuffmanHeader(p, chm_ac_codelens, chm_ac_symbols, 1, 1) + return p +} From a9e1ebc0a8da09f321e5eb08973c7c7b6118d8b5 Mon Sep 17 00:00:00 2001 From: Bruno Tomassetti Couto Date: Sun, 5 Jan 2025 22:54:20 -0300 Subject: [PATCH 113/166] Improve ONVIF server by adding rate control for video encoder configuration --- pkg/onvif/server.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/onvif/server.go b/pkg/onvif/server.go index 42343d37..66f27095 100644 --- a/pkg/onvif/server.go +++ b/pkg/onvif/server.go @@ -172,6 +172,7 @@ func appendProfile(e *Envelope, tag, name string) { VEC H264 19201080 + `) From c065db6da149c06dc7f6988848e9246ca80d6ac0 Mon Sep 17 00:00:00 2001 From: Alex X Date: Mon, 6 Jan 2025 06:32:13 +0300 Subject: [PATCH 114/166] Code refactoring after #1539 --- pkg/onvif/server.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/onvif/server.go b/pkg/onvif/server.go index 66f27095..db0bb2fb 100644 --- a/pkg/onvif/server.go +++ b/pkg/onvif/server.go @@ -161,6 +161,7 @@ func GetProfileResponse(name string) []byte { } func appendProfile(e *Envelope, tag, name string) { + // empty `RateControl` important for UniFi Protect e.Append(` `, name, ` @@ -172,7 +173,7 @@ func appendProfile(e *Envelope, tag, name string) { VEC H264 19201080 - + `) From df831833b1fcea9fd5e0cab0c11fea6920de47eb Mon Sep 17 00:00:00 2001 From: Alex X Date: Mon, 6 Jan 2025 19:31:03 +0300 Subject: [PATCH 115/166] Collect list of dependency license --- scripts/README.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/scripts/README.md b/scripts/README.md index 36f667b2..acc6e0c9 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -54,6 +54,41 @@ go list -deps .\cmd\go2rtc_rtsp\ - golang.org/x/tools ``` +## Licenses + +- github.com/asticode/go-astits - MIT +- github.com/expr-lang/expr - MIT +- github.com/gorilla/websocket - BSD-2 +- github.com/mattn/go-isatty - MIT +- github.com/miekg/dns - BSD-3 +- github.com/pion/ice/v2 - MIT +- github.com/pion/interceptor - MIT +- github.com/pion/rtcp - MIT +- github.com/pion/rtp - MIT +- github.com/pion/sdp/v3 - MIT +- github.com/pion/srtp/v2 - MIT +- github.com/pion/stun - MIT +- github.com/pion/webrtc/v3 - MIT +- github.com/rs/zerolog - MIT +- github.com/sigurn/crc16 - MIT +- github.com/sigurn/crc8 - MIT +- github.com/stretchr/testify - MIT +- github.com/tadglines/go-pkgs - Apache +- golang.org/x/crypto - BSD-3 +- gopkg.in/yaml.v3 - MIT and Apache +- github.com/asticode/go-astikit - MIT +- github.com/davecgh/go-spew - ISC (BSD/MIT like) +- github.com/google/uuid - BSD-3 +- github.com/kr/pretty - MIT +- github.com/mattn/go-colorable - MIT +- github.com/pmezard/go-difflib - ??? +- github.com/wlynxg/anet - BSD-3 +- golang.org/x/mod - BSD-3 +- golang.org/x/net - BSD-3 +- golang.org/x/sync - BSD-3 +- golang.org/x/sys - BSD-3 +- golang.org/x/tools - BSD-3 + ## Virus - https://go.dev/doc/faq#virus From d59139a2ab200eb064bab7be9fdc4b1d9f1227e1 Mon Sep 17 00:00:00 2001 From: Alex X Date: Mon, 6 Jan 2025 23:47:35 +0300 Subject: [PATCH 116/166] Add support v4l2 source --- examples/go2rtc_mjpeg/main.go | 21 +++ internal/v4l2/v4l2.go | 7 + internal/v4l2/v4l2_linux.go | 79 ++++++++++ main.go | 2 + pkg/v4l2/device/device.go | 244 ++++++++++++++++++++++++++++++ pkg/v4l2/device/formats.go | 40 +++++ pkg/v4l2/device/videodev2_test.go | 34 +++++ pkg/v4l2/device/videodev2_x32.go | 152 +++++++++++++++++++ pkg/v4l2/device/videodev2_x64.go | 153 +++++++++++++++++++ pkg/v4l2/producer.go | 115 ++++++++++++++ www/add.html | 12 ++ 11 files changed, 859 insertions(+) create mode 100644 examples/go2rtc_mjpeg/main.go create mode 100644 internal/v4l2/v4l2.go create mode 100644 internal/v4l2/v4l2_linux.go create mode 100644 pkg/v4l2/device/device.go create mode 100644 pkg/v4l2/device/formats.go create mode 100644 pkg/v4l2/device/videodev2_test.go create mode 100644 pkg/v4l2/device/videodev2_x32.go create mode 100644 pkg/v4l2/device/videodev2_x64.go create mode 100644 pkg/v4l2/producer.go diff --git a/examples/go2rtc_mjpeg/main.go b/examples/go2rtc_mjpeg/main.go new file mode 100644 index 00000000..a3e08ff5 --- /dev/null +++ b/examples/go2rtc_mjpeg/main.go @@ -0,0 +1,21 @@ +package main + +import ( + "github.com/AlexxIT/go2rtc/internal/api" + "github.com/AlexxIT/go2rtc/internal/app" + "github.com/AlexxIT/go2rtc/internal/mjpeg" + "github.com/AlexxIT/go2rtc/internal/streams" + "github.com/AlexxIT/go2rtc/internal/v4l2" + "github.com/AlexxIT/go2rtc/pkg/shell" +) + +func main() { + app.Init() + streams.Init() + + api.Init() + mjpeg.Init() + v4l2.Init() + + shell.RunUntilSignal() +} diff --git a/internal/v4l2/v4l2.go b/internal/v4l2/v4l2.go new file mode 100644 index 00000000..9cef99a5 --- /dev/null +++ b/internal/v4l2/v4l2.go @@ -0,0 +1,7 @@ +//go:build !linux + +package v4l2 + +func Init() { + // not supported +} diff --git a/internal/v4l2/v4l2_linux.go b/internal/v4l2/v4l2_linux.go new file mode 100644 index 00000000..4a54e1e1 --- /dev/null +++ b/internal/v4l2/v4l2_linux.go @@ -0,0 +1,79 @@ +package v4l2 + +import ( + "encoding/binary" + "fmt" + "net/http" + "os" + "strings" + + "github.com/AlexxIT/go2rtc/internal/api" + "github.com/AlexxIT/go2rtc/internal/streams" + "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/AlexxIT/go2rtc/pkg/v4l2" + "github.com/AlexxIT/go2rtc/pkg/v4l2/device" +) + +func Init() { + streams.HandleFunc("v4l2", func(source string) (core.Producer, error) { + return v4l2.Open(source) + }) + + api.HandleFunc("api/v4l2", apiV4L2) +} + +func apiV4L2(w http.ResponseWriter, r *http.Request) { + files, err := os.ReadDir("/dev") + if err != nil { + return + } + + var sources []*api.Source + + for _, file := range files { + if !strings.HasPrefix(file.Name(), core.KindVideo) { + continue + } + + path := "/dev/" + file.Name() + + dev, err := device.Open(path) + if err != nil { + continue + } + + formats, _ := dev.ListFormats() + for _, fourCC := range formats { + source := &api.Source{} + + for _, format := range device.Formats { + if format.FourCC == fourCC { + source.Name = format.Name + source.URL = "v4l2:device?video=" + path + "&input_format=" + format.FFmpeg + "&video_size=" + break + } + } + + if source.Name != "" { + sizes, _ := dev.ListSizes(fourCC) + for i := 0; i < len(sizes); i += 2 { + size := fmt.Sprintf("%dx%d", sizes[i], sizes[i+1]) + if i > 0 { + source.Info += " " + size + } else { + source.Info = size + source.URL += size + } + } + } else { + source.Name = string(binary.LittleEndian.AppendUint32(nil, fourCC)) + } + + sources = append(sources, source) + } + + _ = dev.Close() + } + + api.ResponseSources(w, sources) +} diff --git a/main.go b/main.go index b8d58b27..db8de9f4 100644 --- a/main.go +++ b/main.go @@ -31,6 +31,7 @@ import ( "github.com/AlexxIT/go2rtc/internal/srtp" "github.com/AlexxIT/go2rtc/internal/streams" "github.com/AlexxIT/go2rtc/internal/tapo" + "github.com/AlexxIT/go2rtc/internal/v4l2" "github.com/AlexxIT/go2rtc/internal/webrtc" "github.com/AlexxIT/go2rtc/internal/webtorrent" "github.com/AlexxIT/go2rtc/pkg/shell" @@ -84,6 +85,7 @@ func main() { expr.Init() // expr source gopro.Init() // gopro source doorbird.Init() // doorbird source + v4l2.Init() // v4l2 source // 6. Helper modules diff --git a/pkg/v4l2/device/device.go b/pkg/v4l2/device/device.go new file mode 100644 index 00000000..4e4d6ade --- /dev/null +++ b/pkg/v4l2/device/device.go @@ -0,0 +1,244 @@ +//go:build linux + +package device + +import ( + "bytes" + "errors" + "fmt" + "syscall" + "unsafe" +) + +type Device struct { + fd int + bufs [][]byte +} + +func Open(path string) (*Device, error) { + fd, err := syscall.Open(path, syscall.O_RDWR|syscall.O_CLOEXEC, 0) + if err != nil { + return nil, err + } + return &Device{fd: fd}, nil +} + +const buffersCount = 2 + +type Capability struct { + Driver string + Card string + BusInfo string + Version string +} + +func (d *Device) Capability() (*Capability, error) { + c := v4l2_capability{} + if err := ioctl(d.fd, VIDIOC_QUERYCAP, unsafe.Pointer(&c)); err != nil { + return nil, err + } + return &Capability{ + Driver: str(c.driver[:]), + Card: str(c.card[:]), + BusInfo: str(c.bus_info[:]), + Version: fmt.Sprintf("%d.%d.%d", byte(c.version>>16), byte(c.version>>8), byte(c.version)), + }, nil +} + +func (d *Device) ListFormats() ([]uint32, error) { + var items []uint32 + + for i := uint32(0); ; i++ { + fd := v4l2_fmtdesc{ + index: i, + typ: V4L2_BUF_TYPE_VIDEO_CAPTURE, + } + if err := ioctl(d.fd, VIDIOC_ENUM_FMT, unsafe.Pointer(&fd)); err != nil { + if !errors.Is(err, syscall.EINVAL) { + return nil, err + } + break + } + + items = append(items, fd.pixelformat) + } + + return items, nil +} + +func (d *Device) ListSizes(pixFmt uint32) ([]uint32, error) { + var items []uint32 + + for i := uint32(0); ; i++ { + fs := v4l2_frmsizeenum{ + index: i, + pixel_format: pixFmt, + } + if err := ioctl(d.fd, VIDIOC_ENUM_FRAMESIZES, unsafe.Pointer(&fs)); err != nil { + if !errors.Is(err, syscall.EINVAL) { + return nil, err + } + break + } + + if fs.typ != V4L2_FRMSIZE_TYPE_DISCRETE { + continue + } + + items = append(items, fs.discrete.width, fs.discrete.height) + } + + return items, nil +} + +func (d *Device) ListFrameRates(pixFmt, width, height uint32) ([]uint32, error) { + var items []uint32 + + for i := uint32(0); ; i++ { + fi := v4l2_frmivalenum{ + index: i, + pixel_format: pixFmt, + width: width, + height: height, + } + if err := ioctl(d.fd, VIDIOC_ENUM_FRAMEINTERVALS, unsafe.Pointer(&fi)); err != nil { + if !errors.Is(err, syscall.EINVAL) { + return nil, err + } + break + } + + if fi.typ != V4L2_FRMIVAL_TYPE_DISCRETE || fi.discrete.numerator != 1 { + continue + } + + items = append(items, fi.discrete.denominator) + } + + return items, nil +} + +func (d *Device) SetFormat(width, height, pixFmt uint32) error { + f := v4l2_format{ + typ: V4L2_BUF_TYPE_VIDEO_CAPTURE, + fmt: v4l2_pix_format{ + width: width, + height: height, + pixelformat: pixFmt, + field: V4L2_FIELD_NONE, + colorspace: V4L2_COLORSPACE_DEFAULT, + }, + } + return ioctl(d.fd, VIDIOC_S_FMT, unsafe.Pointer(&f)) +} + +func (d *Device) SetParam(fps uint32) error { + p := v4l2_streamparm{ + typ: V4L2_BUF_TYPE_VIDEO_CAPTURE, + capture: v4l2_captureparm{ + timeperframe: v4l2_fract{numerator: 1, denominator: fps}, + }, + } + return ioctl(d.fd, VIDIOC_S_PARM, unsafe.Pointer(&p)) +} + +func (d *Device) StreamOn() (err error) { + rb := v4l2_requestbuffers{ + count: buffersCount, + typ: V4L2_BUF_TYPE_VIDEO_CAPTURE, + memory: V4L2_MEMORY_MMAP, + } + if err = ioctl(d.fd, VIDIOC_REQBUFS, unsafe.Pointer(&rb)); err != nil { + return err + } + + d.bufs = make([][]byte, buffersCount) + for i := uint32(0); i < buffersCount; i++ { + qb := v4l2_buffer{ + index: i, + typ: V4L2_BUF_TYPE_VIDEO_CAPTURE, + memory: V4L2_MEMORY_MMAP, + } + if err = ioctl(d.fd, VIDIOC_QUERYBUF, unsafe.Pointer(&qb)); err != nil { + return err + } + + if d.bufs[i], err = syscall.Mmap( + d.fd, int64(qb.offset), int(qb.length), syscall.PROT_READ, syscall.MAP_SHARED, + ); nil != err { + return err + } + + if err = ioctl(d.fd, VIDIOC_QBUF, unsafe.Pointer(&qb)); err != nil { + return err + } + } + + typ := uint32(V4L2_BUF_TYPE_VIDEO_CAPTURE) + return ioctl(d.fd, VIDIOC_STREAMON, unsafe.Pointer(&typ)) +} + +func (d *Device) StreamOff() (err error) { + typ := uint32(V4L2_BUF_TYPE_VIDEO_CAPTURE) + if err = ioctl(d.fd, VIDIOC_STREAMOFF, unsafe.Pointer(&typ)); err != nil { + return err + } + + for i := range d.bufs { + _ = syscall.Munmap(d.bufs[i]) + } + + rb := v4l2_requestbuffers{ + count: 0, + typ: V4L2_BUF_TYPE_VIDEO_CAPTURE, + memory: V4L2_MEMORY_MMAP, + } + return ioctl(d.fd, VIDIOC_REQBUFS, unsafe.Pointer(&rb)) +} + +func (d *Device) Capture(cositedYUV bool) ([]byte, error) { + dec := v4l2_buffer{ + typ: V4L2_BUF_TYPE_VIDEO_CAPTURE, + memory: V4L2_MEMORY_MMAP, + } + if err := ioctl(d.fd, VIDIOC_DQBUF, unsafe.Pointer(&dec)); err != nil { + return nil, err + } + + buf := make([]byte, dec.bytesused) + if cositedYUV { + YUYV2YUV(buf, d.bufs[dec.index][:dec.bytesused]) + } else { + copy(buf, d.bufs[dec.index][:dec.bytesused]) + } + + enc := v4l2_buffer{ + typ: V4L2_BUF_TYPE_VIDEO_CAPTURE, + memory: V4L2_MEMORY_MMAP, + index: dec.index, + } + if err := ioctl(d.fd, VIDIOC_QBUF, unsafe.Pointer(&enc)); err != nil { + return nil, err + } + + return buf, nil +} + +func (d *Device) Close() error { + return syscall.Close(d.fd) +} + +func ioctl(fd int, req uint, arg unsafe.Pointer) error { + _, _, err := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), uintptr(req), uintptr(arg)) + if err != 0 { + return err + } + return nil +} + +func str(b []byte) string { + if i := bytes.IndexByte(b, 0); i >= 0 { + return string(b[:i]) + } + return string(b) +} diff --git a/pkg/v4l2/device/formats.go b/pkg/v4l2/device/formats.go new file mode 100644 index 00000000..94d12504 --- /dev/null +++ b/pkg/v4l2/device/formats.go @@ -0,0 +1,40 @@ +package device + +const ( + V4L2_PIX_FMT_YUYV = 'Y' | 'U'<<8 | 'Y'<<16 | 'V'<<24 + V4L2_PIX_FMT_MJPEG = 'M' | 'J'<<8 | 'P'<<16 | 'G'<<24 +) + +type Format struct { + FourCC uint32 + Name string + FFmpeg string +} + +var Formats = []Format{ + {V4L2_PIX_FMT_YUYV, "YUV 4:2:2", "yuyv422"}, + {V4L2_PIX_FMT_MJPEG, "Motion-JPEG", "mjpeg"}, +} + +// YUYV2YUV convert [Y0 Cb Y1 Cr] to cosited [Y0Y1... Cb... Cr...] +func YUYV2YUV(dst, src []byte) { + n := len(src) + i0 := 0 + iy := 0 + iu := n / 2 + iv := n / 4 * 3 + for i0 < n { + dst[iy] = src[i0] + i0++ + iy++ + dst[iu] = src[i0] + i0++ + iu++ + dst[iy] = src[i0] + i0++ + iy++ + dst[iv] = src[i0] + i0++ + iv++ + } +} diff --git a/pkg/v4l2/device/videodev2_test.go b/pkg/v4l2/device/videodev2_test.go new file mode 100644 index 00000000..2556feef --- /dev/null +++ b/pkg/v4l2/device/videodev2_test.go @@ -0,0 +1,34 @@ +package device + +import ( + "runtime" + "testing" + "unsafe" + + "github.com/stretchr/testify/require" +) + +func TestSize(t *testing.T) { + switch runtime.GOARCH { + case "amd64", "arm64": + require.Equal(t, 104, int(unsafe.Sizeof(v4l2_capability{}))) + require.Equal(t, 208, int(unsafe.Sizeof(v4l2_format{}))) + require.Equal(t, 204, int(unsafe.Sizeof(v4l2_streamparm{}))) + require.Equal(t, 20, int(unsafe.Sizeof(v4l2_requestbuffers{}))) + require.Equal(t, 88, int(unsafe.Sizeof(v4l2_buffer{}))) + require.Equal(t, 16, int(unsafe.Sizeof(v4l2_timecode{}))) + require.Equal(t, 64, int(unsafe.Sizeof(v4l2_fmtdesc{}))) + require.Equal(t, 44, int(unsafe.Sizeof(v4l2_frmsizeenum{}))) + require.Equal(t, 52, int(unsafe.Sizeof(v4l2_frmivalenum{}))) + case "386", "arm": + require.Equal(t, 104, int(unsafe.Sizeof(v4l2_capability{}))) + require.Equal(t, 204, int(unsafe.Sizeof(v4l2_format{}))) + require.Equal(t, 204, int(unsafe.Sizeof(v4l2_streamparm{}))) + require.Equal(t, 20, int(unsafe.Sizeof(v4l2_requestbuffers{}))) + require.Equal(t, 68, int(unsafe.Sizeof(v4l2_buffer{}))) + require.Equal(t, 16, int(unsafe.Sizeof(v4l2_timecode{}))) + require.Equal(t, 64, int(unsafe.Sizeof(v4l2_fmtdesc{}))) + require.Equal(t, 44, int(unsafe.Sizeof(v4l2_frmsizeenum{}))) + require.Equal(t, 52, int(unsafe.Sizeof(v4l2_frmivalenum{}))) + } +} diff --git a/pkg/v4l2/device/videodev2_x32.go b/pkg/v4l2/device/videodev2_x32.go new file mode 100644 index 00000000..4c4db26d --- /dev/null +++ b/pkg/v4l2/device/videodev2_x32.go @@ -0,0 +1,152 @@ +//go:build 386 || arm + +package device + +// https://github.com/torvalds/linux/blob/master/include/uapi/linux/videodev2.h + +const ( + VIDIOC_QUERYCAP = 0x80685600 + VIDIOC_ENUM_FMT = 0xc0405602 + VIDIOC_G_FMT = 0xc0cc5604 + VIDIOC_S_FMT = 0xc0cc5605 + VIDIOC_REQBUFS = 0xc0145608 + VIDIOC_QUERYBUF = 0xc0445609 + + VIDIOC_QBUF = 0xc044560f + VIDIOC_DQBUF = 0xc0445611 + VIDIOC_STREAMON = 0x40045612 + VIDIOC_STREAMOFF = 0x40045613 + VIDIOC_G_PARM = 0xc0cc5615 + VIDIOC_S_PARM = 0xc0cc5616 + + VIDIOC_ENUM_FRAMESIZES = 0xc02c564a + VIDIOC_ENUM_FRAMEINTERVALS = 0xc034564b +) + +const ( + V4L2_BUF_TYPE_VIDEO_CAPTURE = 1 + V4L2_COLORSPACE_DEFAULT = 0 + V4L2_FIELD_NONE = 1 + V4L2_FRMIVAL_TYPE_DISCRETE = 1 + V4L2_FRMSIZE_TYPE_DISCRETE = 1 + V4L2_MEMORY_MMAP = 1 +) + +type v4l2_capability struct { + driver [16]byte + card [32]byte + bus_info [32]byte + version uint32 + capabilities uint32 + device_caps uint32 + reserved [3]uint32 +} + +type v4l2_format struct { + typ uint32 + fmt v4l2_pix_format +} + +type v4l2_pix_format struct { + width uint32 // 0 + height uint32 // 4 + pixelformat uint32 // 8 + field uint32 // 12 + bytesperline uint32 // 16 + sizeimage uint32 // 20 + colorspace uint32 // 24 + priv uint32 // 28 + flags uint32 // 32 + ycbcr_enc uint32 // 36 + quantization uint32 // 40 + xfer_func uint32 // 44 + + _ [152]byte // 48 +} + +type v4l2_streamparm struct { + typ uint32 + capture v4l2_captureparm +} + +type v4l2_captureparm struct { + capability uint32 // 0 + capturemode uint32 // 4 + timeperframe v4l2_fract // 8 + extendedmode uint32 // 16 + readbuffers uint32 // 20 + + _ [176]byte // 24 +} + +type v4l2_fract struct { + numerator uint32 + denominator uint32 +} + +type v4l2_requestbuffers struct { + count uint32 + typ uint32 + memory uint32 + capabilities uint32 + flags uint8 + reserved [3]uint8 +} + +type v4l2_buffer struct { + index uint32 // 0 + typ uint32 // 4 + bytesused uint32 // 8 + flags uint32 // 12 + field uint32 // 16 + _ [8]byte // 20 + timecode v4l2_timecode // 28 + sequence uint32 // 44 + memory uint32 // 48 + offset uint32 // 52 + length uint32 // 56 + _ [8]byte // 60 +} + +type v4l2_timecode struct { + typ uint32 + flags uint32 + frames uint8 + seconds uint8 + minutes uint8 + hours uint8 + userbits [4]uint8 +} + +type v4l2_fmtdesc struct { + index uint32 + typ uint32 + flags uint32 + description [32]byte + pixelformat uint32 + mbus_code uint32 + reserved [3]uint32 +} + +type v4l2_frmsizeenum struct { + index uint32 // 0 + pixel_format uint32 // 4 + typ uint32 // 8 + discrete v4l2_frmsize_discrete // 12 + _ [24]byte +} + +type v4l2_frmsize_discrete struct { + width uint32 + height uint32 +} + +type v4l2_frmivalenum struct { + index uint32 + pixel_format uint32 + width uint32 + height uint32 + typ uint32 + discrete v4l2_fract + _ [24]byte +} diff --git a/pkg/v4l2/device/videodev2_x64.go b/pkg/v4l2/device/videodev2_x64.go new file mode 100644 index 00000000..97c3ab95 --- /dev/null +++ b/pkg/v4l2/device/videodev2_x64.go @@ -0,0 +1,153 @@ +//go:build amd64 || arm64 + +package device + +// https://github.com/torvalds/linux/blob/master/include/uapi/linux/videodev2.h + +const ( + VIDIOC_QUERYCAP = 0x80685600 + VIDIOC_ENUM_FMT = 0xc0405602 + VIDIOC_G_FMT = 0xc0d05604 + VIDIOC_S_FMT = 0xc0d05605 + VIDIOC_REQBUFS = 0xc0145608 + VIDIOC_QUERYBUF = 0xc0585609 + + VIDIOC_QBUF = 0xc058560f + VIDIOC_DQBUF = 0xc0585611 + VIDIOC_STREAMON = 0x40045612 + VIDIOC_STREAMOFF = 0x40045613 + VIDIOC_G_PARM = 0xc0cc5615 + VIDIOC_S_PARM = 0xc0cc5616 + + VIDIOC_ENUM_FRAMESIZES = 0xc02c564a + VIDIOC_ENUM_FRAMEINTERVALS = 0xc034564b +) + +const ( + V4L2_BUF_TYPE_VIDEO_CAPTURE = 1 + V4L2_COLORSPACE_DEFAULT = 0 + V4L2_FIELD_NONE = 1 + V4L2_FRMIVAL_TYPE_DISCRETE = 1 + V4L2_FRMSIZE_TYPE_DISCRETE = 1 + V4L2_MEMORY_MMAP = 1 +) + +type v4l2_capability struct { + driver [16]byte + card [32]byte + bus_info [32]byte + version uint32 + capabilities uint32 + device_caps uint32 + reserved [3]uint32 +} + +type v4l2_format struct { + typ uint64 + fmt v4l2_pix_format +} + +type v4l2_pix_format struct { + width uint32 // 0 + height uint32 // 4 + pixelformat uint32 // 8 + field uint32 // 12 + bytesperline uint32 // 16 + sizeimage uint32 // 20 + colorspace uint32 // 24 + priv uint32 // 28 + flags uint32 // 32 + ycbcr_enc uint32 // 36 + quantization uint32 // 40 + xfer_func uint32 // 44 + + _ [152]byte // 48 +} + +type v4l2_streamparm struct { + typ uint32 + capture v4l2_captureparm +} + +type v4l2_captureparm struct { + capability uint32 // 0 + capturemode uint32 // 4 + timeperframe v4l2_fract // 8 + extendedmode uint32 // 16 + readbuffers uint32 // 20 + + _ [176]byte // 24 +} + +type v4l2_fract struct { + numerator uint32 + denominator uint32 +} + +type v4l2_requestbuffers struct { + count uint32 + typ uint32 + memory uint32 + capabilities uint32 + flags uint8 + reserved [3]uint8 +} + +type v4l2_buffer struct { + index uint32 // 0 + typ uint32 // 4 + bytesused uint32 // 8 + flags uint32 // 12 + field uint32 // 16 + _ [20]byte // 20 + timecode v4l2_timecode // 40 + sequence uint32 // 56 + memory uint32 // 60 + offset uint32 // 64 + _ [4]byte // 68 + length uint32 // 72 + _ [12]byte // 76 +} + +type v4l2_timecode struct { + typ uint32 + flags uint32 + frames uint8 + seconds uint8 + minutes uint8 + hours uint8 + userbits [4]uint8 +} + +type v4l2_fmtdesc struct { + index uint32 + typ uint32 + flags uint32 + description [32]byte + pixelformat uint32 + mbus_code uint32 + reserved [3]uint32 +} + +type v4l2_frmsizeenum struct { + index uint32 // 0 + pixel_format uint32 // 4 + typ uint32 // 8 + discrete v4l2_frmsize_discrete // 12 + _ [24]byte +} + +type v4l2_frmsize_discrete struct { + width uint32 + height uint32 +} + +type v4l2_frmivalenum struct { + index uint32 + pixel_format uint32 + width uint32 + height uint32 + typ uint32 + discrete v4l2_fract + _ [24]byte +} diff --git a/pkg/v4l2/producer.go b/pkg/v4l2/producer.go new file mode 100644 index 00000000..644c5ee5 --- /dev/null +++ b/pkg/v4l2/producer.go @@ -0,0 +1,115 @@ +//go:build linux + +package v4l2 + +import ( + "errors" + "net/url" + "strings" + + "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/AlexxIT/go2rtc/pkg/v4l2/device" + "github.com/pion/rtp" +) + +type Producer struct { + core.Connection + dev *device.Device +} + +func Open(rawURL string) (*Producer, error) { + // Example (ffmpeg source compatible): + // v4l2:device?video=/dev/video0&input_format=mjpeg&video_size=1280x720 + u, err := url.Parse(rawURL) + if err != nil { + return nil, err + } + + query := u.Query() + + dev, err := device.Open(query.Get("video")) + if err != nil { + return nil, err + } + + codec := &core.Codec{ + ClockRate: 90000, + PayloadType: core.PayloadTypeRAW, + } + + var width, height, pixFmt uint32 + + if wh := strings.Split(query.Get("video_size"), "x"); len(wh) == 2 { + codec.FmtpLine = "width=" + wh[0] + ";height=" + wh[1] + width = uint32(core.Atoi(wh[0])) + height = uint32(core.Atoi(wh[1])) + } + + switch query.Get("input_format") { + case "mjpeg": + codec.Name = core.CodecJPEG + pixFmt = device.V4L2_PIX_FMT_MJPEG + case "yuyv422": + if codec.FmtpLine == "" { + return nil, errors.New("v4l2: invalid video_size") + } + + codec.Name = core.CodecRAW + codec.FmtpLine += ";colorspace=422" + pixFmt = device.V4L2_PIX_FMT_YUYV + default: + return nil, errors.New("v4l2: invalid input_format") + } + + if err = dev.SetFormat(width, height, pixFmt); err != nil { + return nil, err + } + + medias := []*core.Media{ + { + Kind: core.KindVideo, + Direction: core.DirectionRecvonly, + Codecs: []*core.Codec{codec}, + }, + } + return &Producer{ + Connection: core.Connection{ + ID: core.NewID(), + FormatName: "v4l2", + Medias: medias, + }, + dev: dev, + }, nil +} + +func (c *Producer) Start() error { + if err := c.dev.StreamOn(); err != nil { + return err + } + + cositedYUV := c.Medias[0].Codecs[0].Name == core.CodecRAW + + for { + buf, err := c.dev.Capture(cositedYUV) + if err != nil { + return err + } + + c.Recv += len(buf) + + if len(c.Receivers) == 0 { + continue + } + + pkt := &rtp.Packet{ + Header: rtp.Header{Timestamp: core.Now90000()}, + Payload: buf, + } + c.Receivers[0].WriteRTP(pkt) + } +} + +func (c *Producer) Stop() error { + _ = c.Connection.Stop() + return errors.Join(c.dev.StreamOff(), c.dev.Close()) +} diff --git a/www/add.html b/www/add.html index 4b40f431..49e954d3 100644 --- a/www/add.html +++ b/www/add.html @@ -292,6 +292,18 @@ + +
    +
    +
    + + +
    From 33e0ccdd109d3073b1fd05b00621f6955ea08845 Mon Sep 17 00:00:00 2001 From: Alex X Date: Tue, 7 Jan 2025 00:19:53 +0300 Subject: [PATCH 117/166] Fix build for mipsle --- internal/v4l2/v4l2.go | 2 +- internal/v4l2/v4l2_linux.go | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/internal/v4l2/v4l2.go b/internal/v4l2/v4l2.go index 9cef99a5..cfef9667 100644 --- a/internal/v4l2/v4l2.go +++ b/internal/v4l2/v4l2.go @@ -1,4 +1,4 @@ -//go:build !linux +//go:build !(linux && (386 || arm || amd64 || arm64)) package v4l2 diff --git a/internal/v4l2/v4l2_linux.go b/internal/v4l2/v4l2_linux.go index 4a54e1e1..4c67235d 100644 --- a/internal/v4l2/v4l2_linux.go +++ b/internal/v4l2/v4l2_linux.go @@ -1,3 +1,5 @@ +//go:build linux && (386 || arm || amd64 || arm64) + package v4l2 import ( From e4b8d1807dee455122d2953a2f0e98de41d86bfc Mon Sep 17 00:00:00 2001 From: Alex X Date: Tue, 7 Jan 2025 20:08:29 +0300 Subject: [PATCH 118/166] Add support snapshot for raw image format --- pkg/magic/keyframe.go | 10 ++++++++++ pkg/mjpeg/consumer.go | 2 +- pkg/mjpeg/helpers.go | 9 ++++++++- pkg/y4m/y4m.go | 24 ++++++++++++++++++++++++ 4 files changed, 43 insertions(+), 2 deletions(-) diff --git a/pkg/magic/keyframe.go b/pkg/magic/keyframe.go index 8f70eec6..9b6ef562 100644 --- a/pkg/magic/keyframe.go +++ b/pkg/magic/keyframe.go @@ -24,6 +24,7 @@ func NewKeyframe() *Keyframe { Direction: core.DirectionSendonly, Codecs: []*core.Codec{ {Name: core.CodecJPEG}, + {Name: core.CodecRAW}, {Name: core.CodecH264}, {Name: core.CodecH265}, }, @@ -87,6 +88,15 @@ func (k *Keyframe) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiv if track.Codec.IsRTP() { sender.Handler = mjpeg.RTPDepay(sender.Handler) } + + case core.CodecRAW: + sender.Handler = func(packet *rtp.Packet) { + if n, err := k.wr.Write(packet.Payload); err == nil { + k.Send += n + } + } + + sender.Handler = mjpeg.Encoder(track.Codec, 5, sender.Handler) } sender.HandleRTP(track) diff --git a/pkg/mjpeg/consumer.go b/pkg/mjpeg/consumer.go index 16edc895..819c558a 100644 --- a/pkg/mjpeg/consumer.go +++ b/pkg/mjpeg/consumer.go @@ -46,7 +46,7 @@ func (c *Consumer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiv if track.Codec.IsRTP() { sender.Handler = RTPDepay(sender.Handler) } else if track.Codec.Name == core.CodecRAW { - sender.Handler = Encoder(track.Codec, sender.Handler) + sender.Handler = Encoder(track.Codec, 0, sender.Handler) } sender.HandleRTP(track) diff --git a/pkg/mjpeg/helpers.go b/pkg/mjpeg/helpers.go index d1acbd45..87f59e07 100644 --- a/pkg/mjpeg/helpers.go +++ b/pkg/mjpeg/helpers.go @@ -52,12 +52,19 @@ func FixJPEG(b []byte) []byte { return buf.Bytes() } -func Encoder(codec *core.Codec, handler core.HandlerFunc) core.HandlerFunc { +// Encoder convert YUV frame to Img. +// Support skipping empty frames, for example if USB cam needs time to start. +func Encoder(codec *core.Codec, skipEmpty int, handler core.HandlerFunc) core.HandlerFunc { newImage := y4m.NewImage(codec.FmtpLine) return func(packet *rtp.Packet) { img := newImage(packet.Payload) + if skipEmpty != 0 && y4m.HasSameColor(img) { + skipEmpty-- + return + } + buf := bytes.NewBuffer(nil) if err := jpeg.Encode(buf, img, nil); err != nil { return diff --git a/pkg/y4m/y4m.go b/pkg/y4m/y4m.go index 4ac54da6..24c43164 100644 --- a/pkg/y4m/y4m.go +++ b/pkg/y4m/y4m.go @@ -123,3 +123,27 @@ func NewImage(fmtp string) func(frame []byte) image.Image { return nil } + +// HasSameColor checks if all pixels has same color +func HasSameColor(img image.Image) bool { + var pix []byte + + switch img := img.(type) { + case *image.Gray: + pix = img.Pix + case *image.YCbCr: + pix = img.Y + } + + if len(pix) == 0 { + return false + } + + i0 := pix[0] + for _, i := range pix { + if i != i0 { + return false + } + } + return true +} From 93252fc5d271cae590ac902cfbd6185ddcd4687a Mon Sep 17 00:00:00 2001 From: Alex X Date: Tue, 7 Jan 2025 22:17:35 +0300 Subject: [PATCH 119/166] Change ListSizes function for V4L2 device --- internal/v4l2/v4l2_linux.go | 6 +++--- pkg/v4l2/device/device.go | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/v4l2/v4l2_linux.go b/internal/v4l2/v4l2_linux.go index 4c67235d..a16779f4 100644 --- a/internal/v4l2/v4l2_linux.go +++ b/internal/v4l2/v4l2_linux.go @@ -58,9 +58,9 @@ func apiV4L2(w http.ResponseWriter, r *http.Request) { if source.Name != "" { sizes, _ := dev.ListSizes(fourCC) - for i := 0; i < len(sizes); i += 2 { - size := fmt.Sprintf("%dx%d", sizes[i], sizes[i+1]) - if i > 0 { + for _, wh := range sizes { + size := fmt.Sprintf("%dx%d", wh[0], wh[1]) + if source.Info != "" { source.Info += " " + size } else { source.Info = size diff --git a/pkg/v4l2/device/device.go b/pkg/v4l2/device/device.go index 4e4d6ade..7377aef6 100644 --- a/pkg/v4l2/device/device.go +++ b/pkg/v4l2/device/device.go @@ -66,8 +66,8 @@ func (d *Device) ListFormats() ([]uint32, error) { return items, nil } -func (d *Device) ListSizes(pixFmt uint32) ([]uint32, error) { - var items []uint32 +func (d *Device) ListSizes(pixFmt uint32) ([][2]uint32, error) { + var items [][2]uint32 for i := uint32(0); ; i++ { fs := v4l2_frmsizeenum{ @@ -85,7 +85,7 @@ func (d *Device) ListSizes(pixFmt uint32) ([]uint32, error) { continue } - items = append(items, fs.discrete.width, fs.discrete.height) + items = append(items, [2]uint32{fs.discrete.width, fs.discrete.height}) } return items, nil From 59161c663b9c6d3beab9f81202782c606c1d1459 Mon Sep 17 00:00:00 2001 From: Alex X Date: Tue, 7 Jan 2025 22:18:09 +0300 Subject: [PATCH 120/166] Add support framerate param for v4l2 source --- pkg/v4l2/producer.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pkg/v4l2/producer.go b/pkg/v4l2/producer.go index 644c5ee5..b979d346 100644 --- a/pkg/v4l2/producer.go +++ b/pkg/v4l2/producer.go @@ -65,6 +65,12 @@ func Open(rawURL string) (*Producer, error) { return nil, err } + if fps := core.Atoi(query.Get("framerate")); fps > 0 { + if err = dev.SetParam(uint32(fps)); err != nil { + return nil, err + } + } + medias := []*core.Media{ { Kind: core.KindVideo, From 8e4088e08f18edf2d375d339032ea2a3ba69f93f Mon Sep 17 00:00:00 2001 From: Timo Christeleit Date: Wed, 8 Jan 2025 11:03:17 +0100 Subject: [PATCH 121/166] fix tapo h200 + d230 doorbell stream --- pkg/tapo/client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/tapo/client.go b/pkg/tapo/client.go index 6ccafe4e..78bf5fce 100644 --- a/pkg/tapo/client.go +++ b/pkg/tapo/client.go @@ -291,7 +291,7 @@ func dial(req *http.Request, brand, username, password string) (net.Conn, *http. if err != nil { return nil, nil, err } - _ = res.Body.Close() // ignore response body + _, _ = io.Copy(io.Discard, res.Body) // ignore response body auth := res.Header.Get("WWW-Authenticate") From 7e0a163f120c98051b35b2f32690a323cb1aeb82 Mon Sep 17 00:00:00 2001 From: Alex X Date: Thu, 9 Jan 2025 00:20:48 +0300 Subject: [PATCH 122/166] Add support mips arch for v4l2 source --- internal/v4l2/v4l2.go | 2 +- internal/v4l2/v4l2_linux.go | 2 - pkg/v4l2/device/README.md | 19 +++ pkg/v4l2/device/arch.c | 163 ++++++++++++++++++++++++ pkg/v4l2/device/device.go | 2 +- pkg/v4l2/device/videodev2_386.go | 149 ++++++++++++++++++++++ pkg/v4l2/device/videodev2_arm.go | 149 ++++++++++++++++++++++ pkg/v4l2/device/videodev2_mipsle.go | 149 ++++++++++++++++++++++ pkg/v4l2/device/videodev2_test.go | 34 ----- pkg/v4l2/device/videodev2_x32.go | 152 ---------------------- pkg/v4l2/device/videodev2_x64.go | 190 ++++++++++++++-------------- 11 files changed, 725 insertions(+), 286 deletions(-) create mode 100644 pkg/v4l2/device/README.md create mode 100644 pkg/v4l2/device/arch.c create mode 100644 pkg/v4l2/device/videodev2_386.go create mode 100644 pkg/v4l2/device/videodev2_arm.go create mode 100644 pkg/v4l2/device/videodev2_mipsle.go delete mode 100644 pkg/v4l2/device/videodev2_test.go delete mode 100644 pkg/v4l2/device/videodev2_x32.go diff --git a/internal/v4l2/v4l2.go b/internal/v4l2/v4l2.go index cfef9667..9cef99a5 100644 --- a/internal/v4l2/v4l2.go +++ b/internal/v4l2/v4l2.go @@ -1,4 +1,4 @@ -//go:build !(linux && (386 || arm || amd64 || arm64)) +//go:build !linux package v4l2 diff --git a/internal/v4l2/v4l2_linux.go b/internal/v4l2/v4l2_linux.go index a16779f4..b9cc82a0 100644 --- a/internal/v4l2/v4l2_linux.go +++ b/internal/v4l2/v4l2_linux.go @@ -1,5 +1,3 @@ -//go:build linux && (386 || arm || amd64 || arm64) - package v4l2 import ( diff --git a/pkg/v4l2/device/README.md b/pkg/v4l2/device/README.md new file mode 100644 index 00000000..a816b23e --- /dev/null +++ b/pkg/v4l2/device/README.md @@ -0,0 +1,19 @@ +Build on Ubuntu + +```bash +sudo apt install gcc-x86-64-linux-gnu +sudo apt install gcc-i686-linux-gnu +sudo apt install gcc-aarch64-linux-gnu binutils +sudo apt install gcc-arm-linux-gnueabihf +sudo apt install gcc-mipsel-linux-gnu + +x86_64-linux-gnu-gcc -w -static arch.c -o arch_x86_64 +i686-linux-gnu-gcc -w -static arch.c -o arch_i686 +aarch64-linux-gnu-gcc -w -static arch.c -o arch_aarch64 +arm-linux-gnueabihf-gcc -w -static arch.c -o arch_armhf +mipsel-linux-gnu-gcc -static arch.c -o arch_mipsel +``` + +## Useful links + +- https://github.com/torvalds/linux/blob/master/include/uapi/linux/videodev2.h diff --git a/pkg/v4l2/device/arch.c b/pkg/v4l2/device/arch.c new file mode 100644 index 00000000..0b119584 --- /dev/null +++ b/pkg/v4l2/device/arch.c @@ -0,0 +1,163 @@ +#include +#include +#include + +#define printconst1(con) printf("\t%s = 0x%08lx\n", #con, con) +#define printconst2(con) printf("\t%s = %d\n", #con, con) +#define printstruct(str) printf("type %s struct { // size %lu\n", #str, sizeof(struct str)) +#define printmember(str, mem, typ) printf("\t%s %s // offset %lu, size %lu\n", #mem == "type" ? "typ" : #mem, typ, offsetof(struct str, mem), sizeof((struct str){0}.mem)) +#define printunimem(str, uni, mem, typ) printf("\t%s %s // offset %lu, size %lu\n", #mem, typ, offsetof(struct str, uni.mem), sizeof((struct str){0}.uni.mem)) +#define printalign1(str, mem2, mem1, siz1) printf("\t_ [%lu]byte // align\n", offsetof(struct str, mem2) - offsetof(struct str, mem1) - siz1) +#define printfiller(str, mem1, siz1) printf("\t_ [%lu]byte // filler\n", sizeof(struct str) - offsetof(struct str, mem1) - siz1) + +int main() { + printf("const (\n"); + printconst1(VIDIOC_QUERYCAP); + printconst1(VIDIOC_ENUM_FMT); + printconst1(VIDIOC_G_FMT); + printconst1(VIDIOC_S_FMT); + printconst1(VIDIOC_REQBUFS); + printconst1(VIDIOC_QUERYBUF); + printf("\n"); + printconst1(VIDIOC_QBUF); + printconst1(VIDIOC_DQBUF); + printconst1(VIDIOC_STREAMON); + printconst1(VIDIOC_STREAMOFF); + printconst1(VIDIOC_G_PARM); + printconst1(VIDIOC_S_PARM); + printf("\n"); + printconst1(VIDIOC_ENUM_FRAMESIZES); + printconst1(VIDIOC_ENUM_FRAMEINTERVALS); + printf(")\n\n"); + + printf("const (\n"); + printconst2(V4L2_BUF_TYPE_VIDEO_CAPTURE); + printconst2(V4L2_COLORSPACE_DEFAULT); + printconst2(V4L2_FIELD_NONE); + printconst2(V4L2_FRMIVAL_TYPE_DISCRETE); + printconst2(V4L2_FRMSIZE_TYPE_DISCRETE); + printconst2(V4L2_MEMORY_MMAP); + printf(")\n\n"); + + printstruct(v4l2_capability); + printmember(v4l2_capability, driver, "[16]byte"); + printmember(v4l2_capability, card, "[32]byte"); + printmember(v4l2_capability, bus_info, "[32]byte"); + printmember(v4l2_capability, version, "uint32"); + printmember(v4l2_capability, capabilities, "uint32"); + printmember(v4l2_capability, device_caps, "uint32"); + printmember(v4l2_capability, reserved, "[3]uint32"); + printf("}\n\n"); + + printstruct(v4l2_format); + printmember(v4l2_format, type, "uint32"); + printalign1(v4l2_format, fmt, type, 4); + printunimem(v4l2_format, fmt, pix, "v4l2_pix_format"); + printfiller(v4l2_format, fmt, sizeof(struct v4l2_pix_format)); + printf("}\n\n"); + + printstruct(v4l2_pix_format); + printmember(v4l2_pix_format, width, "uint32"); + printmember(v4l2_pix_format, height, "uint32"); + printmember(v4l2_pix_format, pixelformat, "uint32"); + printmember(v4l2_pix_format, field, "uint32"); + printmember(v4l2_pix_format, bytesperline, "uint32"); + printmember(v4l2_pix_format, sizeimage, "uint32"); + printmember(v4l2_pix_format, colorspace, "uint32"); + printmember(v4l2_pix_format, priv, "uint32"); + printmember(v4l2_pix_format, flags, "uint32"); + printmember(v4l2_pix_format, ycbcr_enc, "uint32"); + printmember(v4l2_pix_format, quantization, "uint32"); + printmember(v4l2_pix_format, xfer_func, "uint32"); + printf("}\n\n"); + + printstruct(v4l2_streamparm); + printmember(v4l2_streamparm, type, "uint32"); + printunimem(v4l2_streamparm, parm, capture, "v4l2_captureparm"); + printfiller(v4l2_streamparm, parm, sizeof(struct v4l2_captureparm)); + printf("}\n\n"); + + printstruct(v4l2_captureparm); + printmember(v4l2_captureparm, capability, "uint32"); + printmember(v4l2_captureparm, capturemode, "uint32"); + printmember(v4l2_captureparm, timeperframe, "v4l2_fract"); + printmember(v4l2_captureparm, extendedmode, "uint32"); + printmember(v4l2_captureparm, readbuffers, "uint32"); + printmember(v4l2_captureparm, reserved, "[4]uint32"); + printf("}\n\n"); + + printstruct(v4l2_fract); + printmember(v4l2_fract, numerator, "uint32"); + printmember(v4l2_fract, denominator, "uint32"); + printf("}\n\n"); + + printstruct(v4l2_requestbuffers); + printmember(v4l2_requestbuffers, count, "uint32"); + printmember(v4l2_requestbuffers, type, "uint32"); + printmember(v4l2_requestbuffers, memory, "uint32"); + printmember(v4l2_requestbuffers, capabilities, "uint32"); + printmember(v4l2_requestbuffers, flags, "uint8"); + printmember(v4l2_requestbuffers, reserved, "[3]uint8"); + printf("}\n\n"); + + printstruct(v4l2_buffer); + printmember(v4l2_buffer, index, "uint32"); + printmember(v4l2_buffer, type, "uint32"); + printmember(v4l2_buffer, bytesused, "uint32"); + printmember(v4l2_buffer, flags, "uint32"); + printmember(v4l2_buffer, field, "uint32"); + printalign1(v4l2_buffer, timecode, field, 4); + printmember(v4l2_buffer, timecode, "v4l2_timecode"); + printmember(v4l2_buffer, sequence, "uint32"); + printmember(v4l2_buffer, memory, "uint32"); + printunimem(v4l2_buffer, m, offset, "uint32"); + printalign1(v4l2_buffer, length, m, 4); + printmember(v4l2_buffer, length, "uint32"); + printfiller(v4l2_buffer, length, 4); + printf("}\n\n"); + + printstruct(v4l2_timecode); + printmember(v4l2_timecode, type, "uint32"); + printmember(v4l2_timecode, flags, "uint32"); + printmember(v4l2_timecode, frames, "uint8"); + printmember(v4l2_timecode, seconds, "uint8"); + printmember(v4l2_timecode, minutes, "uint8"); + printmember(v4l2_timecode, hours, "uint8"); + printmember(v4l2_timecode, userbits, "[4]uint8"); + printf("}\n\n"); + + printstruct(v4l2_fmtdesc); + printmember(v4l2_fmtdesc, index, "uint32"); + printmember(v4l2_fmtdesc, type, "uint32"); + printmember(v4l2_fmtdesc, flags, "uint32"); + printmember(v4l2_fmtdesc, description, "[32]byte"); + printmember(v4l2_fmtdesc, pixelformat, "uint32"); + printmember(v4l2_fmtdesc, mbus_code, "uint32"); + printmember(v4l2_fmtdesc, reserved, "[3]uint32"); + printf("}\n\n"); + + printstruct(v4l2_frmsizeenum); + printmember(v4l2_frmsizeenum, index, "uint32"); + printmember(v4l2_frmsizeenum, pixel_format, "uint32"); + printmember(v4l2_frmsizeenum, type, "uint32"); + printmember(v4l2_frmsizeenum, discrete, "v4l2_frmsize_discrete"); + printfiller(v4l2_frmsizeenum, discrete, sizeof(struct v4l2_frmsize_discrete)); + printf("}\n\n"); + + printstruct(v4l2_frmsize_discrete); + printmember(v4l2_frmsize_discrete, width, "uint32"); + printmember(v4l2_frmsize_discrete, height, "uint32"); + printf("}\n\n"); + + printstruct(v4l2_frmivalenum); + printmember(v4l2_frmivalenum, index, "uint32"); + printmember(v4l2_frmivalenum, pixel_format, "uint32"); + printmember(v4l2_frmivalenum, width, "uint32"); + printmember(v4l2_frmivalenum, height, "uint32"); + printmember(v4l2_frmivalenum, type, "uint32"); + printmember(v4l2_frmivalenum, discrete, "v4l2_fract"); + printfiller(v4l2_frmivalenum, discrete, sizeof(struct v4l2_fract)); + printf("}\n\n"); + + return 0; +} \ No newline at end of file diff --git a/pkg/v4l2/device/device.go b/pkg/v4l2/device/device.go index 7377aef6..27bbf350 100644 --- a/pkg/v4l2/device/device.go +++ b/pkg/v4l2/device/device.go @@ -121,7 +121,7 @@ func (d *Device) ListFrameRates(pixFmt, width, height uint32) ([]uint32, error) func (d *Device) SetFormat(width, height, pixFmt uint32) error { f := v4l2_format{ typ: V4L2_BUF_TYPE_VIDEO_CAPTURE, - fmt: v4l2_pix_format{ + pix: v4l2_pix_format{ width: width, height: height, pixelformat: pixFmt, diff --git a/pkg/v4l2/device/videodev2_386.go b/pkg/v4l2/device/videodev2_386.go new file mode 100644 index 00000000..8737ca9d --- /dev/null +++ b/pkg/v4l2/device/videodev2_386.go @@ -0,0 +1,149 @@ +package device + +const ( + VIDIOC_QUERYCAP = 0x80685600 + VIDIOC_ENUM_FMT = 0xc0405602 + VIDIOC_G_FMT = 0xc0cc5604 + VIDIOC_S_FMT = 0xc0cc5605 + VIDIOC_REQBUFS = 0xc0145608 + VIDIOC_QUERYBUF = 0xc0445609 + + VIDIOC_QBUF = 0xc044560f + VIDIOC_DQBUF = 0xc0445611 + VIDIOC_STREAMON = 0x40045612 + VIDIOC_STREAMOFF = 0x40045613 + VIDIOC_G_PARM = 0xc0cc5615 + VIDIOC_S_PARM = 0xc0cc5616 + + VIDIOC_ENUM_FRAMESIZES = 0xc02c564a + VIDIOC_ENUM_FRAMEINTERVALS = 0xc034564b +) + +const ( + V4L2_BUF_TYPE_VIDEO_CAPTURE = 1 + V4L2_COLORSPACE_DEFAULT = 0 + V4L2_FIELD_NONE = 1 + V4L2_FRMIVAL_TYPE_DISCRETE = 1 + V4L2_FRMSIZE_TYPE_DISCRETE = 1 + V4L2_MEMORY_MMAP = 1 +) + +type v4l2_capability struct { // size 104 + driver [16]byte // offset 0, size 16 + card [32]byte // offset 16, size 32 + bus_info [32]byte // offset 48, size 32 + version uint32 // offset 80, size 4 + capabilities uint32 // offset 84, size 4 + device_caps uint32 // offset 88, size 4 + reserved [3]uint32 // offset 92, size 12 +} + +type v4l2_format struct { // size 204 + typ uint32 // offset 0, size 4 + _ [0]byte // align + pix v4l2_pix_format // offset 4, size 48 + _ [152]byte // filler +} + +type v4l2_pix_format struct { // size 48 + width uint32 // offset 0, size 4 + height uint32 // offset 4, size 4 + pixelformat uint32 // offset 8, size 4 + field uint32 // offset 12, size 4 + bytesperline uint32 // offset 16, size 4 + sizeimage uint32 // offset 20, size 4 + colorspace uint32 // offset 24, size 4 + priv uint32 // offset 28, size 4 + flags uint32 // offset 32, size 4 + ycbcr_enc uint32 // offset 36, size 4 + quantization uint32 // offset 40, size 4 + xfer_func uint32 // offset 44, size 4 +} + +type v4l2_streamparm struct { // size 204 + typ uint32 // offset 0, size 4 + capture v4l2_captureparm // offset 4, size 40 + _ [160]byte // filler +} + +type v4l2_captureparm struct { // size 40 + capability uint32 // offset 0, size 4 + capturemode uint32 // offset 4, size 4 + timeperframe v4l2_fract // offset 8, size 8 + extendedmode uint32 // offset 16, size 4 + readbuffers uint32 // offset 20, size 4 + reserved [4]uint32 // offset 24, size 16 +} + +type v4l2_fract struct { // size 8 + numerator uint32 // offset 0, size 4 + denominator uint32 // offset 4, size 4 +} + +type v4l2_requestbuffers struct { // size 20 + count uint32 // offset 0, size 4 + typ uint32 // offset 4, size 4 + memory uint32 // offset 8, size 4 + capabilities uint32 // offset 12, size 4 + flags uint8 // offset 16, size 1 + reserved [3]uint8 // offset 17, size 3 +} + +type v4l2_buffer struct { // size 68 + index uint32 // offset 0, size 4 + typ uint32 // offset 4, size 4 + bytesused uint32 // offset 8, size 4 + flags uint32 // offset 12, size 4 + field uint32 // offset 16, size 4 + _ [8]byte // align + timecode v4l2_timecode // offset 28, size 16 + sequence uint32 // offset 44, size 4 + memory uint32 // offset 48, size 4 + offset uint32 // offset 52, size 4 + _ [0]byte // align + length uint32 // offset 56, size 4 + _ [8]byte // filler +} + +type v4l2_timecode struct { // size 16 + typ uint32 // offset 0, size 4 + flags uint32 // offset 4, size 4 + frames uint8 // offset 8, size 1 + seconds uint8 // offset 9, size 1 + minutes uint8 // offset 10, size 1 + hours uint8 // offset 11, size 1 + userbits [4]uint8 // offset 12, size 4 +} + +type v4l2_fmtdesc struct { // size 64 + index uint32 // offset 0, size 4 + typ uint32 // offset 4, size 4 + flags uint32 // offset 8, size 4 + description [32]byte // offset 12, size 32 + pixelformat uint32 // offset 44, size 4 + mbus_code uint32 // offset 48, size 4 + reserved [3]uint32 // offset 52, size 12 +} + +type v4l2_frmsizeenum struct { // size 44 + index uint32 // offset 0, size 4 + pixel_format uint32 // offset 4, size 4 + typ uint32 // offset 8, size 4 + discrete v4l2_frmsize_discrete // offset 12, size 8 + _ [24]byte // filler +} + +type v4l2_frmsize_discrete struct { // size 8 + width uint32 // offset 0, size 4 + height uint32 // offset 4, size 4 +} + +type v4l2_frmivalenum struct { // size 52 + index uint32 // offset 0, size 4 + pixel_format uint32 // offset 4, size 4 + width uint32 // offset 8, size 4 + height uint32 // offset 12, size 4 + typ uint32 // offset 16, size 4 + discrete v4l2_fract // offset 20, size 8 + _ [24]byte // filler +} diff --git a/pkg/v4l2/device/videodev2_arm.go b/pkg/v4l2/device/videodev2_arm.go new file mode 100644 index 00000000..098ca5a3 --- /dev/null +++ b/pkg/v4l2/device/videodev2_arm.go @@ -0,0 +1,149 @@ +package device + +const ( + VIDIOC_QUERYCAP = 0x80685600 + VIDIOC_ENUM_FMT = 0xc0405602 + VIDIOC_G_FMT = 0xc0cc5604 + VIDIOC_S_FMT = 0xc0cc5605 + VIDIOC_REQBUFS = 0xc0145608 + VIDIOC_QUERYBUF = 0xc0505609 + + VIDIOC_QBUF = 0xc050560f + VIDIOC_DQBUF = 0xc0505611 + VIDIOC_STREAMON = 0x40045612 + VIDIOC_STREAMOFF = 0x40045613 + VIDIOC_G_PARM = 0xc0cc5615 + VIDIOC_S_PARM = 0xc0cc5616 + + VIDIOC_ENUM_FRAMESIZES = 0xc02c564a + VIDIOC_ENUM_FRAMEINTERVALS = 0xc034564b +) + +const ( + V4L2_BUF_TYPE_VIDEO_CAPTURE = 1 + V4L2_COLORSPACE_DEFAULT = 0 + V4L2_FIELD_NONE = 1 + V4L2_FRMIVAL_TYPE_DISCRETE = 1 + V4L2_FRMSIZE_TYPE_DISCRETE = 1 + V4L2_MEMORY_MMAP = 1 +) + +type v4l2_capability struct { // size 104 + driver [16]byte // offset 0, size 16 + card [32]byte // offset 16, size 32 + bus_info [32]byte // offset 48, size 32 + version uint32 // offset 80, size 4 + capabilities uint32 // offset 84, size 4 + device_caps uint32 // offset 88, size 4 + reserved [3]uint32 // offset 92, size 12 +} + +type v4l2_format struct { // size 204 + typ uint32 // offset 0, size 4 + _ [0]byte // align + pix v4l2_pix_format // offset 4, size 48 + _ [152]byte // filler +} + +type v4l2_pix_format struct { // size 48 + width uint32 // offset 0, size 4 + height uint32 // offset 4, size 4 + pixelformat uint32 // offset 8, size 4 + field uint32 // offset 12, size 4 + bytesperline uint32 // offset 16, size 4 + sizeimage uint32 // offset 20, size 4 + colorspace uint32 // offset 24, size 4 + priv uint32 // offset 28, size 4 + flags uint32 // offset 32, size 4 + ycbcr_enc uint32 // offset 36, size 4 + quantization uint32 // offset 40, size 4 + xfer_func uint32 // offset 44, size 4 +} + +type v4l2_streamparm struct { // size 204 + typ uint32 // offset 0, size 4 + capture v4l2_captureparm // offset 4, size 40 + _ [160]byte // filler +} + +type v4l2_captureparm struct { // size 40 + capability uint32 // offset 0, size 4 + capturemode uint32 // offset 4, size 4 + timeperframe v4l2_fract // offset 8, size 8 + extendedmode uint32 // offset 16, size 4 + readbuffers uint32 // offset 20, size 4 + reserved [4]uint32 // offset 24, size 16 +} + +type v4l2_fract struct { // size 8 + numerator uint32 // offset 0, size 4 + denominator uint32 // offset 4, size 4 +} + +type v4l2_requestbuffers struct { // size 20 + count uint32 // offset 0, size 4 + typ uint32 // offset 4, size 4 + memory uint32 // offset 8, size 4 + capabilities uint32 // offset 12, size 4 + flags uint8 // offset 16, size 1 + reserved [3]uint8 // offset 17, size 3 +} + +type v4l2_buffer struct { // size 80 + index uint32 // offset 0, size 4 + typ uint32 // offset 4, size 4 + bytesused uint32 // offset 8, size 4 + flags uint32 // offset 12, size 4 + field uint32 // offset 16, size 4 + _ [20]byte // align + timecode v4l2_timecode // offset 40, size 16 + sequence uint32 // offset 56, size 4 + memory uint32 // offset 60, size 4 + offset uint32 // offset 64, size 4 + _ [0]byte // align + length uint32 // offset 68, size 4 + _ [8]byte // filler +} + +type v4l2_timecode struct { // size 16 + typ uint32 // offset 0, size 4 + flags uint32 // offset 4, size 4 + frames uint8 // offset 8, size 1 + seconds uint8 // offset 9, size 1 + minutes uint8 // offset 10, size 1 + hours uint8 // offset 11, size 1 + userbits [4]uint8 // offset 12, size 4 +} + +type v4l2_fmtdesc struct { // size 64 + index uint32 // offset 0, size 4 + typ uint32 // offset 4, size 4 + flags uint32 // offset 8, size 4 + description [32]byte // offset 12, size 32 + pixelformat uint32 // offset 44, size 4 + mbus_code uint32 // offset 48, size 4 + reserved [3]uint32 // offset 52, size 12 +} + +type v4l2_frmsizeenum struct { // size 44 + index uint32 // offset 0, size 4 + pixel_format uint32 // offset 4, size 4 + typ uint32 // offset 8, size 4 + discrete v4l2_frmsize_discrete // offset 12, size 8 + _ [24]byte // filler +} + +type v4l2_frmsize_discrete struct { // size 8 + width uint32 // offset 0, size 4 + height uint32 // offset 4, size 4 +} + +type v4l2_frmivalenum struct { // size 52 + index uint32 // offset 0, size 4 + pixel_format uint32 // offset 4, size 4 + width uint32 // offset 8, size 4 + height uint32 // offset 12, size 4 + typ uint32 // offset 16, size 4 + discrete v4l2_fract // offset 20, size 8 + _ [24]byte // filler +} diff --git a/pkg/v4l2/device/videodev2_mipsle.go b/pkg/v4l2/device/videodev2_mipsle.go new file mode 100644 index 00000000..19e8164f --- /dev/null +++ b/pkg/v4l2/device/videodev2_mipsle.go @@ -0,0 +1,149 @@ +package device + +const ( + VIDIOC_QUERYCAP = 0x40685600 + VIDIOC_ENUM_FMT = 0xc0405602 + VIDIOC_G_FMT = 0xc0cc5604 + VIDIOC_S_FMT = 0xc0cc5605 + VIDIOC_REQBUFS = 0xc0145608 + VIDIOC_QUERYBUF = 0xc0505609 + + VIDIOC_QBUF = 0xc050560f + VIDIOC_DQBUF = 0xc0505611 + VIDIOC_STREAMON = 0x80045612 + VIDIOC_STREAMOFF = 0x80045613 + VIDIOC_G_PARM = 0xc0cc5615 + VIDIOC_S_PARM = 0xc0cc5616 + + VIDIOC_ENUM_FRAMESIZES = 0xc02c564a + VIDIOC_ENUM_FRAMEINTERVALS = 0xc034564b +) + +const ( + V4L2_BUF_TYPE_VIDEO_CAPTURE = 1 + V4L2_COLORSPACE_DEFAULT = 0 + V4L2_FIELD_NONE = 1 + V4L2_FRMIVAL_TYPE_DISCRETE = 1 + V4L2_FRMSIZE_TYPE_DISCRETE = 1 + V4L2_MEMORY_MMAP = 1 +) + +type v4l2_capability struct { // size 104 + driver [16]byte // offset 0, size 16 + card [32]byte // offset 16, size 32 + bus_info [32]byte // offset 48, size 32 + version uint32 // offset 80, size 4 + capabilities uint32 // offset 84, size 4 + device_caps uint32 // offset 88, size 4 + reserved [3]uint32 // offset 92, size 12 +} + +type v4l2_format struct { // size 204 + typ uint32 // offset 0, size 4 + _ [0]byte // align + pix v4l2_pix_format // offset 4, size 48 + _ [152]byte // filler +} + +type v4l2_pix_format struct { // size 48 + width uint32 // offset 0, size 4 + height uint32 // offset 4, size 4 + pixelformat uint32 // offset 8, size 4 + field uint32 // offset 12, size 4 + bytesperline uint32 // offset 16, size 4 + sizeimage uint32 // offset 20, size 4 + colorspace uint32 // offset 24, size 4 + priv uint32 // offset 28, size 4 + flags uint32 // offset 32, size 4 + ycbcr_enc uint32 // offset 36, size 4 + quantization uint32 // offset 40, size 4 + xfer_func uint32 // offset 44, size 4 +} + +type v4l2_streamparm struct { // size 204 + typ uint32 // offset 0, size 4 + capture v4l2_captureparm // offset 4, size 40 + _ [160]byte // filler +} + +type v4l2_captureparm struct { // size 40 + capability uint32 // offset 0, size 4 + capturemode uint32 // offset 4, size 4 + timeperframe v4l2_fract // offset 8, size 8 + extendedmode uint32 // offset 16, size 4 + readbuffers uint32 // offset 20, size 4 + reserved [4]uint32 // offset 24, size 16 +} + +type v4l2_fract struct { // size 8 + numerator uint32 // offset 0, size 4 + denominator uint32 // offset 4, size 4 +} + +type v4l2_requestbuffers struct { // size 20 + count uint32 // offset 0, size 4 + typ uint32 // offset 4, size 4 + memory uint32 // offset 8, size 4 + capabilities uint32 // offset 12, size 4 + flags uint8 // offset 16, size 1 + reserved [3]uint8 // offset 17, size 3 +} + +type v4l2_buffer struct { // size 80 + index uint32 // offset 0, size 4 + typ uint32 // offset 4, size 4 + bytesused uint32 // offset 8, size 4 + flags uint32 // offset 12, size 4 + field uint32 // offset 16, size 4 + _ [20]byte // align + timecode v4l2_timecode // offset 40, size 16 + sequence uint32 // offset 56, size 4 + memory uint32 // offset 60, size 4 + offset uint32 // offset 64, size 4 + _ [0]byte // align + length uint32 // offset 68, size 4 + _ [8]byte // filler +} + +type v4l2_timecode struct { // size 16 + typ uint32 // offset 0, size 4 + flags uint32 // offset 4, size 4 + frames uint8 // offset 8, size 1 + seconds uint8 // offset 9, size 1 + minutes uint8 // offset 10, size 1 + hours uint8 // offset 11, size 1 + userbits [4]uint8 // offset 12, size 4 +} + +type v4l2_fmtdesc struct { // size 64 + index uint32 // offset 0, size 4 + typ uint32 // offset 4, size 4 + flags uint32 // offset 8, size 4 + description [32]byte // offset 12, size 32 + pixelformat uint32 // offset 44, size 4 + mbus_code uint32 // offset 48, size 4 + reserved [3]uint32 // offset 52, size 12 +} + +type v4l2_frmsizeenum struct { // size 44 + index uint32 // offset 0, size 4 + pixel_format uint32 // offset 4, size 4 + typ uint32 // offset 8, size 4 + discrete v4l2_frmsize_discrete // offset 12, size 8 + _ [24]byte // filler +} + +type v4l2_frmsize_discrete struct { // size 8 + width uint32 // offset 0, size 4 + height uint32 // offset 4, size 4 +} + +type v4l2_frmivalenum struct { // size 52 + index uint32 // offset 0, size 4 + pixel_format uint32 // offset 4, size 4 + width uint32 // offset 8, size 4 + height uint32 // offset 12, size 4 + typ uint32 // offset 16, size 4 + discrete v4l2_fract // offset 20, size 8 + _ [24]byte // filler +} diff --git a/pkg/v4l2/device/videodev2_test.go b/pkg/v4l2/device/videodev2_test.go deleted file mode 100644 index 2556feef..00000000 --- a/pkg/v4l2/device/videodev2_test.go +++ /dev/null @@ -1,34 +0,0 @@ -package device - -import ( - "runtime" - "testing" - "unsafe" - - "github.com/stretchr/testify/require" -) - -func TestSize(t *testing.T) { - switch runtime.GOARCH { - case "amd64", "arm64": - require.Equal(t, 104, int(unsafe.Sizeof(v4l2_capability{}))) - require.Equal(t, 208, int(unsafe.Sizeof(v4l2_format{}))) - require.Equal(t, 204, int(unsafe.Sizeof(v4l2_streamparm{}))) - require.Equal(t, 20, int(unsafe.Sizeof(v4l2_requestbuffers{}))) - require.Equal(t, 88, int(unsafe.Sizeof(v4l2_buffer{}))) - require.Equal(t, 16, int(unsafe.Sizeof(v4l2_timecode{}))) - require.Equal(t, 64, int(unsafe.Sizeof(v4l2_fmtdesc{}))) - require.Equal(t, 44, int(unsafe.Sizeof(v4l2_frmsizeenum{}))) - require.Equal(t, 52, int(unsafe.Sizeof(v4l2_frmivalenum{}))) - case "386", "arm": - require.Equal(t, 104, int(unsafe.Sizeof(v4l2_capability{}))) - require.Equal(t, 204, int(unsafe.Sizeof(v4l2_format{}))) - require.Equal(t, 204, int(unsafe.Sizeof(v4l2_streamparm{}))) - require.Equal(t, 20, int(unsafe.Sizeof(v4l2_requestbuffers{}))) - require.Equal(t, 68, int(unsafe.Sizeof(v4l2_buffer{}))) - require.Equal(t, 16, int(unsafe.Sizeof(v4l2_timecode{}))) - require.Equal(t, 64, int(unsafe.Sizeof(v4l2_fmtdesc{}))) - require.Equal(t, 44, int(unsafe.Sizeof(v4l2_frmsizeenum{}))) - require.Equal(t, 52, int(unsafe.Sizeof(v4l2_frmivalenum{}))) - } -} diff --git a/pkg/v4l2/device/videodev2_x32.go b/pkg/v4l2/device/videodev2_x32.go deleted file mode 100644 index 4c4db26d..00000000 --- a/pkg/v4l2/device/videodev2_x32.go +++ /dev/null @@ -1,152 +0,0 @@ -//go:build 386 || arm - -package device - -// https://github.com/torvalds/linux/blob/master/include/uapi/linux/videodev2.h - -const ( - VIDIOC_QUERYCAP = 0x80685600 - VIDIOC_ENUM_FMT = 0xc0405602 - VIDIOC_G_FMT = 0xc0cc5604 - VIDIOC_S_FMT = 0xc0cc5605 - VIDIOC_REQBUFS = 0xc0145608 - VIDIOC_QUERYBUF = 0xc0445609 - - VIDIOC_QBUF = 0xc044560f - VIDIOC_DQBUF = 0xc0445611 - VIDIOC_STREAMON = 0x40045612 - VIDIOC_STREAMOFF = 0x40045613 - VIDIOC_G_PARM = 0xc0cc5615 - VIDIOC_S_PARM = 0xc0cc5616 - - VIDIOC_ENUM_FRAMESIZES = 0xc02c564a - VIDIOC_ENUM_FRAMEINTERVALS = 0xc034564b -) - -const ( - V4L2_BUF_TYPE_VIDEO_CAPTURE = 1 - V4L2_COLORSPACE_DEFAULT = 0 - V4L2_FIELD_NONE = 1 - V4L2_FRMIVAL_TYPE_DISCRETE = 1 - V4L2_FRMSIZE_TYPE_DISCRETE = 1 - V4L2_MEMORY_MMAP = 1 -) - -type v4l2_capability struct { - driver [16]byte - card [32]byte - bus_info [32]byte - version uint32 - capabilities uint32 - device_caps uint32 - reserved [3]uint32 -} - -type v4l2_format struct { - typ uint32 - fmt v4l2_pix_format -} - -type v4l2_pix_format struct { - width uint32 // 0 - height uint32 // 4 - pixelformat uint32 // 8 - field uint32 // 12 - bytesperline uint32 // 16 - sizeimage uint32 // 20 - colorspace uint32 // 24 - priv uint32 // 28 - flags uint32 // 32 - ycbcr_enc uint32 // 36 - quantization uint32 // 40 - xfer_func uint32 // 44 - - _ [152]byte // 48 -} - -type v4l2_streamparm struct { - typ uint32 - capture v4l2_captureparm -} - -type v4l2_captureparm struct { - capability uint32 // 0 - capturemode uint32 // 4 - timeperframe v4l2_fract // 8 - extendedmode uint32 // 16 - readbuffers uint32 // 20 - - _ [176]byte // 24 -} - -type v4l2_fract struct { - numerator uint32 - denominator uint32 -} - -type v4l2_requestbuffers struct { - count uint32 - typ uint32 - memory uint32 - capabilities uint32 - flags uint8 - reserved [3]uint8 -} - -type v4l2_buffer struct { - index uint32 // 0 - typ uint32 // 4 - bytesused uint32 // 8 - flags uint32 // 12 - field uint32 // 16 - _ [8]byte // 20 - timecode v4l2_timecode // 28 - sequence uint32 // 44 - memory uint32 // 48 - offset uint32 // 52 - length uint32 // 56 - _ [8]byte // 60 -} - -type v4l2_timecode struct { - typ uint32 - flags uint32 - frames uint8 - seconds uint8 - minutes uint8 - hours uint8 - userbits [4]uint8 -} - -type v4l2_fmtdesc struct { - index uint32 - typ uint32 - flags uint32 - description [32]byte - pixelformat uint32 - mbus_code uint32 - reserved [3]uint32 -} - -type v4l2_frmsizeenum struct { - index uint32 // 0 - pixel_format uint32 // 4 - typ uint32 // 8 - discrete v4l2_frmsize_discrete // 12 - _ [24]byte -} - -type v4l2_frmsize_discrete struct { - width uint32 - height uint32 -} - -type v4l2_frmivalenum struct { - index uint32 - pixel_format uint32 - width uint32 - height uint32 - typ uint32 - discrete v4l2_fract - _ [24]byte -} diff --git a/pkg/v4l2/device/videodev2_x64.go b/pkg/v4l2/device/videodev2_x64.go index 97c3ab95..6e1018e0 100644 --- a/pkg/v4l2/device/videodev2_x64.go +++ b/pkg/v4l2/device/videodev2_x64.go @@ -2,8 +2,6 @@ package device -// https://github.com/torvalds/linux/blob/master/include/uapi/linux/videodev2.h - const ( VIDIOC_QUERYCAP = 0x80685600 VIDIOC_ENUM_FMT = 0xc0405602 @@ -32,122 +30,122 @@ const ( V4L2_MEMORY_MMAP = 1 ) -type v4l2_capability struct { - driver [16]byte - card [32]byte - bus_info [32]byte - version uint32 - capabilities uint32 - device_caps uint32 - reserved [3]uint32 +type v4l2_capability struct { // size 104 + driver [16]byte // offset 0, size 16 + card [32]byte // offset 16, size 32 + bus_info [32]byte // offset 48, size 32 + version uint32 // offset 80, size 4 + capabilities uint32 // offset 84, size 4 + device_caps uint32 // offset 88, size 4 + reserved [3]uint32 // offset 92, size 12 } -type v4l2_format struct { - typ uint64 - fmt v4l2_pix_format +type v4l2_format struct { // size 208 + typ uint32 // offset 0, size 4 + _ [4]byte // align + pix v4l2_pix_format // offset 8, size 48 + _ [152]byte // filler } -type v4l2_pix_format struct { - width uint32 // 0 - height uint32 // 4 - pixelformat uint32 // 8 - field uint32 // 12 - bytesperline uint32 // 16 - sizeimage uint32 // 20 - colorspace uint32 // 24 - priv uint32 // 28 - flags uint32 // 32 - ycbcr_enc uint32 // 36 - quantization uint32 // 40 - xfer_func uint32 // 44 - - _ [152]byte // 48 +type v4l2_pix_format struct { // size 48 + width uint32 // offset 0, size 4 + height uint32 // offset 4, size 4 + pixelformat uint32 // offset 8, size 4 + field uint32 // offset 12, size 4 + bytesperline uint32 // offset 16, size 4 + sizeimage uint32 // offset 20, size 4 + colorspace uint32 // offset 24, size 4 + priv uint32 // offset 28, size 4 + flags uint32 // offset 32, size 4 + ycbcr_enc uint32 // offset 36, size 4 + quantization uint32 // offset 40, size 4 + xfer_func uint32 // offset 44, size 4 } -type v4l2_streamparm struct { - typ uint32 - capture v4l2_captureparm +type v4l2_streamparm struct { // size 204 + typ uint32 // offset 0, size 4 + capture v4l2_captureparm // offset 4, size 40 + _ [160]byte // filler } -type v4l2_captureparm struct { - capability uint32 // 0 - capturemode uint32 // 4 - timeperframe v4l2_fract // 8 - extendedmode uint32 // 16 - readbuffers uint32 // 20 - - _ [176]byte // 24 +type v4l2_captureparm struct { // size 40 + capability uint32 // offset 0, size 4 + capturemode uint32 // offset 4, size 4 + timeperframe v4l2_fract // offset 8, size 8 + extendedmode uint32 // offset 16, size 4 + readbuffers uint32 // offset 20, size 4 + reserved [4]uint32 // offset 24, size 16 } -type v4l2_fract struct { - numerator uint32 - denominator uint32 +type v4l2_fract struct { // size 8 + numerator uint32 // offset 0, size 4 + denominator uint32 // offset 4, size 4 } -type v4l2_requestbuffers struct { - count uint32 - typ uint32 - memory uint32 - capabilities uint32 - flags uint8 - reserved [3]uint8 +type v4l2_requestbuffers struct { // size 20 + count uint32 // offset 0, size 4 + typ uint32 // offset 4, size 4 + memory uint32 // offset 8, size 4 + capabilities uint32 // offset 12, size 4 + flags uint8 // offset 16, size 1 + reserved [3]uint8 // offset 17, size 3 } -type v4l2_buffer struct { - index uint32 // 0 - typ uint32 // 4 - bytesused uint32 // 8 - flags uint32 // 12 - field uint32 // 16 - _ [20]byte // 20 - timecode v4l2_timecode // 40 - sequence uint32 // 56 - memory uint32 // 60 - offset uint32 // 64 - _ [4]byte // 68 - length uint32 // 72 - _ [12]byte // 76 +type v4l2_buffer struct { // size 88 + index uint32 // offset 0, size 4 + typ uint32 // offset 4, size 4 + bytesused uint32 // offset 8, size 4 + flags uint32 // offset 12, size 4 + field uint32 // offset 16, size 4 + _ [20]byte // align + timecode v4l2_timecode // offset 40, size 16 + sequence uint32 // offset 56, size 4 + memory uint32 // offset 60, size 4 + offset uint32 // offset 64, size 4 + _ [4]byte // align + length uint32 // offset 72, size 4 + _ [12]byte // filler } -type v4l2_timecode struct { - typ uint32 - flags uint32 - frames uint8 - seconds uint8 - minutes uint8 - hours uint8 - userbits [4]uint8 +type v4l2_timecode struct { // size 16 + typ uint32 // offset 0, size 4 + flags uint32 // offset 4, size 4 + frames uint8 // offset 8, size 1 + seconds uint8 // offset 9, size 1 + minutes uint8 // offset 10, size 1 + hours uint8 // offset 11, size 1 + userbits [4]uint8 // offset 12, size 4 } -type v4l2_fmtdesc struct { - index uint32 - typ uint32 - flags uint32 - description [32]byte - pixelformat uint32 - mbus_code uint32 - reserved [3]uint32 +type v4l2_fmtdesc struct { // size 64 + index uint32 // offset 0, size 4 + typ uint32 // offset 4, size 4 + flags uint32 // offset 8, size 4 + description [32]byte // offset 12, size 32 + pixelformat uint32 // offset 44, size 4 + mbus_code uint32 // offset 48, size 4 + reserved [3]uint32 // offset 52, size 12 } -type v4l2_frmsizeenum struct { - index uint32 // 0 - pixel_format uint32 // 4 - typ uint32 // 8 - discrete v4l2_frmsize_discrete // 12 - _ [24]byte +type v4l2_frmsizeenum struct { // size 44 + index uint32 // offset 0, size 4 + pixel_format uint32 // offset 4, size 4 + typ uint32 // offset 8, size 4 + discrete v4l2_frmsize_discrete // offset 12, size 8 + _ [24]byte // filler } -type v4l2_frmsize_discrete struct { - width uint32 - height uint32 +type v4l2_frmsize_discrete struct { // size 8 + width uint32 // offset 0, size 4 + height uint32 // offset 4, size 4 } -type v4l2_frmivalenum struct { - index uint32 - pixel_format uint32 - width uint32 - height uint32 - typ uint32 - discrete v4l2_fract - _ [24]byte +type v4l2_frmivalenum struct { // size 52 + index uint32 // offset 0, size 4 + pixel_format uint32 // offset 4, size 4 + width uint32 // offset 8, size 4 + height uint32 // offset 12, size 4 + typ uint32 // offset 16, size 4 + discrete v4l2_fract // offset 20, size 8 + _ [24]byte // filler } From 879ef603fe2c4ec80ee56e0e5243f7c623db91b5 Mon Sep 17 00:00:00 2001 From: Alex X Date: Thu, 9 Jan 2025 00:30:01 +0300 Subject: [PATCH 123/166] Update v4l2 discovery --- internal/v4l2/v4l2_linux.go | 48 ++++++++++++++++++++++--------------- 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/internal/v4l2/v4l2_linux.go b/internal/v4l2/v4l2_linux.go index b9cc82a0..2cd60692 100644 --- a/internal/v4l2/v4l2_linux.go +++ b/internal/v4l2/v4l2_linux.go @@ -44,32 +44,33 @@ func apiV4L2(w http.ResponseWriter, r *http.Request) { formats, _ := dev.ListFormats() for _, fourCC := range formats { - source := &api.Source{} + name, ffmpeg := findFormat(fourCC) + source := &api.Source{Name: name} - for _, format := range device.Formats { - if format.FourCC == fourCC { - source.Name = format.Name - source.URL = "v4l2:device?video=" + path + "&input_format=" + format.FFmpeg + "&video_size=" - break + sizes, _ := dev.ListSizes(fourCC) + for _, wh := range sizes { + if source.Info != "" { + source.Info += " " } - } - if source.Name != "" { - sizes, _ := dev.ListSizes(fourCC) - for _, wh := range sizes { - size := fmt.Sprintf("%dx%d", wh[0], wh[1]) - if source.Info != "" { - source.Info += " " + size - } else { - source.Info = size - source.URL += size + source.Info += fmt.Sprintf("%dx%d", wh[0], wh[1]) + + frameRates, _ := dev.ListFrameRates(fourCC, wh[0], wh[1]) + for _, fr := range frameRates { + source.Info += fmt.Sprintf("@%d", fr) + + if source.URL == "" && ffmpeg != "" { + source.URL = fmt.Sprintf( + "v4l2:device?video=%s&input_format=%s&video_size=%dx%d&framerate=%d", + path, ffmpeg, wh[0], wh[1], fr, + ) } } - } else { - source.Name = string(binary.LittleEndian.AppendUint32(nil, fourCC)) } - sources = append(sources, source) + if source.Info != "" { + sources = append(sources, source) + } } _ = dev.Close() @@ -77,3 +78,12 @@ func apiV4L2(w http.ResponseWriter, r *http.Request) { api.ResponseSources(w, sources) } + +func findFormat(fourCC uint32) (name, ffmpeg string) { + for _, format := range device.Formats { + if format.FourCC == fourCC { + return format.Name, format.FFmpeg + } + } + return string(binary.LittleEndian.AppendUint32(nil, fourCC)), "" +} From 9e673559c4e725628613dfd831e4fc08d09906e1 Mon Sep 17 00:00:00 2001 From: Xiaokui Shu Date: Wed, 8 Jan 2025 21:31:37 -0500 Subject: [PATCH 124/166] Improve log formatting with Msgf --- internal/rtsp/rtsp.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/internal/rtsp/rtsp.go b/internal/rtsp/rtsp.go index 0777219d..2e1d04e8 100644 --- a/internal/rtsp/rtsp.go +++ b/internal/rtsp/rtsp.go @@ -1,7 +1,6 @@ package rtsp import ( - "fmt" "io" "net" "net/url" @@ -239,8 +238,7 @@ func tcpHandler(conn *rtsp.Conn) { if err := conn.Accept(); err != nil { if err == rtsp.FailedAuth { - rAddr := conn.Connection.RemoteAddr - log.Warn().Msg(fmt.Sprintf("[rtsp] failed authentication from %s", rAddr)) + log.Warn().Str("remote_addr", conn.Connection.RemoteAddr).Msg("[rtsp] failed authentication") } else if err != io.EOF { log.WithLevel(level).Err(err).Caller().Send() } From 773e415dff51a471a83e6a4e3bd227b3c445eb99 Mon Sep 17 00:00:00 2001 From: Alex X Date: Thu, 9 Jan 2025 07:18:36 +0300 Subject: [PATCH 125/166] Code refactoring for v4l2 device --- pkg/v4l2/device/README.md | 12 +++++++----- pkg/v4l2/device/{arch.c => videodev2_arch.c} | 20 ++++++++++---------- 2 files changed, 17 insertions(+), 15 deletions(-) rename pkg/v4l2/device/{arch.c => videodev2_arch.c} (88%) diff --git a/pkg/v4l2/device/README.md b/pkg/v4l2/device/README.md index a816b23e..802eca93 100644 --- a/pkg/v4l2/device/README.md +++ b/pkg/v4l2/device/README.md @@ -1,3 +1,5 @@ +# Video For Linux Two + Build on Ubuntu ```bash @@ -7,11 +9,11 @@ sudo apt install gcc-aarch64-linux-gnu binutils sudo apt install gcc-arm-linux-gnueabihf sudo apt install gcc-mipsel-linux-gnu -x86_64-linux-gnu-gcc -w -static arch.c -o arch_x86_64 -i686-linux-gnu-gcc -w -static arch.c -o arch_i686 -aarch64-linux-gnu-gcc -w -static arch.c -o arch_aarch64 -arm-linux-gnueabihf-gcc -w -static arch.c -o arch_armhf -mipsel-linux-gnu-gcc -static arch.c -o arch_mipsel +x86_64-linux-gnu-gcc -w -static videodev2_arch.c -o videodev2_x86_64 +i686-linux-gnu-gcc -w -static videodev2_arch.c -o videodev2_i686 +aarch64-linux-gnu-gcc -w -static videodev2_arch.c -o videodev2_aarch64 +arm-linux-gnueabihf-gcc -w -static videodev2_arch.c -o videodev2_armhf +mipsel-linux-gnu-gcc -w -static videodev2_arch.c -o videodev2_mipsel ``` ## Useful links diff --git a/pkg/v4l2/device/arch.c b/pkg/v4l2/device/videodev2_arch.c similarity index 88% rename from pkg/v4l2/device/arch.c rename to pkg/v4l2/device/videodev2_arch.c index 0b119584..1053a088 100644 --- a/pkg/v4l2/device/arch.c +++ b/pkg/v4l2/device/videodev2_arch.c @@ -7,8 +7,8 @@ #define printstruct(str) printf("type %s struct { // size %lu\n", #str, sizeof(struct str)) #define printmember(str, mem, typ) printf("\t%s %s // offset %lu, size %lu\n", #mem == "type" ? "typ" : #mem, typ, offsetof(struct str, mem), sizeof((struct str){0}.mem)) #define printunimem(str, uni, mem, typ) printf("\t%s %s // offset %lu, size %lu\n", #mem, typ, offsetof(struct str, uni.mem), sizeof((struct str){0}.uni.mem)) -#define printalign1(str, mem2, mem1, siz1) printf("\t_ [%lu]byte // align\n", offsetof(struct str, mem2) - offsetof(struct str, mem1) - siz1) -#define printfiller(str, mem1, siz1) printf("\t_ [%lu]byte // filler\n", sizeof(struct str) - offsetof(struct str, mem1) - siz1) +#define printalign1(str, mem2, mem1) printf("\t_ [%lu]byte // align\n", offsetof(struct str, mem2) - offsetof(struct str, mem1) - sizeof((struct str){0}.mem1)) +#define printfiller(str, mem) printf("\t_ [%lu]byte // filler\n", sizeof(struct str) - offsetof(struct str, mem) - sizeof((struct str){0}.mem)) int main() { printf("const (\n"); @@ -51,9 +51,9 @@ int main() { printstruct(v4l2_format); printmember(v4l2_format, type, "uint32"); - printalign1(v4l2_format, fmt, type, 4); + printalign1(v4l2_format, fmt, type); printunimem(v4l2_format, fmt, pix, "v4l2_pix_format"); - printfiller(v4l2_format, fmt, sizeof(struct v4l2_pix_format)); + printfiller(v4l2_format, fmt.pix); printf("}\n\n"); printstruct(v4l2_pix_format); @@ -74,7 +74,7 @@ int main() { printstruct(v4l2_streamparm); printmember(v4l2_streamparm, type, "uint32"); printunimem(v4l2_streamparm, parm, capture, "v4l2_captureparm"); - printfiller(v4l2_streamparm, parm, sizeof(struct v4l2_captureparm)); + printfiller(v4l2_streamparm, parm.capture); printf("}\n\n"); printstruct(v4l2_captureparm); @@ -106,14 +106,14 @@ int main() { printmember(v4l2_buffer, bytesused, "uint32"); printmember(v4l2_buffer, flags, "uint32"); printmember(v4l2_buffer, field, "uint32"); - printalign1(v4l2_buffer, timecode, field, 4); + printalign1(v4l2_buffer, timecode, field); printmember(v4l2_buffer, timecode, "v4l2_timecode"); printmember(v4l2_buffer, sequence, "uint32"); printmember(v4l2_buffer, memory, "uint32"); printunimem(v4l2_buffer, m, offset, "uint32"); - printalign1(v4l2_buffer, length, m, 4); + printalign1(v4l2_buffer, length, m.offset); printmember(v4l2_buffer, length, "uint32"); - printfiller(v4l2_buffer, length, 4); + printfiller(v4l2_buffer, length); printf("}\n\n"); printstruct(v4l2_timecode); @@ -141,7 +141,7 @@ int main() { printmember(v4l2_frmsizeenum, pixel_format, "uint32"); printmember(v4l2_frmsizeenum, type, "uint32"); printmember(v4l2_frmsizeenum, discrete, "v4l2_frmsize_discrete"); - printfiller(v4l2_frmsizeenum, discrete, sizeof(struct v4l2_frmsize_discrete)); + printfiller(v4l2_frmsizeenum, discrete); printf("}\n\n"); printstruct(v4l2_frmsize_discrete); @@ -156,7 +156,7 @@ int main() { printmember(v4l2_frmivalenum, height, "uint32"); printmember(v4l2_frmivalenum, type, "uint32"); printmember(v4l2_frmivalenum, discrete, "v4l2_fract"); - printfiller(v4l2_frmivalenum, discrete, sizeof(struct v4l2_fract)); + printfiller(v4l2_frmivalenum, discrete); printf("}\n\n"); return 0; From 2ca97a42c5769f57c0605ba433701d8bf4f43eff Mon Sep 17 00:00:00 2001 From: Timo Christeleit Date: Thu, 9 Jan 2025 09:44:23 +0100 Subject: [PATCH 126/166] Update pkg/tapo/client.go Co-authored-by: Sergey Vilgelm <523825+SVilgelm@users.noreply.github.com> --- pkg/tapo/client.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/tapo/client.go b/pkg/tapo/client.go index 78bf5fce..c19267ff 100644 --- a/pkg/tapo/client.go +++ b/pkg/tapo/client.go @@ -291,7 +291,8 @@ func dial(req *http.Request, brand, username, password string) (net.Conn, *http. if err != nil { return nil, nil, err } - _, _ = io.Copy(io.Discard, res.Body) // ignore response body + _, _ = io.Copy(io.Discard, res.Body) // discard leftovers + _ = res.Body.Close() // ignore response body auth := res.Header.Get("WWW-Authenticate") From 0664e46a4bc45107e478619a2c1bdca215dca27a Mon Sep 17 00:00:00 2001 From: Alex X Date: Fri, 10 Jan 2025 12:57:37 +0300 Subject: [PATCH 127/166] Fix v4l2 source for MIPS --- pkg/v4l2/device/README.md | 2 +- pkg/v4l2/device/videodev2_mipsle.go | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/pkg/v4l2/device/README.md b/pkg/v4l2/device/README.md index 802eca93..de686ea0 100644 --- a/pkg/v4l2/device/README.md +++ b/pkg/v4l2/device/README.md @@ -13,7 +13,7 @@ x86_64-linux-gnu-gcc -w -static videodev2_arch.c -o videodev2_x86_64 i686-linux-gnu-gcc -w -static videodev2_arch.c -o videodev2_i686 aarch64-linux-gnu-gcc -w -static videodev2_arch.c -o videodev2_aarch64 arm-linux-gnueabihf-gcc -w -static videodev2_arch.c -o videodev2_armhf -mipsel-linux-gnu-gcc -w -static videodev2_arch.c -o videodev2_mipsel +mipsel-linux-gnu-gcc -w -static videodev2_arch.c -o videodev2_mipsel -D_TIME_BITS=32 ``` ## Useful links diff --git a/pkg/v4l2/device/videodev2_mipsle.go b/pkg/v4l2/device/videodev2_mipsle.go index 19e8164f..cecc54c4 100644 --- a/pkg/v4l2/device/videodev2_mipsle.go +++ b/pkg/v4l2/device/videodev2_mipsle.go @@ -6,10 +6,10 @@ const ( VIDIOC_G_FMT = 0xc0cc5604 VIDIOC_S_FMT = 0xc0cc5605 VIDIOC_REQBUFS = 0xc0145608 - VIDIOC_QUERYBUF = 0xc0505609 + VIDIOC_QUERYBUF = 0xc0445609 - VIDIOC_QBUF = 0xc050560f - VIDIOC_DQBUF = 0xc0505611 + VIDIOC_QBUF = 0xc044560f + VIDIOC_DQBUF = 0xc0445611 VIDIOC_STREAMON = 0x80045612 VIDIOC_STREAMOFF = 0x80045613 VIDIOC_G_PARM = 0xc0cc5615 @@ -89,19 +89,19 @@ type v4l2_requestbuffers struct { // size 20 reserved [3]uint8 // offset 17, size 3 } -type v4l2_buffer struct { // size 80 +type v4l2_buffer struct { // size 68 index uint32 // offset 0, size 4 typ uint32 // offset 4, size 4 bytesused uint32 // offset 8, size 4 flags uint32 // offset 12, size 4 field uint32 // offset 16, size 4 - _ [20]byte // align - timecode v4l2_timecode // offset 40, size 16 - sequence uint32 // offset 56, size 4 - memory uint32 // offset 60, size 4 - offset uint32 // offset 64, size 4 + _ [8]byte // align + timecode v4l2_timecode // offset 28, size 16 + sequence uint32 // offset 44, size 4 + memory uint32 // offset 48, size 4 + offset uint32 // offset 52, size 4 _ [0]byte // align - length uint32 // offset 68, size 4 + length uint32 // offset 56, size 4 _ [8]byte // filler } From 7dc9beb1712ac1074657ca052f7df3ee1230c954 Mon Sep 17 00:00:00 2001 From: Alex X Date: Fri, 10 Jan 2025 14:56:42 +0300 Subject: [PATCH 128/166] Add ws and ffmpeg modules to go2rtc_mjpeg --- examples/go2rtc_mjpeg/main.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/examples/go2rtc_mjpeg/main.go b/examples/go2rtc_mjpeg/main.go index a3e08ff5..3c915b3c 100644 --- a/examples/go2rtc_mjpeg/main.go +++ b/examples/go2rtc_mjpeg/main.go @@ -2,7 +2,9 @@ package main import ( "github.com/AlexxIT/go2rtc/internal/api" + "github.com/AlexxIT/go2rtc/internal/api/ws" "github.com/AlexxIT/go2rtc/internal/app" + "github.com/AlexxIT/go2rtc/internal/ffmpeg" "github.com/AlexxIT/go2rtc/internal/mjpeg" "github.com/AlexxIT/go2rtc/internal/streams" "github.com/AlexxIT/go2rtc/internal/v4l2" @@ -14,6 +16,9 @@ func main() { streams.Init() api.Init() + ws.Init() + + ffmpeg.Init() mjpeg.Init() v4l2.Init() From 83907132b57466e9d094675a2f1c972f8e5bceb8 Mon Sep 17 00:00:00 2001 From: Alex X Date: Fri, 10 Jan 2025 15:01:01 +0300 Subject: [PATCH 129/166] Update about packed and planar YUV formats --- pkg/v4l2/device/device.go | 4 ++-- pkg/v4l2/device/formats.go | 2 +- pkg/v4l2/producer.go | 4 ++-- pkg/y4m/README.md | 14 ++++++++++++++ 4 files changed, 19 insertions(+), 5 deletions(-) diff --git a/pkg/v4l2/device/device.go b/pkg/v4l2/device/device.go index 27bbf350..7f16fd23 100644 --- a/pkg/v4l2/device/device.go +++ b/pkg/v4l2/device/device.go @@ -196,7 +196,7 @@ func (d *Device) StreamOff() (err error) { return ioctl(d.fd, VIDIOC_REQBUFS, unsafe.Pointer(&rb)) } -func (d *Device) Capture(cositedYUV bool) ([]byte, error) { +func (d *Device) Capture(planarYUV bool) ([]byte, error) { dec := v4l2_buffer{ typ: V4L2_BUF_TYPE_VIDEO_CAPTURE, memory: V4L2_MEMORY_MMAP, @@ -206,7 +206,7 @@ func (d *Device) Capture(cositedYUV bool) ([]byte, error) { } buf := make([]byte, dec.bytesused) - if cositedYUV { + if planarYUV { YUYV2YUV(buf, d.bufs[dec.index][:dec.bytesused]) } else { copy(buf, d.bufs[dec.index][:dec.bytesused]) diff --git a/pkg/v4l2/device/formats.go b/pkg/v4l2/device/formats.go index 94d12504..fb54bbd1 100644 --- a/pkg/v4l2/device/formats.go +++ b/pkg/v4l2/device/formats.go @@ -16,7 +16,7 @@ var Formats = []Format{ {V4L2_PIX_FMT_MJPEG, "Motion-JPEG", "mjpeg"}, } -// YUYV2YUV convert [Y0 Cb Y1 Cr] to cosited [Y0Y1... Cb... Cr...] +// YUYV2YUV convert packed YUV to planar YUV func YUYV2YUV(dst, src []byte) { n := len(src) i0 := 0 diff --git a/pkg/v4l2/producer.go b/pkg/v4l2/producer.go index b979d346..87199762 100644 --- a/pkg/v4l2/producer.go +++ b/pkg/v4l2/producer.go @@ -93,10 +93,10 @@ func (c *Producer) Start() error { return err } - cositedYUV := c.Medias[0].Codecs[0].Name == core.CodecRAW + planarYUV := c.Medias[0].Codecs[0].Name == core.CodecRAW for { - buf, err := c.dev.Capture(cositedYUV) + buf, err := c.dev.Capture(planarYUV) if err != nil { return err } diff --git a/pkg/y4m/README.md b/pkg/y4m/README.md index 6f4d863e..ff97813b 100644 --- a/pkg/y4m/README.md +++ b/pkg/y4m/README.md @@ -1,5 +1,19 @@ +## Planar YUV formats + +Packed YUV - yuyv422 - YUYV 4:2:2 +Semi-Planar - nv12 - Y/CbCr 4:2:0 +Planar YUV - yuv420p - Planar YUV 4:2:0 - aka. [cosited](https://manned.org/yuv4mpeg.5) + +``` +[video4linux2,v4l2 @ 0x55fddc42a940] Raw : yuyv422 : YUYV 4:2:2 : 1920x1080 +[video4linux2,v4l2 @ 0x55fddc42a940] Raw : nv12 : Y/CbCr 4:2:0 : 1920x1080 +[video4linux2,v4l2 @ 0x55fddc42a940] Raw : yuv420p : Planar YUV 4:2:0 : 1920x1080 +``` + ## Useful links - https://learn.microsoft.com/en-us/windows/win32/medfound/recommended-8-bit-yuv-formats-for-video-rendering - https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Video_concepts - https://fourcc.org/yuv.php#YV12 +- https://docs.kernel.org/userspace-api/media/v4l/pixfmt-yuv-planar.html +- https://gist.github.com/Jim-Bar/3cbba684a71d1a9d468a6711a6eddbeb From 22e63a7367c66f579ca80f314307a3dae51b90d6 Mon Sep 17 00:00:00 2001 From: Alex X Date: Fri, 10 Jan 2025 19:57:10 +0300 Subject: [PATCH 130/166] Fix comment about OpenIPC --- pkg/flv/producer.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/flv/producer.go b/pkg/flv/producer.go index 7535a8a4..33762d20 100644 --- a/pkg/flv/producer.go +++ b/pkg/flv/producer.go @@ -141,7 +141,7 @@ func (c *Producer) probe() error { // 2. MedaData without stereo key for AAC // 3. Audio header after Video keyframe tag - // OpenIPC camera sends: + // OpenIPC camera (on old firmwares) sends: // 1. Empty video/audio flag // 2. No MetaData packet // 3. Sends a video packet in more than 3 seconds From 485448cbc7c0a3d0fc25cae2b7e24fed6f2b58f2 Mon Sep 17 00:00:00 2001 From: seydx Date: Fri, 24 Jan 2025 12:38:45 +0100 Subject: [PATCH 131/166] initial ring implementation --- internal/ring/init.go | 47 ++++ main.go | 2 + pkg/ring/api.go | 416 +++++++++++++++++++++++++++++++ pkg/ring/client.go | 551 ++++++++++++++++++++++++++++++++++++++++++ www/add.html | 25 +- 5 files changed, 1040 insertions(+), 1 deletion(-) create mode 100644 internal/ring/init.go create mode 100644 pkg/ring/api.go create mode 100644 pkg/ring/client.go diff --git a/internal/ring/init.go b/internal/ring/init.go new file mode 100644 index 00000000..24a91ac6 --- /dev/null +++ b/internal/ring/init.go @@ -0,0 +1,47 @@ +package ring + +import ( + "net/http" + + "github.com/AlexxIT/go2rtc/internal/api" + "github.com/AlexxIT/go2rtc/internal/streams" + "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/AlexxIT/go2rtc/pkg/ring" +) + +func Init() { + streams.HandleFunc("ring", func(source string) (core.Producer, error) { + return ring.Dial(source) + }) + + api.HandleFunc("api/ring", apiRing) +} + +func apiRing(w http.ResponseWriter, r *http.Request) { + query := r.URL.Query() + refreshToken := query.Get("refresh_token") + + ringAPI, err := ring.NewRingRestClient(ring.RefreshTokenAuth{RefreshToken: refreshToken}, nil) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + devices, err := ringAPI.FetchRingDevices() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + var items []*api.Source + + for _, camera := range devices.AllCameras { + query.Set("device_id", camera.DeviceID) + + items = append(items, &api.Source{ + Name: camera.Description, URL: "ring:?" + query.Encode(), + }) + } + + api.ResponseSources(w, items) +} diff --git a/main.go b/main.go index db8de9f4..db3983cc 100644 --- a/main.go +++ b/main.go @@ -25,6 +25,7 @@ import ( "github.com/AlexxIT/go2rtc/internal/nest" "github.com/AlexxIT/go2rtc/internal/ngrok" "github.com/AlexxIT/go2rtc/internal/onvif" + "github.com/AlexxIT/go2rtc/internal/ring" "github.com/AlexxIT/go2rtc/internal/roborock" "github.com/AlexxIT/go2rtc/internal/rtmp" "github.com/AlexxIT/go2rtc/internal/rtsp" @@ -80,6 +81,7 @@ func main() { mpegts.Init() // mpegts passive source roborock.Init() // roborock source homekit.Init() // homekit source + ring.Init() // ring source nest.Init() // nest source bubble.Init() // bubble source expr.Init() // expr source diff --git a/pkg/ring/api.go b/pkg/ring/api.go new file mode 100644 index 00000000..faebf6b9 --- /dev/null +++ b/pkg/ring/api.go @@ -0,0 +1,416 @@ +package ring + +import ( + "bytes" + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "net/http" + "reflect" + "time" +) + +type RefreshTokenAuth struct { + RefreshToken string +} + +// AuthConfig represents the decoded refresh token data +type AuthConfig struct { + RT string `json:"rt"` // Refresh Token + HID string `json:"hid"` // Hardware ID +} + +// AuthTokenResponse represents the response from the authentication endpoint +type AuthTokenResponse struct { + AccessToken string `json:"access_token"` + ExpiresIn int `json:"expires_in"` + RefreshToken string `json:"refresh_token"` + Scope string `json:"scope"` // Always "client" + TokenType string `json:"token_type"` // Always "Bearer" +} + +// SocketTicketRequest represents the request to get a socket ticket +type SocketTicketResponse struct { + Ticket string `json:"ticket"` + ResponseTimestamp int64 `json:"response_timestamp"` +} + +// RingRestClient handles authentication and requests to Ring API +type RingRestClient struct { + httpClient *http.Client + authConfig *AuthConfig + hardwareID string + authToken *AuthTokenResponse + auth RefreshTokenAuth + onTokenRefresh func(string) // Callback when refresh token is updated +} + +// CameraKind represents the different types of Ring cameras +type CameraKind string + +const ( + Doorbot CameraKind = "doorbot" + Doorbell CameraKind = "doorbell" + DoorbellV3 CameraKind = "doorbell_v3" + DoorbellV4 CameraKind = "doorbell_v4" + DoorbellV5 CameraKind = "doorbell_v5" + DoorbellOyster CameraKind = "doorbell_oyster" + DoorbellPortal CameraKind = "doorbell_portal" + DoorbellScallop CameraKind = "doorbell_scallop" + DoorbellScallopLite CameraKind = "doorbell_scallop_lite" + DoorbellGraham CameraKind = "doorbell_graham_cracker" + LpdV1 CameraKind = "lpd_v1" + LpdV2 CameraKind = "lpd_v2" + LpdV4 CameraKind = "lpd_v4" + JboxV1 CameraKind = "jbox_v1" + StickupCam CameraKind = "stickup_cam" + StickupCamV3 CameraKind = "stickup_cam_v3" + StickupCamElite CameraKind = "stickup_cam_elite" + StickupCamLongfin CameraKind = "stickup_cam_longfin" + StickupCamLunar CameraKind = "stickup_cam_lunar" + SpotlightV2 CameraKind = "spotlightw_v2" + HpCamV1 CameraKind = "hp_cam_v1" + HpCamV2 CameraKind = "hp_cam_v2" + StickupCamV4 CameraKind = "stickup_cam_v4" + FloodlightV1 CameraKind = "floodlight_v1" + FloodlightV2 CameraKind = "floodlight_v2" + FloodlightPro CameraKind = "floodlight_pro" + CocoaCamera CameraKind = "cocoa_camera" + CocoaDoorbell CameraKind = "cocoa_doorbell" + CocoaFloodlight CameraKind = "cocoa_floodlight" + CocoaSpotlight CameraKind = "cocoa_spotlight" + StickupCamMini CameraKind = "stickup_cam_mini" + OnvifCamera CameraKind = "onvif_camera" +) + +// RingDeviceType represents different types of Ring devices +type RingDeviceType string + +const ( + IntercomHandsetAudio RingDeviceType = "intercom_handset_audio" + OnvifCameraType RingDeviceType = "onvif_camera" +) + +// CameraData contains common fields for all camera types +type CameraData struct { + ID float64 `json:"id"` + Description string `json:"description"` + DeviceID string `json:"device_id"` + Kind string `json:"kind"` + LocationID string `json:"location_id"` +} + +// RingDevicesResponse represents the response from the Ring API +type RingDevicesResponse struct { + Doorbots []CameraData `json:"doorbots"` + AuthorizedDoorbots []CameraData `json:"authorized_doorbots"` + StickupCams []CameraData `json:"stickup_cams"` + AllCameras []CameraData `json:"all_cameras"` + Chimes []CameraData `json:"chimes"` + Other []map[string]interface{} `json:"other"` +} + +const ( + clientAPIBaseURL = "https://api.ring.com/clients_api/" + deviceAPIBaseURL = "https://api.ring.com/devices/v1/" + commandsAPIBaseURL = "https://api.ring.com/commands/v1/" + appAPIBaseURL = "https://prd-api-us.prd.rings.solutions/api/v1/" + oauthURL = "https://oauth.ring.com/oauth/token" + apiVersion = 11 + defaultTimeout = 20 * time.Second + maxRetries = 3 +) + +// NewRingRestClient creates a new Ring client instance +func NewRingRestClient(auth RefreshTokenAuth, onTokenRefresh func(string)) (*RingRestClient, error) { + client := &RingRestClient{ + httpClient: &http.Client{ + Timeout: defaultTimeout, + }, + auth: auth, + onTokenRefresh: onTokenRefresh, + hardwareID: generateHardwareID(), + } + + // check if refresh token is provided + if auth.RefreshToken == "" { + return nil, fmt.Errorf("refresh token is required") + } + + if config, err := parseAuthConfig(auth.RefreshToken); err == nil { + client.authConfig = config + client.hardwareID = config.HID + } + + return client, nil +} + +// Request makes an authenticated request to the Ring API +func (c *RingRestClient) Request(method, url string, body interface{}) ([]byte, error) { + // Ensure we have a valid auth token + if err := c.ensureAuth(); err != nil { + return nil, fmt.Errorf("authentication failed: %w", err) + } + + var bodyReader io.Reader + if body != nil { + jsonBody, err := json.Marshal(body) + if err != nil { + return nil, fmt.Errorf("failed to marshal request body: %w", err) + } + bodyReader = bytes.NewReader(jsonBody) + } + + // Create request + req, err := http.NewRequest(method, url, bodyReader) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + // Set headers + req.Header.Set("Authorization", "Bearer "+c.authToken.AccessToken) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json") + req.Header.Set("hardware_id", c.hardwareID) + req.Header.Set("User-Agent", "android:com.ringapp") + + // Make request with retries + var resp *http.Response + var responseBody []byte + + for attempt := 0; attempt <= maxRetries; attempt++ { + resp, err = c.httpClient.Do(req) + if err != nil { + if attempt == maxRetries { + return nil, fmt.Errorf("request failed after %d retries: %w", maxRetries, err) + } + time.Sleep(5 * time.Second) + continue + } + defer resp.Body.Close() + + responseBody, err = io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %w", err) + } + + // Handle 401 by refreshing auth and retrying + if resp.StatusCode == http.StatusUnauthorized { + c.authToken = nil // Force token refresh + if attempt == maxRetries { + return nil, fmt.Errorf("authentication failed after %d retries", maxRetries) + } + if err := c.ensureAuth(); err != nil { + return nil, fmt.Errorf("failed to refresh authentication: %w", err) + } + req.Header.Set("Authorization", "Bearer "+c.authToken.AccessToken) + continue + } + + // Handle other error status codes + if resp.StatusCode >= 400 { + return nil, fmt.Errorf("request failed with status %d: %s", resp.StatusCode, string(responseBody)) + } + + break + } + + return responseBody, nil +} + +// ensureAuth ensures we have a valid auth token +func (c *RingRestClient) ensureAuth() error { + if c.authToken != nil { + return nil + } + + var grantData = map[string]string{ + "grant_type": "refresh_token", + "refresh_token": c.authConfig.RT, + } + + // Add common fields + grantData["client_id"] = "ring_official_android" + grantData["scope"] = "client" + + // Make auth request + body, err := json.Marshal(grantData) + if err != nil { + return fmt.Errorf("failed to marshal auth request: %w", err) + } + + req, err := http.NewRequest("POST", oauthURL, bytes.NewReader(body)) + if err != nil { + return fmt.Errorf("failed to create auth request: %w", err) + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json") + req.Header.Set("hardware_id", c.hardwareID) + req.Header.Set("User-Agent", "android:com.ringapp") + req.Header.Set("2fa-support", "true") + + resp, err := c.httpClient.Do(req) + if err != nil { + return fmt.Errorf("auth request failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode == http.StatusPreconditionFailed { + return fmt.Errorf("2FA required. Please see documentation for handling 2FA") + } + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return fmt.Errorf("auth request failed with status %d: %s", resp.StatusCode, string(body)) + } + + var authResp AuthTokenResponse + if err := json.NewDecoder(resp.Body).Decode(&authResp); err != nil { + return fmt.Errorf("failed to decode auth response: %w", err) + } + + // Update auth config and refresh token + c.authToken = &authResp + c.authConfig = &AuthConfig{ + RT: authResp.RefreshToken, + HID: c.hardwareID, + } + + // Encode and notify about new refresh token + if c.onTokenRefresh != nil { + newRefreshToken := encodeAuthConfig(c.authConfig) + c.onTokenRefresh(newRefreshToken) + } + + return nil +} + +// Helper functions for auth config encoding/decoding +func parseAuthConfig(refreshToken string) (*AuthConfig, error) { + decoded, err := base64.StdEncoding.DecodeString(refreshToken) + if err != nil { + return nil, err + } + + var config AuthConfig + if err := json.Unmarshal(decoded, &config); err != nil { + // Handle legacy format where refresh token is the raw token + return &AuthConfig{RT: refreshToken}, nil + } + + return &config, nil +} + +func encodeAuthConfig(config *AuthConfig) string { + jsonBytes, _ := json.Marshal(config) + return base64.StdEncoding.EncodeToString(jsonBytes) +} + +// API URL helpers +func ClientAPI(path string) string { + return clientAPIBaseURL + path +} + +func DeviceAPI(path string) string { + return deviceAPIBaseURL + path +} + +func CommandsAPI(path string) string { + return commandsAPIBaseURL + path +} + +func AppAPI(path string) string { + return appAPIBaseURL + path +} + +// FetchRingDevices gets all Ring devices and categorizes them +func (c *RingRestClient) FetchRingDevices() (*RingDevicesResponse, error) { + response, err := c.Request("GET", ClientAPI("ring_devices"), nil) + if err != nil { + return nil, fmt.Errorf("failed to fetch ring devices: %w", err) + } + + var devices RingDevicesResponse + if err := json.Unmarshal(response, &devices); err != nil { + return nil, fmt.Errorf("failed to unmarshal devices response: %w", err) + } + + // Process "other" devices + var onvifCameras []CameraData + var intercoms []CameraData + + for _, device := range devices.Other { + kind, ok := device["kind"].(string) + if !ok { + continue + } + + switch RingDeviceType(kind) { + case OnvifCameraType: + var camera CameraData + if deviceJson, err := json.Marshal(device); err == nil { + if err := json.Unmarshal(deviceJson, &camera); err == nil { + onvifCameras = append(onvifCameras, camera) + } + } + case IntercomHandsetAudio: + var intercom CameraData + if deviceJson, err := json.Marshal(device); err == nil { + if err := json.Unmarshal(deviceJson, &intercom); err == nil { + intercoms = append(intercoms, intercom) + } + } + } + } + + // Combine all cameras into AllCameras slice + allCameras := make([]CameraData, 0) + allCameras = append(allCameras, interfaceSlice(devices.Doorbots)...) + allCameras = append(allCameras, interfaceSlice(devices.StickupCams)...) + allCameras = append(allCameras, interfaceSlice(devices.AuthorizedDoorbots)...) + allCameras = append(allCameras, interfaceSlice(onvifCameras)...) + allCameras = append(allCameras, interfaceSlice(intercoms)...) + + devices.AllCameras = allCameras + + return &devices, nil +} + +func (c *RingRestClient) GetSocketTicket() (*SocketTicketResponse, error) { + response, err := c.Request("POST", AppAPI("clap/ticket/request/signalsocket"), nil) + if err != nil { + return nil, fmt.Errorf("failed to fetch socket ticket: %w", err) + } + + var ticket SocketTicketResponse + if err := json.Unmarshal(response, &ticket); err != nil { + return nil, fmt.Errorf("failed to unmarshal socket ticket response: %w", err) + } + + return &ticket, nil +} + +func generateHardwareID() string { + h := sha256.New() + h.Write([]byte("ring-client-go2rtc")) + return hex.EncodeToString(h.Sum(nil)[:16]) +} + +func interfaceSlice(slice interface{}) []CameraData { + s := reflect.ValueOf(slice) + if s.Kind() != reflect.Slice { + return nil + } + + ret := make([]CameraData, s.Len()) + for i := 0; i < s.Len(); i++ { + if camera, ok := s.Index(i).Interface().(CameraData); ok { + ret[i] = camera + } + } + return ret +} \ No newline at end of file diff --git a/pkg/ring/client.go b/pkg/ring/client.go new file mode 100644 index 00000000..db8e2eaa --- /dev/null +++ b/pkg/ring/client.go @@ -0,0 +1,551 @@ +package ring + +import ( + "encoding/json" + "errors" + "fmt" + "net/url" + "time" + + "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/AlexxIT/go2rtc/pkg/webrtc" + "github.com/google/uuid" + "github.com/gorilla/websocket" + pion "github.com/pion/webrtc/v3" + "github.com/rs/zerolog/log" +) + +type Client struct { + conn *webrtc.Conn + ws *websocket.Conn + api *RingRestClient + camera *CameraData + dialogID string + sessionID string + done chan struct{} +} + +type SessionBody struct { + DoorbotID int `json:"doorbot_id"` + SessionID string `json:"session_id"` +} + +type AnswerMessage struct { + Method string `json:"method"` // "sdp" + Body struct { + SessionBody + SDP string `json:"sdp"` + Type string `json:"type"` // "answer" + } `json:"body"` +} + +type IceCandidateMessage struct { + Method string `json:"method"` // "ice" + Body struct { + SessionBody + Ice string `json:"ice"` + MLineIndex int `json:"mlineindex"` + } `json:"body"` +} + +type SessionMessage struct { + Method string `json:"method"` // "session_created" or "session_started" + Body SessionBody `json:"body"` +} + +type PongMessage struct { + Method string `json:"method"` // "pong" + Body SessionBody `json:"body"` +} + +type NotificationMessage struct { + Method string `json:"method"` // "notification" + Body struct { + SessionBody + IsOK bool `json:"is_ok"` + Text string `json:"text"` + } `json:"body"` +} + +type StreamInfoMessage struct { + Method string `json:"method"` // "stream_info" + Body struct { + SessionBody + Transcoding bool `json:"transcoding"` + TranscodingReason string `json:"transcoding_reason"` + } `json:"body"` +} + +type CloseMessage struct { + Method string `json:"method"` // "close" + Body struct { + SessionBody + Reason struct { + Code int `json:"code"` + Text string `json:"text"` + } `json:"reason"` + } `json:"body"` +} + +type BaseMessage struct { + Method string `json:"method"` + Body map[string]any `json:"body"` +} + +// Close reason codes +const ( + CloseReasonNormalClose = 0 + CloseReasonAuthenticationFailed = 5 + CloseReasonTimeout = 6 +) + +func Dial(rawURL string) (*Client, error) { + // 1. Create Ring Rest API client + u, err := url.Parse(rawURL) + if err != nil { + return nil, err + } + + query := u.Query() + encodedToken := query.Get("refresh_token") + deviceID := query.Get("device_id") + + if encodedToken == "" || deviceID == "" { + return nil, errors.New("ring: wrong query") + } + + // URL-decode the refresh token + refreshToken, err := url.QueryUnescape(encodedToken) + if err != nil { + return nil, fmt.Errorf("ring: invalid refresh token encoding: %w", err) + } + + println("Connecting to Ring WebSocket") + println("Refresh Token: ", refreshToken) + println("Device ID: ", deviceID) + + // Initialize Ring API client + ringAPI, err := NewRingRestClient(RefreshTokenAuth{RefreshToken: refreshToken}, nil) + if err != nil { + return nil, err + } + + // Get camera details + devices, err := ringAPI.FetchRingDevices() + if err != nil { + return nil, err + } + + var camera *CameraData + for _, cam := range devices.AllCameras { + if fmt.Sprint(cam.DeviceID) == deviceID { + camera = &cam + break + } + } + if camera == nil { + return nil, errors.New("ring: camera not found") + } + + // 2. Connect to signaling server + ticket, err := ringAPI.GetSocketTicket() + if err != nil { + return nil, err + } + + println("WebSocket Ticket: ", ticket.Ticket) + println("WebSocket ResponseTimestamp: ", ticket.ResponseTimestamp) + + // Create WebSocket connection + wsURL := fmt.Sprintf("wss://api.prod.signalling.ring.devices.a2z.com/ws?api_version=4.0&auth_type=ring_solutions&client_id=ring_site-%s&token=%s", + uuid.NewString(), url.QueryEscape(ticket.Ticket)) + + println("WebSocket URL: ", wsURL) + + conn, _, err := websocket.DefaultDialer.Dial(wsURL, map[string][]string{ + "User-Agent": {"android:com.ringapp"}, + }) + if err != nil { + return nil, err + } + + println("WebSocket handshake completed successfully") + + // 3. Create Peer Connection + println("Creating Peer Connection") + + conf := pion.Configuration{ + ICEServers: []pion.ICEServer{ + {URLs: []string{ + "stun:stun.kinesisvideo.us-east-1.amazonaws.com:443", + "stun:stun.kinesisvideo.us-east-2.amazonaws.com:443", + "stun:stun.kinesisvideo.us-west-2.amazonaws.com:443", + "stun:stun.l.google.com:19302", + "stun:stun1.l.google.com:19302", + "stun:stun2.l.google.com:19302", + "stun:stun3.l.google.com:19302", + "stun:stun4.l.google.com:19302", + }}, + }, + ICETransportPolicy: pion.ICETransportPolicyAll, + BundlePolicy: pion.BundlePolicyBalanced, + } + + api, err := webrtc.NewAPI() + if err != nil { + println("Failed to create WebRTC API") + conn.Close() + return nil, err + } + + pc, err := api.NewPeerConnection(conf) + if err != nil { + println("Failed to create Peer Connection") + conn.Close() + return nil, err + } + + println("Peer Connection created") + + // protect from sending ICE candidate before Offer + var sendOffer core.Waiter + + // protect from blocking on errors + defer sendOffer.Done(nil) + + // waiter will wait PC error or WS error or nil (connection OK) + var connState core.Waiter + + prod := webrtc.NewConn(pc) + prod.FormatName = "ring/webrtc" + prod.Mode = core.ModeActiveProducer + prod.Protocol = "ws" + prod.URL = rawURL + + client := &Client{ + ws: conn, + api: ringAPI, + camera: camera, + dialogID: uuid.NewString(), + conn: prod, + done: make(chan struct{}), + } + + prod.Listen(func(msg any) { + switch msg := msg.(type) { + case *pion.ICECandidate: + _ = sendOffer.Wait() + + iceCandidate := msg.ToJSON() + + icePayload := map[string]interface{}{ + "ice": iceCandidate.Candidate, + "mlineindex": iceCandidate.SDPMLineIndex, + } + + if err = client.sendSessionMessage("ice", icePayload); err != nil { + connState.Done(err) + return + } + + case pion.PeerConnectionState: + switch msg { + case pion.PeerConnectionStateConnecting: + case pion.PeerConnectionStateConnected: + connState.Done(nil) + default: + connState.Done(errors.New("ring: " + msg.String())) + } + } + }) + + // Setup media configuration + medias := []*core.Media{ + { + Kind: core.KindAudio, + Direction: core.DirectionSendRecv, + Codecs: []*core.Codec{ + { + Name: "opus", + ClockRate: 48000, + Channels: 2, + }, + }, + }, + { + Kind: core.KindVideo, + Direction: core.DirectionRecvonly, + Codecs: []*core.Codec{ + { + Name: "H264", + ClockRate: 90000, + }, + }, + }, + } + + // 4. Create offer + offer, err := prod.CreateOffer(medias) + if err != nil { + println("Failed to create offer") + client.Stop() + return nil, err + } + + println("Offer created") + println(offer) + + // 5. Send offer + offerPayload := map[string]interface{}{ + "stream_options": map[string]bool{ + "audio_enabled": true, + "video_enabled": true, + }, + "sdp": offer, + } + + if err = client.sendSessionMessage("live_view", offerPayload); err != nil { + println("Failed to send live_view message") + client.Stop() + return nil, err + } + + sendOffer.Done(nil) + + // Ring expects a ping message every 5 seconds + go func() { + ticker := time.NewTicker(5 * time.Second) + defer ticker.Stop() + + for { + select { + case <-client.done: + return + case <-ticker.C: + if pc.ConnectionState() == pion.PeerConnectionStateConnected { + if err := client.sendSessionMessage("ping", nil); err != nil { + println("Failed to send ping:", err) + return + } + } + } + } + }() + + go func() { + var err error + + // will be closed when conn will be closed + defer func() { + connState.Done(err) + }() + + for { + select { + case <-client.done: + return + default: + var res BaseMessage + if err = conn.ReadJSON(&res); err != nil { + select { + case <-client.done: + return + default: + } + + if websocket.IsCloseError(err, websocket.CloseNormalClosure) { + println("WebSocket closed normally") + } else { + println("Failed to read JSON message:", err) + client.Stop() + } + return + } + + body, _ := json.Marshal(res.Body) + bodyStr := string(body) + + println("Received message:", res.Method) + println("Message body:", bodyStr) + + // check if "doorbot_id" is present and matches the camera ID + if _, ok := res.Body["doorbot_id"]; !ok { + println("Received message without doorbot_id") + continue + } + + doorbotID := res.Body["doorbot_id"].(float64) + if doorbotID != float64(client.camera.ID) { + println("Received message from unknown doorbot:", doorbotID) + continue + } + + if res.Method == "session_created" || res.Method == "session_started" { + if _, ok := res.Body["session_id"]; ok && client.sessionID == "" { + client.sessionID = res.Body["session_id"].(string) + println("Session established:", client.sessionID) + } + } + + if _, ok := res.Body["session_id"]; ok { + if res.Body["session_id"].(string) != client.sessionID { + println("Received message with wrong session ID") + continue + } + } + + rawMsg, _ := json.Marshal(res) + + switch res.Method { + case "sdp": + // 6. Get answer + var msg AnswerMessage + if err = json.Unmarshal(rawMsg, &msg); err != nil { + println("Failed to parse SDP message:", err) + client.Stop() + return + } + if err = prod.SetAnswer(msg.Body.SDP); err != nil { + println("Failed to set answer:", err) + client.Stop() + return + } + if err = client.activateSession(); err != nil { + println("Failed to activate session:", err) + client.Stop() + return + } + + case "ice": + // 7. Continue to receiving candidates + var msg IceCandidateMessage + if err = json.Unmarshal(rawMsg, &msg); err != nil { + println("Failed to parse ICE message:", err) + client.Stop() + return + } + + if err = prod.AddCandidate(msg.Body.Ice); err != nil { + client.Stop() + return + } + + case "close": + client.Stop() + return + + case "pong": + // Ignore + continue + } + } + } + }() + + if err = connState.Wait(); err != nil { + return nil, err + } + + return client, nil +} + +func (c *Client) activateSession() error { + println("Activating session") + + if err := c.sendSessionMessage("activate_session", nil); err != nil { + return err + } + + streamPayload := map[string]interface{}{ + "audio_enabled": true, + "video_enabled": true, + } + + if err := c.sendSessionMessage("stream_options", streamPayload); err != nil { + return err + } + + println("Session activated") + + return nil +} + +func (c *Client) sendSessionMessage(method string, body map[string]interface{}) error { + if body == nil { + body = make(map[string]interface{}) + } + + body["doorbot_id"] = c.camera.ID + if c.sessionID != "" { + body["session_id"] = c.sessionID + } + + msg := map[string]interface{}{ + "method": method, + "dialog_id": c.dialogID, + "body": body, + } + + println("Sending session message:", method) + + if err := c.ws.WriteJSON(msg); err != nil { + log.Error().Err(err).Msg("Failed to send JSON message") + return err + } + + return nil +} + +func (c *Client) GetMedias() []*core.Media { + println("Getting medias") + return c.conn.GetMedias() +} + +func (c *Client) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) { + println("Getting track") + return c.conn.GetTrack(media, codec) +} + +func (c *Client) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error { + println("Adding track") + return c.conn.AddTrack(media, codec, track) +} + +func (c *Client) Start() error { + println("Starting client") + return c.conn.Start() +} + +func (c *Client) Stop() error { + select { + case <-c.done: + return nil + default: + println("Stopping client") + close(c.done) + } + + if c.conn != nil { + _ = c.conn.Stop() + c.conn = nil + } + + if c.ws != nil { + closePayload := map[string]interface{}{ + "reason": map[string]interface{}{ + "code": CloseReasonNormalClose, + "text": "", + }, + } + + _ = c.sendSessionMessage("close", closePayload) + _ = c.ws.Close() + c.ws = nil + } + + return nil +} + +func (c *Client) MarshalJSON() ([]byte, error) { + return c.conn.MarshalJSON() +} \ No newline at end of file diff --git a/www/add.html b/www/add.html index 49e954d3..1190f07e 100644 --- a/www/add.html +++ b/www/add.html @@ -35,7 +35,7 @@ function drawTable(table, data) { const cols = ['id', 'name', 'info', 'url', 'location']; const th = (row) => cols.reduce((html, k) => k in row ? `${html}${k}` : html, '') + ''; - const td = (row) => cols.reduce((html, k) => k in row ? `${html}${row[k]}` : html, '') + ''; + const td = (row) => cols.reduce((html, k) => k in row ? `${html}${row[k]}` : html, '') + ''; const thead = th(data.sources[0]); const tbody = data.sources.reduce((html, source) => `${html}${td(source)}`, ''); @@ -218,6 +218,29 @@ }); + +
    +
    + + +
    +
    +
    +
    From 17bba4d4a28644fb99bdffc5e6bef62d690ea3fd Mon Sep 17 00:00:00 2001 From: seydx Date: Fri, 24 Jan 2025 12:47:25 +0100 Subject: [PATCH 132/166] skip empty ICE candidates --- pkg/ring/client.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pkg/ring/client.go b/pkg/ring/client.go index db8e2eaa..47790664 100644 --- a/pkg/ring/client.go +++ b/pkg/ring/client.go @@ -238,6 +238,11 @@ func Dial(rawURL string) (*Client, error) { iceCandidate := msg.ToJSON() + // skip empty ICE candidates + if iceCandidate.Candidate == "" { + return + } + icePayload := map[string]interface{}{ "ice": iceCandidate.Candidate, "mlineindex": iceCandidate.SDPMLineIndex, @@ -425,6 +430,12 @@ func Dial(rawURL string) (*Client, error) { return } + // check for empty ICE candidate + if msg.Body.Ice == "" { + println("Received empty ICE candidate") + continue + } + if err = prod.AddCandidate(msg.Body.Ice); err != nil { client.Stop() return From bceb024588813aa8f8a809ffcd99d9684b8dec57 Mon Sep 17 00:00:00 2001 From: seydx Date: Fri, 24 Jan 2025 17:37:50 +0100 Subject: [PATCH 133/166] enable speaker for two way audio --- pkg/ring/client.go | 40 +++++++++++++++++++++++----------------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/pkg/ring/client.go b/pkg/ring/client.go index 47790664..8fa27bd5 100644 --- a/pkg/ring/client.go +++ b/pkg/ring/client.go @@ -16,9 +16,9 @@ import ( ) type Client struct { - conn *webrtc.Conn - ws *websocket.Conn api *RingRestClient + ws *websocket.Conn + prod *webrtc.Conn camera *CameraData dialogID string sessionID string @@ -162,7 +162,7 @@ func Dial(rawURL string) (*Client, error) { println("WebSocket URL: ", wsURL) - conn, _, err := websocket.DefaultDialer.Dial(wsURL, map[string][]string{ + ws, _, err := websocket.DefaultDialer.Dial(wsURL, map[string][]string{ "User-Agent": {"android:com.ringapp"}, }) if err != nil { @@ -194,14 +194,14 @@ func Dial(rawURL string) (*Client, error) { api, err := webrtc.NewAPI() if err != nil { println("Failed to create WebRTC API") - conn.Close() + ws.Close() return nil, err } pc, err := api.NewPeerConnection(conf) if err != nil { println("Failed to create Peer Connection") - conn.Close() + ws.Close() return nil, err } @@ -223,11 +223,11 @@ func Dial(rawURL string) (*Client, error) { prod.URL = rawURL client := &Client{ - ws: conn, api: ringAPI, + ws: ws, + prod: prod, camera: camera, dialogID: uuid.NewString(), - conn: prod, done: make(chan struct{}), } @@ -351,7 +351,7 @@ func Dial(rawURL string) (*Client, error) { return default: var res BaseMessage - if err = conn.ReadJSON(&res); err != nil { + if err = ws.ReadJSON(&res); err != nil { select { case <-client.done: return @@ -509,22 +509,28 @@ func (c *Client) sendSessionMessage(method string, body map[string]interface{}) func (c *Client) GetMedias() []*core.Media { println("Getting medias") - return c.conn.GetMedias() + return c.prod.GetMedias() } func (c *Client) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) { println("Getting track") - return c.conn.GetTrack(media, codec) + return c.prod.GetTrack(media, codec) } func (c *Client) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error { - println("Adding track") - return c.conn.AddTrack(media, codec, track) + // Enable speaker + speakerPayload := map[string]interface{}{ + "stealth_mode": false, + } + + _ = c.sendSessionMessage("camera_options", speakerPayload); + + return c.prod.AddTrack(media, codec, track) } func (c *Client) Start() error { println("Starting client") - return c.conn.Start() + return c.prod.Start() } func (c *Client) Stop() error { @@ -536,9 +542,9 @@ func (c *Client) Stop() error { close(c.done) } - if c.conn != nil { - _ = c.conn.Stop() - c.conn = nil + if c.prod != nil { + _ = c.prod.Stop() + c.prod = nil } if c.ws != nil { @@ -558,5 +564,5 @@ func (c *Client) Stop() error { } func (c *Client) MarshalJSON() ([]byte, error) { - return c.conn.MarshalJSON() + return c.prod.MarshalJSON() } \ No newline at end of file From c9682ca64da755000a6c12b32db3df00cad5525c Mon Sep 17 00:00:00 2001 From: seydx Date: Fri, 24 Jan 2025 18:02:47 +0100 Subject: [PATCH 134/166] remove unnecessary prints and use mutex for ws --- pkg/ring/client.go | 83 ++++++++++------------------------------------ 1 file changed, 18 insertions(+), 65 deletions(-) diff --git a/pkg/ring/client.go b/pkg/ring/client.go index 8fa27bd5..b48727d9 100644 --- a/pkg/ring/client.go +++ b/pkg/ring/client.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "net/url" + "sync" "time" "github.com/AlexxIT/go2rtc/pkg/core" @@ -12,7 +13,6 @@ import ( "github.com/google/uuid" "github.com/gorilla/websocket" pion "github.com/pion/webrtc/v3" - "github.com/rs/zerolog/log" ) type Client struct { @@ -22,6 +22,7 @@ type Client struct { camera *CameraData dialogID string sessionID string + wsMutex sync.Mutex done chan struct{} } @@ -120,10 +121,6 @@ func Dial(rawURL string) (*Client, error) { return nil, fmt.Errorf("ring: invalid refresh token encoding: %w", err) } - println("Connecting to Ring WebSocket") - println("Refresh Token: ", refreshToken) - println("Device ID: ", deviceID) - // Initialize Ring API client ringAPI, err := NewRingRestClient(RefreshTokenAuth{RefreshToken: refreshToken}, nil) if err != nil { @@ -153,15 +150,10 @@ func Dial(rawURL string) (*Client, error) { return nil, err } - println("WebSocket Ticket: ", ticket.Ticket) - println("WebSocket ResponseTimestamp: ", ticket.ResponseTimestamp) - // Create WebSocket connection wsURL := fmt.Sprintf("wss://api.prod.signalling.ring.devices.a2z.com/ws?api_version=4.0&auth_type=ring_solutions&client_id=ring_site-%s&token=%s", uuid.NewString(), url.QueryEscape(ticket.Ticket)) - println("WebSocket URL: ", wsURL) - ws, _, err := websocket.DefaultDialer.Dial(wsURL, map[string][]string{ "User-Agent": {"android:com.ringapp"}, }) @@ -169,11 +161,7 @@ func Dial(rawURL string) (*Client, error) { return nil, err } - println("WebSocket handshake completed successfully") - // 3. Create Peer Connection - println("Creating Peer Connection") - conf := pion.Configuration{ ICEServers: []pion.ICEServer{ {URLs: []string{ @@ -193,20 +181,16 @@ func Dial(rawURL string) (*Client, error) { api, err := webrtc.NewAPI() if err != nil { - println("Failed to create WebRTC API") ws.Close() return nil, err } pc, err := api.NewPeerConnection(conf) if err != nil { - println("Failed to create Peer Connection") ws.Close() return nil, err } - println("Peer Connection created") - // protect from sending ICE candidate before Offer var sendOffer core.Waiter @@ -292,14 +276,10 @@ func Dial(rawURL string) (*Client, error) { // 4. Create offer offer, err := prod.CreateOffer(medias) if err != nil { - println("Failed to create offer") client.Stop() return nil, err } - println("Offer created") - println(offer) - // 5. Send offer offerPayload := map[string]interface{}{ "stream_options": map[string]bool{ @@ -310,7 +290,6 @@ func Dial(rawURL string) (*Client, error) { } if err = client.sendSessionMessage("live_view", offerPayload); err != nil { - println("Failed to send live_view message") client.Stop() return nil, err } @@ -329,7 +308,6 @@ func Dial(rawURL string) (*Client, error) { case <-ticker.C: if pc.ConnectionState() == pion.PeerConnectionStateConnected { if err := client.sendSessionMessage("ping", nil); err != nil { - println("Failed to send ping:", err) return } } @@ -358,43 +336,30 @@ func Dial(rawURL string) (*Client, error) { default: } - if websocket.IsCloseError(err, websocket.CloseNormalClosure) { - println("WebSocket closed normally") - } else { - println("Failed to read JSON message:", err) - client.Stop() - } + client.Stop() return } - body, _ := json.Marshal(res.Body) - bodyStr := string(body) - - println("Received message:", res.Method) - println("Message body:", bodyStr) - - // check if "doorbot_id" is present and matches the camera ID + // check if "doorbot_id" is present if _, ok := res.Body["doorbot_id"]; !ok { - println("Received message without doorbot_id") continue } + // check if the message is from the correct doorbot doorbotID := res.Body["doorbot_id"].(float64) if doorbotID != float64(client.camera.ID) { - println("Received message from unknown doorbot:", doorbotID) continue } + // check if the message is from the correct session if res.Method == "session_created" || res.Method == "session_started" { if _, ok := res.Body["session_id"]; ok && client.sessionID == "" { client.sessionID = res.Body["session_id"].(string) - println("Session established:", client.sessionID) } } if _, ok := res.Body["session_id"]; ok { if res.Body["session_id"].(string) != client.sessionID { - println("Received message with wrong session ID") continue } } @@ -406,17 +371,14 @@ func Dial(rawURL string) (*Client, error) { // 6. Get answer var msg AnswerMessage if err = json.Unmarshal(rawMsg, &msg); err != nil { - println("Failed to parse SDP message:", err) client.Stop() return } if err = prod.SetAnswer(msg.Body.SDP); err != nil { - println("Failed to set answer:", err) client.Stop() return } if err = client.activateSession(); err != nil { - println("Failed to activate session:", err) client.Stop() return } @@ -425,15 +387,12 @@ func Dial(rawURL string) (*Client, error) { // 7. Continue to receiving candidates var msg IceCandidateMessage if err = json.Unmarshal(rawMsg, &msg); err != nil { - println("Failed to parse ICE message:", err) - client.Stop() - return + break } // check for empty ICE candidate if msg.Body.Ice == "" { - println("Received empty ICE candidate") - continue + break } if err = prod.AddCandidate(msg.Body.Ice); err != nil { @@ -461,8 +420,6 @@ func Dial(rawURL string) (*Client, error) { } func (c *Client) activateSession() error { - println("Activating session") - if err := c.sendSessionMessage("activate_session", nil); err != nil { return err } @@ -476,12 +433,13 @@ func (c *Client) activateSession() error { return err } - println("Session activated") - return nil } func (c *Client) sendSessionMessage(method string, body map[string]interface{}) error { + c.wsMutex.Lock() + defer c.wsMutex.Unlock() + if body == nil { body = make(map[string]interface{}) } @@ -497,10 +455,7 @@ func (c *Client) sendSessionMessage(method string, body map[string]interface{}) "body": body, } - println("Sending session message:", method) - if err := c.ws.WriteJSON(msg); err != nil { - log.Error().Err(err).Msg("Failed to send JSON message") return err } @@ -508,28 +463,27 @@ func (c *Client) sendSessionMessage(method string, body map[string]interface{}) } func (c *Client) GetMedias() []*core.Media { - println("Getting medias") return c.prod.GetMedias() } func (c *Client) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) { - println("Getting track") return c.prod.GetTrack(media, codec) } func (c *Client) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error { - // Enable speaker - speakerPayload := map[string]interface{}{ - "stealth_mode": false, + if media.Kind == core.KindAudio { + // Enable speaker + speakerPayload := map[string]interface{}{ + "stealth_mode": false, + } + + _ = c.sendSessionMessage("camera_options", speakerPayload); } - _ = c.sendSessionMessage("camera_options", speakerPayload); - return c.prod.AddTrack(media, codec, track) } func (c *Client) Start() error { - println("Starting client") return c.prod.Start() } @@ -538,7 +492,6 @@ func (c *Client) Stop() error { case <-c.done: return nil default: - println("Stopping client") close(c.done) } From 2c5f1e0417b01d495a70b8d1f709afb9636a01bb Mon Sep 17 00:00:00 2001 From: seydx Date: Fri, 24 Jan 2025 19:37:17 +0100 Subject: [PATCH 135/166] add 2fa --- internal/ring/init.go | 87 +++++++++++++---- pkg/ring/api.go | 219 +++++++++++++++++++++++++++++++++--------- www/add.html | 36 ++++++- 3 files changed, 272 insertions(+), 70 deletions(-) diff --git a/internal/ring/init.go b/internal/ring/init.go index 24a91ac6..521c137a 100644 --- a/internal/ring/init.go +++ b/internal/ring/init.go @@ -1,7 +1,9 @@ package ring import ( + "encoding/json" "net/http" + "net/url" "github.com/AlexxIT/go2rtc/internal/api" "github.com/AlexxIT/go2rtc/internal/streams" @@ -18,30 +20,75 @@ func Init() { } func apiRing(w http.ResponseWriter, r *http.Request) { - query := r.URL.Query() - refreshToken := query.Get("refresh_token") + query := r.URL.Query() + var ringAPI *ring.RingRestClient + var err error - ringAPI, err := ring.NewRingRestClient(ring.RefreshTokenAuth{RefreshToken: refreshToken}, nil) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } + // Check auth method + if email := query.Get("email"); email != "" { + // Email/Password Flow + password := query.Get("password") + code := query.Get("code") - devices, err := ringAPI.FetchRingDevices() - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } + ringAPI, err = ring.NewRingRestClient(ring.EmailAuth{ + Email: email, + Password: password, + }, nil) - var items []*api.Source + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } - for _, camera := range devices.AllCameras { - query.Set("device_id", camera.DeviceID) + // Try authentication (this will trigger 2FA if needed) + if _, err = ringAPI.GetAuth(code); err != nil { + if ringAPI.Using2FA { + // Return 2FA prompt + json.NewEncoder(w).Encode(map[string]interface{}{ + "needs_2fa": true, + "prompt": ringAPI.PromptFor2FA, + }) + return + } + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + } else { + // Refresh Token Flow + refreshToken := query.Get("refresh_token") + if refreshToken == "" { + http.Error(w, "either email/password or refresh_token is required", http.StatusBadRequest) + return + } - items = append(items, &api.Source{ - Name: camera.Description, URL: "ring:?" + query.Encode(), - }) - } + ringAPI, err = ring.NewRingRestClient(ring.RefreshTokenAuth{ + RefreshToken: refreshToken, + }, nil) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + } - api.ResponseSources(w, items) + // Fetch devices + devices, err := ringAPI.FetchRingDevices() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // Create clean query with only required parameters + cleanQuery := url.Values{} + cleanQuery.Set("refresh_token", ringAPI.RefreshToken) + + var items []*api.Source + for _, camera := range devices.AllCameras { + cleanQuery.Set("device_id", camera.DeviceID) + items = append(items, &api.Source{ + Name: camera.Description, + URL: "ring:?" + cleanQuery.Encode(), + }) + } + + api.ResponseSources(w, items) } diff --git a/pkg/ring/api.go b/pkg/ring/api.go index faebf6b9..e025e031 100644 --- a/pkg/ring/api.go +++ b/pkg/ring/api.go @@ -10,6 +10,7 @@ import ( "io" "net/http" "reflect" + "strings" "time" ) @@ -17,6 +18,11 @@ type RefreshTokenAuth struct { RefreshToken string } +type EmailAuth struct { + Email string + Password string +} + // AuthConfig represents the decoded refresh token data type AuthConfig struct { RT string `json:"rt"` // Refresh Token @@ -32,6 +38,14 @@ type AuthTokenResponse struct { TokenType string `json:"token_type"` // Always "Bearer" } +type Auth2faResponse struct { + Error string `json:"error"` + ErrorDescription string `json:"error_description"` + TSVState string `json:"tsv_state"` + Phone string `json:"phone"` + NextTimeInSecs int `json:"next_time_in_secs"` +} + // SocketTicketRequest represents the request to get a socket ticket type SocketTicketResponse struct { Ticket string `json:"ticket"` @@ -40,17 +54,42 @@ type SocketTicketResponse struct { // RingRestClient handles authentication and requests to Ring API type RingRestClient struct { - httpClient *http.Client - authConfig *AuthConfig - hardwareID string - authToken *AuthTokenResponse - auth RefreshTokenAuth - onTokenRefresh func(string) // Callback when refresh token is updated + httpClient *http.Client + authConfig *AuthConfig + hardwareID string + authToken *AuthTokenResponse + Using2FA bool + PromptFor2FA string + RefreshToken string + auth interface{} // EmailAuth or RefreshTokenAuth + onTokenRefresh func(string) } // CameraKind represents the different types of Ring cameras type CameraKind string +// CameraData contains common fields for all camera types +type CameraData struct { + ID float64 `json:"id"` + Description string `json:"description"` + DeviceID string `json:"device_id"` + Kind string `json:"kind"` + LocationID string `json:"location_id"` +} + +// RingDeviceType represents different types of Ring devices +type RingDeviceType string + +// RingDevicesResponse represents the response from the Ring API +type RingDevicesResponse struct { + Doorbots []CameraData `json:"doorbots"` + AuthorizedDoorbots []CameraData `json:"authorized_doorbots"` + StickupCams []CameraData `json:"stickup_cams"` + AllCameras []CameraData `json:"all_cameras"` + Chimes []CameraData `json:"chimes"` + Other []map[string]interface{} `json:"other"` +} + const ( Doorbot CameraKind = "doorbot" Doorbell CameraKind = "doorbell" @@ -86,33 +125,11 @@ const ( OnvifCamera CameraKind = "onvif_camera" ) -// RingDeviceType represents different types of Ring devices -type RingDeviceType string - const ( IntercomHandsetAudio RingDeviceType = "intercom_handset_audio" OnvifCameraType RingDeviceType = "onvif_camera" ) -// CameraData contains common fields for all camera types -type CameraData struct { - ID float64 `json:"id"` - Description string `json:"description"` - DeviceID string `json:"device_id"` - Kind string `json:"kind"` - LocationID string `json:"location_id"` -} - -// RingDevicesResponse represents the response from the Ring API -type RingDevicesResponse struct { - Doorbots []CameraData `json:"doorbots"` - AuthorizedDoorbots []CameraData `json:"authorized_doorbots"` - StickupCams []CameraData `json:"stickup_cams"` - AllCameras []CameraData `json:"all_cameras"` - Chimes []CameraData `json:"chimes"` - Other []map[string]interface{} `json:"other"` -} - const ( clientAPIBaseURL = "https://api.ring.com/clients_api/" deviceAPIBaseURL = "https://api.ring.com/devices/v1/" @@ -125,27 +142,37 @@ const ( ) // NewRingRestClient creates a new Ring client instance -func NewRingRestClient(auth RefreshTokenAuth, onTokenRefresh func(string)) (*RingRestClient, error) { - client := &RingRestClient{ - httpClient: &http.Client{ - Timeout: defaultTimeout, - }, - auth: auth, - onTokenRefresh: onTokenRefresh, - hardwareID: generateHardwareID(), - } +func NewRingRestClient(auth interface{}, onTokenRefresh func(string)) (*RingRestClient, error) { + client := &RingRestClient{ + httpClient: &http.Client{Timeout: defaultTimeout}, + onTokenRefresh: onTokenRefresh, + hardwareID: generateHardwareID(), + auth: auth, + } - // check if refresh token is provided - if auth.RefreshToken == "" { - return nil, fmt.Errorf("refresh token is required") - } + switch a := auth.(type) { + case RefreshTokenAuth: + if a.RefreshToken == "" { + return nil, fmt.Errorf("refresh token is required") + } + + config, err := parseAuthConfig(a.RefreshToken) + if err != nil { + return nil, fmt.Errorf("failed to parse refresh token: %w", err) + } - if config, err := parseAuthConfig(auth.RefreshToken); err == nil { client.authConfig = config - client.hardwareID = config.HID - } + client.hardwareID = config.HID + client.RefreshToken = a.RefreshToken + case EmailAuth: + if a.Email == "" || a.Password == "" { + return nil, fmt.Errorf("email and password are required") + } + default: + return nil, fmt.Errorf("invalid auth type") + } - return client, nil + return client, nil } // Request makes an authenticated request to the Ring API @@ -289,6 +316,108 @@ func (c *RingRestClient) ensureAuth() error { return nil } +// getAuth makes an authentication request to the Ring API +func (c *RingRestClient) GetAuth(twoFactorAuthCode string) (*AuthTokenResponse, error) { + var grantData map[string]string + + if c.authConfig != nil && twoFactorAuthCode == "" { + grantData = map[string]string{ + "grant_type": "refresh_token", + "refresh_token": c.authConfig.RT, + } + } else { + authEmail, ok := c.auth.(EmailAuth) + if !ok { + return nil, fmt.Errorf("invalid auth type for email authentication") + } + grantData = map[string]string{ + "grant_type": "password", + "username": authEmail.Email, + "password": authEmail.Password, + } + } + + grantData["client_id"] = "ring_official_android" + grantData["scope"] = "client" + + body, err := json.Marshal(grantData) + if err != nil { + return nil, fmt.Errorf("failed to marshal auth request: %w", err) + } + + req, err := http.NewRequest("POST", oauthURL, bytes.NewReader(body)) + if err != nil { + return nil, err + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json") + req.Header.Set("hardware_id", c.hardwareID) + req.Header.Set("User-Agent", "android:com.ringapp") + req.Header.Set("2fa-support", "true") + if twoFactorAuthCode != "" { + req.Header.Set("2fa-code", twoFactorAuthCode) + } + + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + // Handle 2FA Responses + if resp.StatusCode == http.StatusPreconditionFailed || + (resp.StatusCode == http.StatusBadRequest && strings.Contains(resp.Header.Get("WWW-Authenticate"), "Verification Code")) { + + var tfaResp Auth2faResponse + if err := json.NewDecoder(resp.Body).Decode(&tfaResp); err != nil { + return nil, err + } + + c.Using2FA = true + if resp.StatusCode == http.StatusBadRequest { + c.PromptFor2FA = "Invalid 2fa code entered. Please try again." + return nil, fmt.Errorf("invalid 2FA code") + } + + if tfaResp.TSVState != "" { + prompt := "from your authenticator app" + if tfaResp.TSVState != "totp" { + prompt = fmt.Sprintf("sent to %s via %s", tfaResp.Phone, tfaResp.TSVState) + } + c.PromptFor2FA = fmt.Sprintf("Please enter the code %s", prompt) + } else { + c.PromptFor2FA = "Please enter the code sent to your text/email" + } + + return nil, fmt.Errorf("2FA required") + } + + // Handle errors + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("auth request failed with status %d: %s", resp.StatusCode, string(body)) + } + + var authResp AuthTokenResponse + if err := json.NewDecoder(resp.Body).Decode(&authResp); err != nil { + return nil, fmt.Errorf("failed to decode auth response: %w", err) + } + + c.authToken = &authResp + c.authConfig = &AuthConfig{ + RT: authResp.RefreshToken, + HID: c.hardwareID, + } + + c.RefreshToken = encodeAuthConfig(c.authConfig) + if c.onTokenRefresh != nil { + c.onTokenRefresh(c.RefreshToken) + } + + return c.authToken, nil +} + // Helper functions for auth config encoding/decoding func parseAuthConfig(refreshToken string) (*AuthConfig, error) { decoded, err := base64.StdEncoding.DecodeString(refreshToken) diff --git a/www/add.html b/www/add.html index 1190f07e..7dae63d4 100644 --- a/www/add.html +++ b/www/add.html @@ -220,7 +220,16 @@
    -
    + + + + + +
    +
    @@ -231,15 +240,32 @@ ev.target.nextElementSibling.style.display = 'block'; }); - document.getElementById('ring-form').addEventListener('submit', async ev => { + async function handleRingAuth(ev) { ev.preventDefault(); - const query = new URLSearchParams(new FormData(ev.target)); const url = new URL('api/ring?' + query.toString(), location.href); const r = await fetch(url, {cache: 'no-cache'}); - await getSources('ring-table', r); - }); + const data = await r.json(); + + if (data.needs_2fa) { + document.getElementById('tfa-field').style.display = 'block'; + document.getElementById('tfa-prompt').textContent = data.prompt || 'Enter 2FA code'; + return; + } + + if (!r.ok) { + const table = document.getElementById('ring-table'); + table.innerText = data.error || 'Unknown error'; + return; + } + + const table = document.getElementById('ring-table'); + drawTable(table, data); + } + + document.getElementById('ring-credentials-form').addEventListener('submit', handleRingAuth); + document.getElementById('ring-token-form').addEventListener('submit', handleRingAuth); From 0651a09a3c0f19250dcc2ff0845195450b6c8684 Mon Sep 17 00:00:00 2001 From: seydx Date: Fri, 24 Jan 2025 22:35:04 +0100 Subject: [PATCH 136/166] add snapshot producer --- internal/ring/init.go | 8 + pkg/ring/client.go | 587 ++++++++++++++++++++++-------------------- pkg/ring/snapshot.go | 64 +++++ 3 files changed, 376 insertions(+), 283 deletions(-) create mode 100644 pkg/ring/snapshot.go diff --git a/internal/ring/init.go b/internal/ring/init.go index 521c137a..bc49178b 100644 --- a/internal/ring/init.go +++ b/internal/ring/init.go @@ -84,10 +84,18 @@ func apiRing(w http.ResponseWriter, r *http.Request) { var items []*api.Source for _, camera := range devices.AllCameras { cleanQuery.Set("device_id", camera.DeviceID) + + // Stream source items = append(items, &api.Source{ Name: camera.Description, URL: "ring:?" + cleanQuery.Encode(), }) + + // Snapshot source + items = append(items, &api.Source{ + Name: camera.Description + " Snapshot", + URL: "ring:?" + cleanQuery.Encode() + "&snapshot", + }) } api.ResponseSources(w, items) diff --git a/pkg/ring/client.go b/pkg/ring/client.go index b48727d9..c432ecf9 100644 --- a/pkg/ring/client.go +++ b/pkg/ring/client.go @@ -18,7 +18,7 @@ import ( type Client struct { api *RingRestClient ws *websocket.Conn - prod *webrtc.Conn + prod core.Producer camera *CameraData dialogID string sessionID string @@ -101,322 +101,337 @@ const ( ) func Dial(rawURL string) (*Client, error) { - // 1. Create Ring Rest API client - u, err := url.Parse(rawURL) - if err != nil { - return nil, err - } + // 1. Parse URL and validate basic params + u, err := url.Parse(rawURL) + if err != nil { + return nil, err + } - query := u.Query() - encodedToken := query.Get("refresh_token") - deviceID := query.Get("device_id") + query := u.Query() + encodedToken := query.Get("refresh_token") + deviceID := query.Get("device_id") + _, isSnapshot := query["snapshot"] - if encodedToken == "" || deviceID == "" { - return nil, errors.New("ring: wrong query") - } + if encodedToken == "" || deviceID == "" { + return nil, errors.New("ring: wrong query") + } - // URL-decode the refresh token - refreshToken, err := url.QueryUnescape(encodedToken) - if err != nil { - return nil, fmt.Errorf("ring: invalid refresh token encoding: %w", err) - } + // URL-decode the refresh token + refreshToken, err := url.QueryUnescape(encodedToken) + if err != nil { + return nil, fmt.Errorf("ring: invalid refresh token encoding: %w", err) + } - // Initialize Ring API client - ringAPI, err := NewRingRestClient(RefreshTokenAuth{RefreshToken: refreshToken}, nil) - if err != nil { - return nil, err - } + // Initialize Ring API client + ringAPI, err := NewRingRestClient(RefreshTokenAuth{RefreshToken: refreshToken}, nil) + if err != nil { + return nil, err + } - // Get camera details - devices, err := ringAPI.FetchRingDevices() - if err != nil { - return nil, err - } + // Get camera details + devices, err := ringAPI.FetchRingDevices() + if err != nil { + return nil, err + } - var camera *CameraData - for _, cam := range devices.AllCameras { - if fmt.Sprint(cam.DeviceID) == deviceID { - camera = &cam - break - } - } - if camera == nil { - return nil, errors.New("ring: camera not found") - } + var camera *CameraData + for _, cam := range devices.AllCameras { + if fmt.Sprint(cam.DeviceID) == deviceID { + camera = &cam + break + } + } + if camera == nil { + return nil, errors.New("ring: camera not found") + } - // 2. Connect to signaling server - ticket, err := ringAPI.GetSocketTicket() - if err != nil { - return nil, err - } + // Create base client + client := &Client{ + api: ringAPI, + camera: camera, + dialogID: uuid.NewString(), + done: make(chan struct{}), + } - // Create WebSocket connection - wsURL := fmt.Sprintf("wss://api.prod.signalling.ring.devices.a2z.com/ws?api_version=4.0&auth_type=ring_solutions&client_id=ring_site-%s&token=%s", - uuid.NewString(), url.QueryEscape(ticket.Ticket)) + // Check if snapshot request + if isSnapshot { + client.prod = NewSnapshotProducer(ringAPI, camera) + return client, nil + } - ws, _, err := websocket.DefaultDialer.Dial(wsURL, map[string][]string{ - "User-Agent": {"android:com.ringapp"}, - }) - if err != nil { - return nil, err - } + // If not snapshot, continue with WebRTC setup + ticket, err := ringAPI.GetSocketTicket() + if err != nil { + return nil, err + } - // 3. Create Peer Connection - conf := pion.Configuration{ - ICEServers: []pion.ICEServer{ - {URLs: []string{ - "stun:stun.kinesisvideo.us-east-1.amazonaws.com:443", - "stun:stun.kinesisvideo.us-east-2.amazonaws.com:443", - "stun:stun.kinesisvideo.us-west-2.amazonaws.com:443", - "stun:stun.l.google.com:19302", - "stun:stun1.l.google.com:19302", - "stun:stun2.l.google.com:19302", - "stun:stun3.l.google.com:19302", - "stun:stun4.l.google.com:19302", - }}, - }, - ICETransportPolicy: pion.ICETransportPolicyAll, + // Create WebSocket connection + wsURL := fmt.Sprintf("wss://api.prod.signalling.ring.devices.a2z.com/ws?api_version=4.0&auth_type=ring_solutions&client_id=ring_site-%s&token=%s", + uuid.NewString(), url.QueryEscape(ticket.Ticket)) + + client.ws, _, err = websocket.DefaultDialer.Dial(wsURL, map[string][]string{ + "User-Agent": {"android:com.ringapp"}, + }) + if err != nil { + return nil, err + } + + // Create Peer Connection + conf := pion.Configuration{ + ICEServers: []pion.ICEServer{ + {URLs: []string{ + "stun:stun.kinesisvideo.us-east-1.amazonaws.com:443", + "stun:stun.kinesisvideo.us-east-2.amazonaws.com:443", + "stun:stun.kinesisvideo.us-west-2.amazonaws.com:443", + "stun:stun.l.google.com:19302", + "stun:stun1.l.google.com:19302", + "stun:stun2.l.google.com:19302", + "stun:stun3.l.google.com:19302", + "stun:stun4.l.google.com:19302", + }}, + }, + ICETransportPolicy: pion.ICETransportPolicyAll, BundlePolicy: pion.BundlePolicyBalanced, - } + } - api, err := webrtc.NewAPI() - if err != nil { - ws.Close() - return nil, err - } + api, err := webrtc.NewAPI() + if err != nil { + client.ws.Close() + return nil, err + } - pc, err := api.NewPeerConnection(conf) - if err != nil { - ws.Close() - return nil, err - } + pc, err := api.NewPeerConnection(conf) + if err != nil { + client.ws.Close() + return nil, err + } - // protect from sending ICE candidate before Offer - var sendOffer core.Waiter + // protect from sending ICE candidate before Offer + var sendOffer core.Waiter - // protect from blocking on errors - defer sendOffer.Done(nil) + // protect from blocking on errors + defer sendOffer.Done(nil) - // waiter will wait PC error or WS error or nil (connection OK) - var connState core.Waiter + // waiter will wait PC error or WS error or nil (connection OK) + var connState core.Waiter - prod := webrtc.NewConn(pc) - prod.FormatName = "ring/webrtc" - prod.Mode = core.ModeActiveProducer - prod.Protocol = "ws" - prod.URL = rawURL + prod := webrtc.NewConn(pc) + prod.FormatName = "ring/webrtc" + prod.Mode = core.ModeActiveProducer + prod.Protocol = "ws" + prod.URL = rawURL - client := &Client{ - api: ringAPI, - ws: ws, - prod: prod, - camera: camera, - dialogID: uuid.NewString(), - done: make(chan struct{}), - } + client.prod = prod - prod.Listen(func(msg any) { - switch msg := msg.(type) { - case *pion.ICECandidate: - _ = sendOffer.Wait() + prod.Listen(func(msg any) { + switch msg := msg.(type) { + case *pion.ICECandidate: + _ = sendOffer.Wait() - iceCandidate := msg.ToJSON() + iceCandidate := msg.ToJSON() - // skip empty ICE candidates - if iceCandidate.Candidate == "" { - return - } + // skip empty ICE candidates + if iceCandidate.Candidate == "" { + return + } - icePayload := map[string]interface{}{ - "ice": iceCandidate.Candidate, - "mlineindex": iceCandidate.SDPMLineIndex, - } - - if err = client.sendSessionMessage("ice", icePayload); err != nil { - connState.Done(err) - return - } + icePayload := map[string]interface{}{ + "ice": iceCandidate.Candidate, + "mlineindex": iceCandidate.SDPMLineIndex, + } + + if err = client.sendSessionMessage("ice", icePayload); err != nil { + connState.Done(err) + return + } - case pion.PeerConnectionState: - switch msg { - case pion.PeerConnectionStateConnecting: - case pion.PeerConnectionStateConnected: - connState.Done(nil) - default: - connState.Done(errors.New("ring: " + msg.String())) - } - } - }) + case pion.PeerConnectionState: + switch msg { + case pion.PeerConnectionStateConnecting: + case pion.PeerConnectionStateConnected: + connState.Done(nil) + default: + connState.Done(errors.New("ring: " + msg.String())) + } + } + }) - // Setup media configuration - medias := []*core.Media{ - { - Kind: core.KindAudio, - Direction: core.DirectionSendRecv, - Codecs: []*core.Codec{ - { - Name: "opus", - ClockRate: 48000, - Channels: 2, - }, - }, - }, - { - Kind: core.KindVideo, - Direction: core.DirectionRecvonly, - Codecs: []*core.Codec{ - { - Name: "H264", - ClockRate: 90000, - }, - }, - }, - } + // Setup media configuration + medias := []*core.Media{ + { + Kind: core.KindAudio, + Direction: core.DirectionSendRecv, + Codecs: []*core.Codec{ + { + Name: "opus", + ClockRate: 48000, + Channels: 2, + }, + }, + }, + { + Kind: core.KindVideo, + Direction: core.DirectionRecvonly, + Codecs: []*core.Codec{ + { + Name: "H264", + ClockRate: 90000, + }, + }, + }, + } - // 4. Create offer - offer, err := prod.CreateOffer(medias) - if err != nil { - client.Stop() - return nil, err - } + // Create offer + offer, err := prod.CreateOffer(medias) + if err != nil { + client.Stop() + return nil, err + } - // 5. Send offer - offerPayload := map[string]interface{}{ - "stream_options": map[string]bool{ - "audio_enabled": true, - "video_enabled": true, - }, - "sdp": offer, - } + // Send offer + offerPayload := map[string]interface{}{ + "stream_options": map[string]bool{ + "audio_enabled": true, + "video_enabled": true, + }, + "sdp": offer, + } - if err = client.sendSessionMessage("live_view", offerPayload); err != nil { - client.Stop() - return nil, err - } + if err = client.sendSessionMessage("live_view", offerPayload); err != nil { + client.Stop() + return nil, err + } - sendOffer.Done(nil) + sendOffer.Done(nil) - // Ring expects a ping message every 5 seconds - go func() { - ticker := time.NewTicker(5 * time.Second) - defer ticker.Stop() + // Ring expects a ping message every 5 seconds + go client.startPingLoop(pc) + go client.startMessageLoop(&connState) - for { - select { - case <-client.done: - return - case <-ticker.C: - if pc.ConnectionState() == pion.PeerConnectionStateConnected { - if err := client.sendSessionMessage("ping", nil); err != nil { - return - } - } - } - } - }() - - go func() { - var err error + if err = connState.Wait(); err != nil { + return nil, err + } - // will be closed when conn will be closed - defer func() { - connState.Done(err) - }() + return client, nil +} - for { - select { - case <-client.done: - return - default: - var res BaseMessage - if err = ws.ReadJSON(&res); err != nil { - select { - case <-client.done: - return - default: - } +func (c *Client) startPingLoop(pc *pion.PeerConnection) { + ticker := time.NewTicker(5 * time.Second) + defer ticker.Stop() - client.Stop() - return - } + for { + select { + case <-c.done: + return + case <-ticker.C: + if pc.ConnectionState() == pion.PeerConnectionStateConnected { + if err := c.sendSessionMessage("ping", nil); err != nil { + return + } + } + } + } +} - // check if "doorbot_id" is present - if _, ok := res.Body["doorbot_id"]; !ok { - continue - } - - // check if the message is from the correct doorbot - doorbotID := res.Body["doorbot_id"].(float64) - if doorbotID != float64(client.camera.ID) { - continue - } +func (c *Client) startMessageLoop(connState *core.Waiter) { + var err error - // check if the message is from the correct session - if res.Method == "session_created" || res.Method == "session_started" { - if _, ok := res.Body["session_id"]; ok && client.sessionID == "" { - client.sessionID = res.Body["session_id"].(string) - } - } + // will be closed when conn will be closed + defer func() { + connState.Done(err) + }() - if _, ok := res.Body["session_id"]; ok { - if res.Body["session_id"].(string) != client.sessionID { - continue - } - } + for { + select { + case <-c.done: + return + default: + var res BaseMessage + if err = c.ws.ReadJSON(&res); err != nil { + select { + case <-c.done: + return + default: + } - rawMsg, _ := json.Marshal(res) + c.Stop() + return + } - switch res.Method { - case "sdp": - // 6. Get answer - var msg AnswerMessage - if err = json.Unmarshal(rawMsg, &msg); err != nil { - client.Stop() - return - } - if err = prod.SetAnswer(msg.Body.SDP); err != nil { - client.Stop() - return - } - if err = client.activateSession(); err != nil { - client.Stop() - return - } - - case "ice": - // 7. Continue to receiving candidates - var msg IceCandidateMessage - if err = json.Unmarshal(rawMsg, &msg); err != nil { - break - } + // check if "doorbot_id" is present + if _, ok := res.Body["doorbot_id"]; !ok { + continue + } + + // check if the message is from the correct doorbot + doorbotID := res.Body["doorbot_id"].(float64) + if doorbotID != float64(c.camera.ID) { + continue + } - // check for empty ICE candidate - if msg.Body.Ice == "" { - break - } + // check if the message is from the correct session + if res.Method == "session_created" || res.Method == "session_started" { + if _, ok := res.Body["session_id"]; ok && c.sessionID == "" { + c.sessionID = res.Body["session_id"].(string) + } + } - if err = prod.AddCandidate(msg.Body.Ice); err != nil { - client.Stop() - return - } + if _, ok := res.Body["session_id"]; ok { + if res.Body["session_id"].(string) != c.sessionID { + continue + } + } - case "close": - client.Stop() - return + rawMsg, _ := json.Marshal(res) - case "pong": - // Ignore - continue - } - } - } - }() + switch res.Method { + case "sdp": + if prod, ok := c.prod.(*webrtc.Conn); ok { + // Get answer + var msg AnswerMessage + if err = json.Unmarshal(rawMsg, &msg); err != nil { + c.Stop() + return + } + if err = prod.SetAnswer(msg.Body.SDP); err != nil { + c.Stop() + return + } + if err = c.activateSession(); err != nil { + c.Stop() + return + } + } + + case "ice": + if prod, ok := c.prod.(*webrtc.Conn); ok { + // Continue to receiving candidates + var msg IceCandidateMessage + if err = json.Unmarshal(rawMsg, &msg); err != nil { + break + } - if err = connState.Wait(); err != nil { - return nil, err - } + // check for empty ICE candidate + if msg.Body.Ice == "" { + break + } - return client, nil + if err = prod.AddCandidate(msg.Body.Ice); err != nil { + c.Stop() + return + } + } + + case "close": + c.Stop() + return + + case "pong": + // Ignore + continue + } + } + } } func (c *Client) activateSession() error { @@ -471,16 +486,18 @@ func (c *Client) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, } func (c *Client) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error { - if media.Kind == core.KindAudio { - // Enable speaker - speakerPayload := map[string]interface{}{ - "stealth_mode": false, - } + if webrtcProd, ok := c.prod.(*webrtc.Conn); ok { + if media.Kind == core.KindAudio { + // Enable speaker + speakerPayload := map[string]interface{}{ + "stealth_mode": false, + } + _ = c.sendSessionMessage("camera_options", speakerPayload) + } + return webrtcProd.AddTrack(media, codec, track) + } - _ = c.sendSessionMessage("camera_options", speakerPayload); - } - - return c.prod.AddTrack(media, codec, track) + return fmt.Errorf("add track not supported for snapshot") } func (c *Client) Start() error { @@ -517,5 +534,9 @@ func (c *Client) Stop() error { } func (c *Client) MarshalJSON() ([]byte, error) { - return c.prod.MarshalJSON() + if webrtcProd, ok := c.prod.(*webrtc.Conn); ok { + return webrtcProd.MarshalJSON() + } + + return nil, errors.New("ring: can't marshal") } \ No newline at end of file diff --git a/pkg/ring/snapshot.go b/pkg/ring/snapshot.go new file mode 100644 index 00000000..bbf86e28 --- /dev/null +++ b/pkg/ring/snapshot.go @@ -0,0 +1,64 @@ +package ring + +import ( + "fmt" + + "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/pion/rtp" +) + +type SnapshotProducer struct { + core.Connection + + client *RingRestClient + camera *CameraData +} + +func NewSnapshotProducer(client *RingRestClient, camera *CameraData) *SnapshotProducer { + return &SnapshotProducer{ + Connection: core.Connection{ + ID: core.NewID(), + FormatName: "ring/snapshot", + Protocol: "https", + Medias: []*core.Media{ + { + Kind: core.KindVideo, + Direction: core.DirectionRecvonly, + Codecs: []*core.Codec{ + { + Name: core.CodecJPEG, + ClockRate: 90000, + PayloadType: core.PayloadTypeRAW, + }, + }, + }, + }, + }, + client: client, + camera: camera, + } +} + +func (p *SnapshotProducer) Start() error { + // Fetch snapshot + response, err := p.client.Request("GET", fmt.Sprintf("https://app-snaps.ring.com/snapshots/next/%d", int(p.camera.ID)), nil) + if err != nil { + return fmt.Errorf("failed to get snapshot: %w", err) + } + + pkt := &rtp.Packet{ + Header: rtp.Header{Timestamp: core.Now90000()}, + Payload: response, + } + + // Send to all receivers + for _, receiver := range p.Receivers { + receiver.WriteRTP(pkt) + } + + return nil +} + +func (p *SnapshotProducer) Stop() error { + return p.Connection.Stop() +} \ No newline at end of file From f072dab07bf3cf0eaeedd06659351cd03c44924e Mon Sep 17 00:00:00 2001 From: Alex X Date: Sat, 25 Jan 2025 11:18:36 +0300 Subject: [PATCH 137/166] Correcting code formatting after #1567 --- internal/ring/init.go | 102 ------- internal/ring/ring.go | 102 +++++++ pkg/ring/api.go | 302 +++++++++--------- pkg/ring/client.go | 694 +++++++++++++++++++++--------------------- pkg/ring/snapshot.go | 80 ++--- www/add.html | 2 +- 6 files changed, 641 insertions(+), 641 deletions(-) delete mode 100644 internal/ring/init.go create mode 100644 internal/ring/ring.go diff --git a/internal/ring/init.go b/internal/ring/init.go deleted file mode 100644 index bc49178b..00000000 --- a/internal/ring/init.go +++ /dev/null @@ -1,102 +0,0 @@ -package ring - -import ( - "encoding/json" - "net/http" - "net/url" - - "github.com/AlexxIT/go2rtc/internal/api" - "github.com/AlexxIT/go2rtc/internal/streams" - "github.com/AlexxIT/go2rtc/pkg/core" - "github.com/AlexxIT/go2rtc/pkg/ring" -) - -func Init() { - streams.HandleFunc("ring", func(source string) (core.Producer, error) { - return ring.Dial(source) - }) - - api.HandleFunc("api/ring", apiRing) -} - -func apiRing(w http.ResponseWriter, r *http.Request) { - query := r.URL.Query() - var ringAPI *ring.RingRestClient - var err error - - // Check auth method - if email := query.Get("email"); email != "" { - // Email/Password Flow - password := query.Get("password") - code := query.Get("code") - - ringAPI, err = ring.NewRingRestClient(ring.EmailAuth{ - Email: email, - Password: password, - }, nil) - - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - // Try authentication (this will trigger 2FA if needed) - if _, err = ringAPI.GetAuth(code); err != nil { - if ringAPI.Using2FA { - // Return 2FA prompt - json.NewEncoder(w).Encode(map[string]interface{}{ - "needs_2fa": true, - "prompt": ringAPI.PromptFor2FA, - }) - return - } - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - } else { - // Refresh Token Flow - refreshToken := query.Get("refresh_token") - if refreshToken == "" { - http.Error(w, "either email/password or refresh_token is required", http.StatusBadRequest) - return - } - - ringAPI, err = ring.NewRingRestClient(ring.RefreshTokenAuth{ - RefreshToken: refreshToken, - }, nil) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - } - - // Fetch devices - devices, err := ringAPI.FetchRingDevices() - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - // Create clean query with only required parameters - cleanQuery := url.Values{} - cleanQuery.Set("refresh_token", ringAPI.RefreshToken) - - var items []*api.Source - for _, camera := range devices.AllCameras { - cleanQuery.Set("device_id", camera.DeviceID) - - // Stream source - items = append(items, &api.Source{ - Name: camera.Description, - URL: "ring:?" + cleanQuery.Encode(), - }) - - // Snapshot source - items = append(items, &api.Source{ - Name: camera.Description + " Snapshot", - URL: "ring:?" + cleanQuery.Encode() + "&snapshot", - }) - } - - api.ResponseSources(w, items) -} diff --git a/internal/ring/ring.go b/internal/ring/ring.go new file mode 100644 index 00000000..673ea480 --- /dev/null +++ b/internal/ring/ring.go @@ -0,0 +1,102 @@ +package ring + +import ( + "encoding/json" + "net/http" + "net/url" + + "github.com/AlexxIT/go2rtc/internal/api" + "github.com/AlexxIT/go2rtc/internal/streams" + "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/AlexxIT/go2rtc/pkg/ring" +) + +func Init() { + streams.HandleFunc("ring", func(source string) (core.Producer, error) { + return ring.Dial(source) + }) + + api.HandleFunc("api/ring", apiRing) +} + +func apiRing(w http.ResponseWriter, r *http.Request) { + query := r.URL.Query() + var ringAPI *ring.RingRestClient + var err error + + // Check auth method + if email := query.Get("email"); email != "" { + // Email/Password Flow + password := query.Get("password") + code := query.Get("code") + + ringAPI, err = ring.NewRingRestClient(ring.EmailAuth{ + Email: email, + Password: password, + }, nil) + + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // Try authentication (this will trigger 2FA if needed) + if _, err = ringAPI.GetAuth(code); err != nil { + if ringAPI.Using2FA { + // Return 2FA prompt + json.NewEncoder(w).Encode(map[string]interface{}{ + "needs_2fa": true, + "prompt": ringAPI.PromptFor2FA, + }) + return + } + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + } else { + // Refresh Token Flow + refreshToken := query.Get("refresh_token") + if refreshToken == "" { + http.Error(w, "either email/password or refresh_token is required", http.StatusBadRequest) + return + } + + ringAPI, err = ring.NewRingRestClient(ring.RefreshTokenAuth{ + RefreshToken: refreshToken, + }, nil) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + } + + // Fetch devices + devices, err := ringAPI.FetchRingDevices() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // Create clean query with only required parameters + cleanQuery := url.Values{} + cleanQuery.Set("refresh_token", ringAPI.RefreshToken) + + var items []*api.Source + for _, camera := range devices.AllCameras { + cleanQuery.Set("device_id", camera.DeviceID) + + // Stream source + items = append(items, &api.Source{ + Name: camera.Description, + URL: "ring:?" + cleanQuery.Encode(), + }) + + // Snapshot source + items = append(items, &api.Source{ + Name: camera.Description + " Snapshot", + URL: "ring:?" + cleanQuery.Encode() + "&snapshot", + }) + } + + api.ResponseSources(w, items) +} diff --git a/pkg/ring/api.go b/pkg/ring/api.go index e025e031..ed69465f 100644 --- a/pkg/ring/api.go +++ b/pkg/ring/api.go @@ -19,8 +19,8 @@ type RefreshTokenAuth struct { } type EmailAuth struct { - Email string - Password string + Email string + Password string } // AuthConfig represents the decoded refresh token data @@ -31,38 +31,38 @@ type AuthConfig struct { // AuthTokenResponse represents the response from the authentication endpoint type AuthTokenResponse struct { - AccessToken string `json:"access_token"` - ExpiresIn int `json:"expires_in"` - RefreshToken string `json:"refresh_token"` - Scope string `json:"scope"` // Always "client" - TokenType string `json:"token_type"` // Always "Bearer" + AccessToken string `json:"access_token"` + ExpiresIn int `json:"expires_in"` + RefreshToken string `json:"refresh_token"` + Scope string `json:"scope"` // Always "client" + TokenType string `json:"token_type"` // Always "Bearer" } type Auth2faResponse struct { - Error string `json:"error"` - ErrorDescription string `json:"error_description"` - TSVState string `json:"tsv_state"` - Phone string `json:"phone"` - NextTimeInSecs int `json:"next_time_in_secs"` + Error string `json:"error"` + ErrorDescription string `json:"error_description"` + TSVState string `json:"tsv_state"` + Phone string `json:"phone"` + NextTimeInSecs int `json:"next_time_in_secs"` } // SocketTicketRequest represents the request to get a socket ticket type SocketTicketResponse struct { - Ticket string `json:"ticket"` - ResponseTimestamp int64 `json:"response_timestamp"` + Ticket string `json:"ticket"` + ResponseTimestamp int64 `json:"response_timestamp"` } // RingRestClient handles authentication and requests to Ring API type RingRestClient struct { - httpClient *http.Client - authConfig *AuthConfig - hardwareID string - authToken *AuthTokenResponse - Using2FA bool - PromptFor2FA string - RefreshToken string - auth interface{} // EmailAuth or RefreshTokenAuth - onTokenRefresh func(string) + httpClient *http.Client + authConfig *AuthConfig + hardwareID string + authToken *AuthTokenResponse + Using2FA bool + PromptFor2FA string + RefreshToken string + auth interface{} // EmailAuth or RefreshTokenAuth + onTokenRefresh func(string) } // CameraKind represents the different types of Ring cameras @@ -70,11 +70,11 @@ type CameraKind string // CameraData contains common fields for all camera types type CameraData struct { - ID float64 `json:"id"` - Description string `json:"description"` - DeviceID string `json:"device_id"` - Kind string `json:"kind"` - LocationID string `json:"location_id"` + ID float64 `json:"id"` + Description string `json:"description"` + DeviceID string `json:"device_id"` + Kind string `json:"kind"` + LocationID string `json:"location_id"` } // RingDeviceType represents different types of Ring devices @@ -82,12 +82,12 @@ type RingDeviceType string // RingDevicesResponse represents the response from the Ring API type RingDevicesResponse struct { - Doorbots []CameraData `json:"doorbots"` - AuthorizedDoorbots []CameraData `json:"authorized_doorbots"` - StickupCams []CameraData `json:"stickup_cams"` - AllCameras []CameraData `json:"all_cameras"` - Chimes []CameraData `json:"chimes"` - Other []map[string]interface{} `json:"other"` + Doorbots []CameraData `json:"doorbots"` + AuthorizedDoorbots []CameraData `json:"authorized_doorbots"` + StickupCams []CameraData `json:"stickup_cams"` + AllCameras []CameraData `json:"all_cameras"` + Chimes []CameraData `json:"chimes"` + Other []map[string]interface{} `json:"other"` } const ( @@ -131,48 +131,48 @@ const ( ) const ( - clientAPIBaseURL = "https://api.ring.com/clients_api/" - deviceAPIBaseURL = "https://api.ring.com/devices/v1/" - commandsAPIBaseURL = "https://api.ring.com/commands/v1/" - appAPIBaseURL = "https://prd-api-us.prd.rings.solutions/api/v1/" - oauthURL = "https://oauth.ring.com/oauth/token" - apiVersion = 11 - defaultTimeout = 20 * time.Second - maxRetries = 3 + clientAPIBaseURL = "https://api.ring.com/clients_api/" + deviceAPIBaseURL = "https://api.ring.com/devices/v1/" + commandsAPIBaseURL = "https://api.ring.com/commands/v1/" + appAPIBaseURL = "https://prd-api-us.prd.rings.solutions/api/v1/" + oauthURL = "https://oauth.ring.com/oauth/token" + apiVersion = 11 + defaultTimeout = 20 * time.Second + maxRetries = 3 ) // NewRingRestClient creates a new Ring client instance func NewRingRestClient(auth interface{}, onTokenRefresh func(string)) (*RingRestClient, error) { - client := &RingRestClient{ - httpClient: &http.Client{Timeout: defaultTimeout}, - onTokenRefresh: onTokenRefresh, - hardwareID: generateHardwareID(), - auth: auth, - } + client := &RingRestClient{ + httpClient: &http.Client{Timeout: defaultTimeout}, + onTokenRefresh: onTokenRefresh, + hardwareID: generateHardwareID(), + auth: auth, + } + + switch a := auth.(type) { + case RefreshTokenAuth: + if a.RefreshToken == "" { + return nil, fmt.Errorf("refresh token is required") + } - switch a := auth.(type) { - case RefreshTokenAuth: - if a.RefreshToken == "" { - return nil, fmt.Errorf("refresh token is required") - } - config, err := parseAuthConfig(a.RefreshToken) - if err != nil { - return nil, fmt.Errorf("failed to parse refresh token: %w", err) - } + if err != nil { + return nil, fmt.Errorf("failed to parse refresh token: %w", err) + } client.authConfig = config - client.hardwareID = config.HID + client.hardwareID = config.HID client.RefreshToken = a.RefreshToken - case EmailAuth: - if a.Email == "" || a.Password == "" { - return nil, fmt.Errorf("email and password are required") - } - default: - return nil, fmt.Errorf("invalid auth type") - } + case EmailAuth: + if a.Email == "" || a.Password == "" { + return nil, fmt.Errorf("email and password are required") + } + default: + return nil, fmt.Errorf("invalid auth type") + } - return client, nil + return client, nil } // Request makes an authenticated request to the Ring API @@ -207,7 +207,7 @@ func (c *RingRestClient) Request(method, url string, body interface{}) ([]byte, // Make request with retries var resp *http.Response var responseBody []byte - + for attempt := 0; attempt <= maxRetries; attempt++ { resp, err = c.httpClient.Do(req) if err != nil { @@ -318,104 +318,104 @@ func (c *RingRestClient) ensureAuth() error { // getAuth makes an authentication request to the Ring API func (c *RingRestClient) GetAuth(twoFactorAuthCode string) (*AuthTokenResponse, error) { - var grantData map[string]string + var grantData map[string]string - if c.authConfig != nil && twoFactorAuthCode == "" { - grantData = map[string]string{ - "grant_type": "refresh_token", - "refresh_token": c.authConfig.RT, - } - } else { - authEmail, ok := c.auth.(EmailAuth) - if !ok { - return nil, fmt.Errorf("invalid auth type for email authentication") - } - grantData = map[string]string{ - "grant_type": "password", - "username": authEmail.Email, - "password": authEmail.Password, - } - } + if c.authConfig != nil && twoFactorAuthCode == "" { + grantData = map[string]string{ + "grant_type": "refresh_token", + "refresh_token": c.authConfig.RT, + } + } else { + authEmail, ok := c.auth.(EmailAuth) + if !ok { + return nil, fmt.Errorf("invalid auth type for email authentication") + } + grantData = map[string]string{ + "grant_type": "password", + "username": authEmail.Email, + "password": authEmail.Password, + } + } - grantData["client_id"] = "ring_official_android" - grantData["scope"] = "client" + grantData["client_id"] = "ring_official_android" + grantData["scope"] = "client" - body, err := json.Marshal(grantData) - if err != nil { - return nil, fmt.Errorf("failed to marshal auth request: %w", err) - } + body, err := json.Marshal(grantData) + if err != nil { + return nil, fmt.Errorf("failed to marshal auth request: %w", err) + } - req, err := http.NewRequest("POST", oauthURL, bytes.NewReader(body)) - if err != nil { - return nil, err - } + req, err := http.NewRequest("POST", oauthURL, bytes.NewReader(body)) + if err != nil { + return nil, err + } - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Accept", "application/json") - req.Header.Set("hardware_id", c.hardwareID) - req.Header.Set("User-Agent", "android:com.ringapp") - req.Header.Set("2fa-support", "true") - if twoFactorAuthCode != "" { - req.Header.Set("2fa-code", twoFactorAuthCode) - } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json") + req.Header.Set("hardware_id", c.hardwareID) + req.Header.Set("User-Agent", "android:com.ringapp") + req.Header.Set("2fa-support", "true") + if twoFactorAuthCode != "" { + req.Header.Set("2fa-code", twoFactorAuthCode) + } - resp, err := c.httpClient.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() - // Handle 2FA Responses - if resp.StatusCode == http.StatusPreconditionFailed || - (resp.StatusCode == http.StatusBadRequest && strings.Contains(resp.Header.Get("WWW-Authenticate"), "Verification Code")) { - - var tfaResp Auth2faResponse - if err := json.NewDecoder(resp.Body).Decode(&tfaResp); err != nil { - return nil, err - } + // Handle 2FA Responses + if resp.StatusCode == http.StatusPreconditionFailed || + (resp.StatusCode == http.StatusBadRequest && strings.Contains(resp.Header.Get("WWW-Authenticate"), "Verification Code")) { - c.Using2FA = true - if resp.StatusCode == http.StatusBadRequest { - c.PromptFor2FA = "Invalid 2fa code entered. Please try again." - return nil, fmt.Errorf("invalid 2FA code") - } + var tfaResp Auth2faResponse + if err := json.NewDecoder(resp.Body).Decode(&tfaResp); err != nil { + return nil, err + } - if tfaResp.TSVState != "" { - prompt := "from your authenticator app" - if tfaResp.TSVState != "totp" { - prompt = fmt.Sprintf("sent to %s via %s", tfaResp.Phone, tfaResp.TSVState) - } - c.PromptFor2FA = fmt.Sprintf("Please enter the code %s", prompt) - } else { - c.PromptFor2FA = "Please enter the code sent to your text/email" - } + c.Using2FA = true + if resp.StatusCode == http.StatusBadRequest { + c.PromptFor2FA = "Invalid 2fa code entered. Please try again." + return nil, fmt.Errorf("invalid 2FA code") + } - return nil, fmt.Errorf("2FA required") - } + if tfaResp.TSVState != "" { + prompt := "from your authenticator app" + if tfaResp.TSVState != "totp" { + prompt = fmt.Sprintf("sent to %s via %s", tfaResp.Phone, tfaResp.TSVState) + } + c.PromptFor2FA = fmt.Sprintf("Please enter the code %s", prompt) + } else { + c.PromptFor2FA = "Please enter the code sent to your text/email" + } - // Handle errors - if resp.StatusCode != http.StatusOK { - body, _ := io.ReadAll(resp.Body) - return nil, fmt.Errorf("auth request failed with status %d: %s", resp.StatusCode, string(body)) - } + return nil, fmt.Errorf("2FA required") + } - var authResp AuthTokenResponse - if err := json.NewDecoder(resp.Body).Decode(&authResp); err != nil { - return nil, fmt.Errorf("failed to decode auth response: %w", err) - } + // Handle errors + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("auth request failed with status %d: %s", resp.StatusCode, string(body)) + } - c.authToken = &authResp - c.authConfig = &AuthConfig{ - RT: authResp.RefreshToken, - HID: c.hardwareID, - } + var authResp AuthTokenResponse + if err := json.NewDecoder(resp.Body).Decode(&authResp); err != nil { + return nil, fmt.Errorf("failed to decode auth response: %w", err) + } - c.RefreshToken = encodeAuthConfig(c.authConfig) - if c.onTokenRefresh != nil { - c.onTokenRefresh(c.RefreshToken) - } + c.authToken = &authResp + c.authConfig = &AuthConfig{ + RT: authResp.RefreshToken, + HID: c.hardwareID, + } - return c.authToken, nil + c.RefreshToken = encodeAuthConfig(c.authConfig) + if c.onTokenRefresh != nil { + c.onTokenRefresh(c.RefreshToken) + } + + return c.authToken, nil } // Helper functions for auth config encoding/decoding @@ -542,4 +542,4 @@ func interfaceSlice(slice interface{}) []CameraData { } } return ret -} \ No newline at end of file +} diff --git a/pkg/ring/client.go b/pkg/ring/client.go index c432ecf9..7014213d 100644 --- a/pkg/ring/client.go +++ b/pkg/ring/client.go @@ -16,422 +16,422 @@ import ( ) type Client struct { - api *RingRestClient - ws *websocket.Conn - prod core.Producer - camera *CameraData - dialogID string - sessionID string - wsMutex sync.Mutex - done chan struct{} + api *RingRestClient + ws *websocket.Conn + prod core.Producer + camera *CameraData + dialogID string + sessionID string + wsMutex sync.Mutex + done chan struct{} } type SessionBody struct { - DoorbotID int `json:"doorbot_id"` - SessionID string `json:"session_id"` + DoorbotID int `json:"doorbot_id"` + SessionID string `json:"session_id"` } type AnswerMessage struct { - Method string `json:"method"` // "sdp" - Body struct { - SessionBody - SDP string `json:"sdp"` - Type string `json:"type"` // "answer" - } `json:"body"` + Method string `json:"method"` // "sdp" + Body struct { + SessionBody + SDP string `json:"sdp"` + Type string `json:"type"` // "answer" + } `json:"body"` } type IceCandidateMessage struct { - Method string `json:"method"` // "ice" - Body struct { - SessionBody - Ice string `json:"ice"` - MLineIndex int `json:"mlineindex"` - } `json:"body"` + Method string `json:"method"` // "ice" + Body struct { + SessionBody + Ice string `json:"ice"` + MLineIndex int `json:"mlineindex"` + } `json:"body"` } type SessionMessage struct { - Method string `json:"method"` // "session_created" or "session_started" - Body SessionBody `json:"body"` + Method string `json:"method"` // "session_created" or "session_started" + Body SessionBody `json:"body"` } type PongMessage struct { - Method string `json:"method"` // "pong" - Body SessionBody `json:"body"` + Method string `json:"method"` // "pong" + Body SessionBody `json:"body"` } type NotificationMessage struct { - Method string `json:"method"` // "notification" - Body struct { - SessionBody - IsOK bool `json:"is_ok"` - Text string `json:"text"` - } `json:"body"` + Method string `json:"method"` // "notification" + Body struct { + SessionBody + IsOK bool `json:"is_ok"` + Text string `json:"text"` + } `json:"body"` } type StreamInfoMessage struct { - Method string `json:"method"` // "stream_info" - Body struct { - SessionBody - Transcoding bool `json:"transcoding"` - TranscodingReason string `json:"transcoding_reason"` - } `json:"body"` + Method string `json:"method"` // "stream_info" + Body struct { + SessionBody + Transcoding bool `json:"transcoding"` + TranscodingReason string `json:"transcoding_reason"` + } `json:"body"` } type CloseMessage struct { - Method string `json:"method"` // "close" - Body struct { - SessionBody - Reason struct { - Code int `json:"code"` - Text string `json:"text"` - } `json:"reason"` - } `json:"body"` + Method string `json:"method"` // "close" + Body struct { + SessionBody + Reason struct { + Code int `json:"code"` + Text string `json:"text"` + } `json:"reason"` + } `json:"body"` } type BaseMessage struct { - Method string `json:"method"` - Body map[string]any `json:"body"` + Method string `json:"method"` + Body map[string]any `json:"body"` } // Close reason codes const ( - CloseReasonNormalClose = 0 - CloseReasonAuthenticationFailed = 5 - CloseReasonTimeout = 6 + CloseReasonNormalClose = 0 + CloseReasonAuthenticationFailed = 5 + CloseReasonTimeout = 6 ) func Dial(rawURL string) (*Client, error) { - // 1. Parse URL and validate basic params - u, err := url.Parse(rawURL) - if err != nil { - return nil, err - } + // 1. Parse URL and validate basic params + u, err := url.Parse(rawURL) + if err != nil { + return nil, err + } - query := u.Query() - encodedToken := query.Get("refresh_token") - deviceID := query.Get("device_id") + query := u.Query() + encodedToken := query.Get("refresh_token") + deviceID := query.Get("device_id") _, isSnapshot := query["snapshot"] - if encodedToken == "" || deviceID == "" { - return nil, errors.New("ring: wrong query") - } + if encodedToken == "" || deviceID == "" { + return nil, errors.New("ring: wrong query") + } - // URL-decode the refresh token - refreshToken, err := url.QueryUnescape(encodedToken) - if err != nil { - return nil, fmt.Errorf("ring: invalid refresh token encoding: %w", err) - } + // URL-decode the refresh token + refreshToken, err := url.QueryUnescape(encodedToken) + if err != nil { + return nil, fmt.Errorf("ring: invalid refresh token encoding: %w", err) + } - // Initialize Ring API client - ringAPI, err := NewRingRestClient(RefreshTokenAuth{RefreshToken: refreshToken}, nil) - if err != nil { - return nil, err - } + // Initialize Ring API client + ringAPI, err := NewRingRestClient(RefreshTokenAuth{RefreshToken: refreshToken}, nil) + if err != nil { + return nil, err + } - // Get camera details - devices, err := ringAPI.FetchRingDevices() - if err != nil { - return nil, err - } + // Get camera details + devices, err := ringAPI.FetchRingDevices() + if err != nil { + return nil, err + } - var camera *CameraData - for _, cam := range devices.AllCameras { - if fmt.Sprint(cam.DeviceID) == deviceID { - camera = &cam - break - } - } - if camera == nil { - return nil, errors.New("ring: camera not found") - } + var camera *CameraData + for _, cam := range devices.AllCameras { + if fmt.Sprint(cam.DeviceID) == deviceID { + camera = &cam + break + } + } + if camera == nil { + return nil, errors.New("ring: camera not found") + } - // Create base client - client := &Client{ - api: ringAPI, - camera: camera, - dialogID: uuid.NewString(), - done: make(chan struct{}), - } + // Create base client + client := &Client{ + api: ringAPI, + camera: camera, + dialogID: uuid.NewString(), + done: make(chan struct{}), + } - // Check if snapshot request - if isSnapshot { - client.prod = NewSnapshotProducer(ringAPI, camera) - return client, nil - } + // Check if snapshot request + if isSnapshot { + client.prod = NewSnapshotProducer(ringAPI, camera) + return client, nil + } - // If not snapshot, continue with WebRTC setup - ticket, err := ringAPI.GetSocketTicket() - if err != nil { - return nil, err - } + // If not snapshot, continue with WebRTC setup + ticket, err := ringAPI.GetSocketTicket() + if err != nil { + return nil, err + } - // Create WebSocket connection - wsURL := fmt.Sprintf("wss://api.prod.signalling.ring.devices.a2z.com/ws?api_version=4.0&auth_type=ring_solutions&client_id=ring_site-%s&token=%s", - uuid.NewString(), url.QueryEscape(ticket.Ticket)) + // Create WebSocket connection + wsURL := fmt.Sprintf("wss://api.prod.signalling.ring.devices.a2z.com/ws?api_version=4.0&auth_type=ring_solutions&client_id=ring_site-%s&token=%s", + uuid.NewString(), url.QueryEscape(ticket.Ticket)) - client.ws, _, err = websocket.DefaultDialer.Dial(wsURL, map[string][]string{ - "User-Agent": {"android:com.ringapp"}, - }) - if err != nil { - return nil, err - } + client.ws, _, err = websocket.DefaultDialer.Dial(wsURL, map[string][]string{ + "User-Agent": {"android:com.ringapp"}, + }) + if err != nil { + return nil, err + } - // Create Peer Connection - conf := pion.Configuration{ - ICEServers: []pion.ICEServer{ - {URLs: []string{ - "stun:stun.kinesisvideo.us-east-1.amazonaws.com:443", - "stun:stun.kinesisvideo.us-east-2.amazonaws.com:443", - "stun:stun.kinesisvideo.us-west-2.amazonaws.com:443", - "stun:stun.l.google.com:19302", - "stun:stun1.l.google.com:19302", - "stun:stun2.l.google.com:19302", - "stun:stun3.l.google.com:19302", - "stun:stun4.l.google.com:19302", - }}, - }, - ICETransportPolicy: pion.ICETransportPolicyAll, - BundlePolicy: pion.BundlePolicyBalanced, - } + // Create Peer Connection + conf := pion.Configuration{ + ICEServers: []pion.ICEServer{ + {URLs: []string{ + "stun:stun.kinesisvideo.us-east-1.amazonaws.com:443", + "stun:stun.kinesisvideo.us-east-2.amazonaws.com:443", + "stun:stun.kinesisvideo.us-west-2.amazonaws.com:443", + "stun:stun.l.google.com:19302", + "stun:stun1.l.google.com:19302", + "stun:stun2.l.google.com:19302", + "stun:stun3.l.google.com:19302", + "stun:stun4.l.google.com:19302", + }}, + }, + ICETransportPolicy: pion.ICETransportPolicyAll, + BundlePolicy: pion.BundlePolicyBalanced, + } - api, err := webrtc.NewAPI() - if err != nil { - client.ws.Close() - return nil, err - } + api, err := webrtc.NewAPI() + if err != nil { + client.ws.Close() + return nil, err + } - pc, err := api.NewPeerConnection(conf) - if err != nil { - client.ws.Close() - return nil, err - } + pc, err := api.NewPeerConnection(conf) + if err != nil { + client.ws.Close() + return nil, err + } - // protect from sending ICE candidate before Offer - var sendOffer core.Waiter + // protect from sending ICE candidate before Offer + var sendOffer core.Waiter - // protect from blocking on errors - defer sendOffer.Done(nil) + // protect from blocking on errors + defer sendOffer.Done(nil) - // waiter will wait PC error or WS error or nil (connection OK) - var connState core.Waiter + // waiter will wait PC error or WS error or nil (connection OK) + var connState core.Waiter - prod := webrtc.NewConn(pc) - prod.FormatName = "ring/webrtc" - prod.Mode = core.ModeActiveProducer - prod.Protocol = "ws" - prod.URL = rawURL + prod := webrtc.NewConn(pc) + prod.FormatName = "ring/webrtc" + prod.Mode = core.ModeActiveProducer + prod.Protocol = "ws" + prod.URL = rawURL - client.prod = prod + client.prod = prod - prod.Listen(func(msg any) { - switch msg := msg.(type) { - case *pion.ICECandidate: - _ = sendOffer.Wait() + prod.Listen(func(msg any) { + switch msg := msg.(type) { + case *pion.ICECandidate: + _ = sendOffer.Wait() - iceCandidate := msg.ToJSON() + iceCandidate := msg.ToJSON() - // skip empty ICE candidates - if iceCandidate.Candidate == "" { - return - } + // skip empty ICE candidates + if iceCandidate.Candidate == "" { + return + } - icePayload := map[string]interface{}{ - "ice": iceCandidate.Candidate, - "mlineindex": iceCandidate.SDPMLineIndex, - } - - if err = client.sendSessionMessage("ice", icePayload); err != nil { - connState.Done(err) - return - } + icePayload := map[string]interface{}{ + "ice": iceCandidate.Candidate, + "mlineindex": iceCandidate.SDPMLineIndex, + } - case pion.PeerConnectionState: - switch msg { - case pion.PeerConnectionStateConnecting: - case pion.PeerConnectionStateConnected: - connState.Done(nil) - default: - connState.Done(errors.New("ring: " + msg.String())) - } - } - }) + if err = client.sendSessionMessage("ice", icePayload); err != nil { + connState.Done(err) + return + } - // Setup media configuration - medias := []*core.Media{ - { - Kind: core.KindAudio, - Direction: core.DirectionSendRecv, - Codecs: []*core.Codec{ - { - Name: "opus", - ClockRate: 48000, - Channels: 2, - }, - }, - }, - { - Kind: core.KindVideo, - Direction: core.DirectionRecvonly, - Codecs: []*core.Codec{ - { - Name: "H264", - ClockRate: 90000, - }, - }, - }, - } + case pion.PeerConnectionState: + switch msg { + case pion.PeerConnectionStateConnecting: + case pion.PeerConnectionStateConnected: + connState.Done(nil) + default: + connState.Done(errors.New("ring: " + msg.String())) + } + } + }) - // Create offer - offer, err := prod.CreateOffer(medias) - if err != nil { - client.Stop() - return nil, err - } + // Setup media configuration + medias := []*core.Media{ + { + Kind: core.KindAudio, + Direction: core.DirectionSendRecv, + Codecs: []*core.Codec{ + { + Name: "opus", + ClockRate: 48000, + Channels: 2, + }, + }, + }, + { + Kind: core.KindVideo, + Direction: core.DirectionRecvonly, + Codecs: []*core.Codec{ + { + Name: "H264", + ClockRate: 90000, + }, + }, + }, + } - // Send offer - offerPayload := map[string]interface{}{ - "stream_options": map[string]bool{ - "audio_enabled": true, - "video_enabled": true, - }, - "sdp": offer, - } + // Create offer + offer, err := prod.CreateOffer(medias) + if err != nil { + client.Stop() + return nil, err + } - if err = client.sendSessionMessage("live_view", offerPayload); err != nil { - client.Stop() - return nil, err - } + // Send offer + offerPayload := map[string]interface{}{ + "stream_options": map[string]bool{ + "audio_enabled": true, + "video_enabled": true, + }, + "sdp": offer, + } - sendOffer.Done(nil) + if err = client.sendSessionMessage("live_view", offerPayload); err != nil { + client.Stop() + return nil, err + } - // Ring expects a ping message every 5 seconds - go client.startPingLoop(pc) - go client.startMessageLoop(&connState) + sendOffer.Done(nil) - if err = connState.Wait(); err != nil { - return nil, err - } + // Ring expects a ping message every 5 seconds + go client.startPingLoop(pc) + go client.startMessageLoop(&connState) - return client, nil + if err = connState.Wait(); err != nil { + return nil, err + } + + return client, nil } func (c *Client) startPingLoop(pc *pion.PeerConnection) { - ticker := time.NewTicker(5 * time.Second) - defer ticker.Stop() + ticker := time.NewTicker(5 * time.Second) + defer ticker.Stop() - for { - select { - case <-c.done: - return - case <-ticker.C: - if pc.ConnectionState() == pion.PeerConnectionStateConnected { - if err := c.sendSessionMessage("ping", nil); err != nil { - return - } - } - } - } + for { + select { + case <-c.done: + return + case <-ticker.C: + if pc.ConnectionState() == pion.PeerConnectionStateConnected { + if err := c.sendSessionMessage("ping", nil); err != nil { + return + } + } + } + } } func (c *Client) startMessageLoop(connState *core.Waiter) { - var err error + var err error - // will be closed when conn will be closed - defer func() { - connState.Done(err) - }() + // will be closed when conn will be closed + defer func() { + connState.Done(err) + }() - for { - select { - case <-c.done: - return - default: - var res BaseMessage - if err = c.ws.ReadJSON(&res); err != nil { - select { - case <-c.done: - return - default: - } + for { + select { + case <-c.done: + return + default: + var res BaseMessage + if err = c.ws.ReadJSON(&res); err != nil { + select { + case <-c.done: + return + default: + } - c.Stop() - return - } + c.Stop() + return + } - // check if "doorbot_id" is present - if _, ok := res.Body["doorbot_id"]; !ok { - continue - } - - // check if the message is from the correct doorbot - doorbotID := res.Body["doorbot_id"].(float64) - if doorbotID != float64(c.camera.ID) { - continue - } + // check if "doorbot_id" is present + if _, ok := res.Body["doorbot_id"]; !ok { + continue + } - // check if the message is from the correct session - if res.Method == "session_created" || res.Method == "session_started" { - if _, ok := res.Body["session_id"]; ok && c.sessionID == "" { - c.sessionID = res.Body["session_id"].(string) - } - } + // check if the message is from the correct doorbot + doorbotID := res.Body["doorbot_id"].(float64) + if doorbotID != float64(c.camera.ID) { + continue + } - if _, ok := res.Body["session_id"]; ok { - if res.Body["session_id"].(string) != c.sessionID { - continue - } - } + // check if the message is from the correct session + if res.Method == "session_created" || res.Method == "session_started" { + if _, ok := res.Body["session_id"]; ok && c.sessionID == "" { + c.sessionID = res.Body["session_id"].(string) + } + } - rawMsg, _ := json.Marshal(res) + if _, ok := res.Body["session_id"]; ok { + if res.Body["session_id"].(string) != c.sessionID { + continue + } + } - switch res.Method { - case "sdp": - if prod, ok := c.prod.(*webrtc.Conn); ok { - // Get answer - var msg AnswerMessage - if err = json.Unmarshal(rawMsg, &msg); err != nil { - c.Stop() - return - } - if err = prod.SetAnswer(msg.Body.SDP); err != nil { - c.Stop() - return - } - if err = c.activateSession(); err != nil { - c.Stop() - return - } - } - - case "ice": - if prod, ok := c.prod.(*webrtc.Conn); ok { - // Continue to receiving candidates - var msg IceCandidateMessage - if err = json.Unmarshal(rawMsg, &msg); err != nil { - break - } + rawMsg, _ := json.Marshal(res) - // check for empty ICE candidate - if msg.Body.Ice == "" { - break - } + switch res.Method { + case "sdp": + if prod, ok := c.prod.(*webrtc.Conn); ok { + // Get answer + var msg AnswerMessage + if err = json.Unmarshal(rawMsg, &msg); err != nil { + c.Stop() + return + } + if err = prod.SetAnswer(msg.Body.SDP); err != nil { + c.Stop() + return + } + if err = c.activateSession(); err != nil { + c.Stop() + return + } + } - if err = prod.AddCandidate(msg.Body.Ice); err != nil { - c.Stop() - return - } - } + case "ice": + if prod, ok := c.prod.(*webrtc.Conn); ok { + // Continue to receiving candidates + var msg IceCandidateMessage + if err = json.Unmarshal(rawMsg, &msg); err != nil { + break + } - case "close": - c.Stop() - return + // check for empty ICE candidate + if msg.Body.Ice == "" { + break + } - case "pong": - // Ignore - continue - } - } - } + if err = prod.AddCandidate(msg.Body.Ice); err != nil { + c.Stop() + return + } + } + + case "close": + c.Stop() + return + + case "pong": + // Ignore + continue + } + } + } } func (c *Client) activateSession() error { @@ -453,7 +453,7 @@ func (c *Client) activateSession() error { func (c *Client) sendSessionMessage(method string, body map[string]interface{}) error { c.wsMutex.Lock() - defer c.wsMutex.Unlock() + defer c.wsMutex.Unlock() if body == nil { body = make(map[string]interface{}) @@ -486,18 +486,18 @@ func (c *Client) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, } func (c *Client) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error { - if webrtcProd, ok := c.prod.(*webrtc.Conn); ok { - if media.Kind == core.KindAudio { - // Enable speaker - speakerPayload := map[string]interface{}{ - "stealth_mode": false, - } - _ = c.sendSessionMessage("camera_options", speakerPayload) - } - return webrtcProd.AddTrack(media, codec, track) - } + if webrtcProd, ok := c.prod.(*webrtc.Conn); ok { + if media.Kind == core.KindAudio { + // Enable speaker + speakerPayload := map[string]interface{}{ + "stealth_mode": false, + } + _ = c.sendSessionMessage("camera_options", speakerPayload) + } + return webrtcProd.AddTrack(media, codec, track) + } - return fmt.Errorf("add track not supported for snapshot") + return fmt.Errorf("add track not supported for snapshot") } func (c *Client) Start() error { @@ -534,9 +534,9 @@ func (c *Client) Stop() error { } func (c *Client) MarshalJSON() ([]byte, error) { - if webrtcProd, ok := c.prod.(*webrtc.Conn); ok { - return webrtcProd.MarshalJSON() - } - + if webrtcProd, ok := c.prod.(*webrtc.Conn); ok { + return webrtcProd.MarshalJSON() + } + return nil, errors.New("ring: can't marshal") -} \ No newline at end of file +} diff --git a/pkg/ring/snapshot.go b/pkg/ring/snapshot.go index bbf86e28..84da0fd3 100644 --- a/pkg/ring/snapshot.go +++ b/pkg/ring/snapshot.go @@ -8,57 +8,57 @@ import ( ) type SnapshotProducer struct { - core.Connection + core.Connection - client *RingRestClient - camera *CameraData + client *RingRestClient + camera *CameraData } func NewSnapshotProducer(client *RingRestClient, camera *CameraData) *SnapshotProducer { - return &SnapshotProducer{ - Connection: core.Connection{ - ID: core.NewID(), - FormatName: "ring/snapshot", - Protocol: "https", - Medias: []*core.Media{ - { - Kind: core.KindVideo, - Direction: core.DirectionRecvonly, - Codecs: []*core.Codec{ - { - Name: core.CodecJPEG, - ClockRate: 90000, - PayloadType: core.PayloadTypeRAW, - }, - }, - }, - }, - }, - client: client, - camera: camera, - } + return &SnapshotProducer{ + Connection: core.Connection{ + ID: core.NewID(), + FormatName: "ring/snapshot", + Protocol: "https", + Medias: []*core.Media{ + { + Kind: core.KindVideo, + Direction: core.DirectionRecvonly, + Codecs: []*core.Codec{ + { + Name: core.CodecJPEG, + ClockRate: 90000, + PayloadType: core.PayloadTypeRAW, + }, + }, + }, + }, + }, + client: client, + camera: camera, + } } func (p *SnapshotProducer) Start() error { - // Fetch snapshot - response, err := p.client.Request("GET", fmt.Sprintf("https://app-snaps.ring.com/snapshots/next/%d", int(p.camera.ID)), nil) - if err != nil { - return fmt.Errorf("failed to get snapshot: %w", err) - } + // Fetch snapshot + response, err := p.client.Request("GET", fmt.Sprintf("https://app-snaps.ring.com/snapshots/next/%d", int(p.camera.ID)), nil) + if err != nil { + return fmt.Errorf("failed to get snapshot: %w", err) + } - pkt := &rtp.Packet{ - Header: rtp.Header{Timestamp: core.Now90000()}, - Payload: response, - } + pkt := &rtp.Packet{ + Header: rtp.Header{Timestamp: core.Now90000()}, + Payload: response, + } - // Send to all receivers + // Send to all receivers for _, receiver := range p.Receivers { - receiver.WriteRTP(pkt) - } + receiver.WriteRTP(pkt) + } - return nil + return nil } func (p *SnapshotProducer) Stop() error { - return p.Connection.Stop() -} \ No newline at end of file + return p.Connection.Stop() +} diff --git a/www/add.html b/www/add.html index 7dae63d4..cec8ed36 100644 --- a/www/add.html +++ b/www/add.html @@ -247,7 +247,7 @@ const r = await fetch(url, {cache: 'no-cache'}); const data = await r.json(); - + if (data.needs_2fa) { document.getElementById('tfa-field').style.display = 'block'; document.getElementById('tfa-prompt').textContent = data.prompt || 'Enter 2FA code'; From 3e3988a67f00f872606675dc1a73863efe534655 Mon Sep 17 00:00:00 2001 From: seydx Date: Sat, 25 Jan 2025 16:11:39 +0100 Subject: [PATCH 138/166] minor improvements --- pkg/ring/client.go | 5 ++--- pkg/ring/snapshot.go | 8 +++----- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/pkg/ring/client.go b/pkg/ring/client.go index 7014213d..4c473276 100644 --- a/pkg/ring/client.go +++ b/pkg/ring/client.go @@ -514,7 +514,6 @@ func (c *Client) Stop() error { if c.prod != nil { _ = c.prod.Stop() - c.prod = nil } if c.ws != nil { @@ -537,6 +536,6 @@ func (c *Client) MarshalJSON() ([]byte, error) { if webrtcProd, ok := c.prod.(*webrtc.Conn); ok { return webrtcProd.MarshalJSON() } - - return nil, errors.New("ring: can't marshal") + + return json.Marshal(c.prod) } diff --git a/pkg/ring/snapshot.go b/pkg/ring/snapshot.go index 84da0fd3..f64e4f79 100644 --- a/pkg/ring/snapshot.go +++ b/pkg/ring/snapshot.go @@ -20,6 +20,7 @@ func NewSnapshotProducer(client *RingRestClient, camera *CameraData) *SnapshotPr ID: core.NewID(), FormatName: "ring/snapshot", Protocol: "https", + RemoteAddr: "app-snaps.ring.com", Medias: []*core.Media{ { Kind: core.KindVideo, @@ -43,7 +44,7 @@ func (p *SnapshotProducer) Start() error { // Fetch snapshot response, err := p.client.Request("GET", fmt.Sprintf("https://app-snaps.ring.com/snapshots/next/%d", int(p.camera.ID)), nil) if err != nil { - return fmt.Errorf("failed to get snapshot: %w", err) + return err } pkt := &rtp.Packet{ @@ -51,10 +52,7 @@ func (p *SnapshotProducer) Start() error { Payload: response, } - // Send to all receivers - for _, receiver := range p.Receivers { - receiver.WriteRTP(pkt) - } + p.Receivers[0].WriteRTP(pkt) return nil } From 82f6c2c550ce2a004a1600821519a338ae624e26 Mon Sep 17 00:00:00 2001 From: Alex X Date: Sun, 26 Jan 2025 16:09:50 +0300 Subject: [PATCH 139/166] Add support H264, H265, NV12 for V4L2 source #1546 --- pkg/h264/annexb/annexb_test.go | 12 ++++++++++++ pkg/v4l2/device/device.go | 26 +++++++++++++++++--------- pkg/v4l2/device/formats.go | 26 ++++++++++++++++++++++++-- pkg/v4l2/producer.go | 33 +++++++++++++++++++++++++++------ 4 files changed, 80 insertions(+), 17 deletions(-) diff --git a/pkg/h264/annexb/annexb_test.go b/pkg/h264/annexb/annexb_test.go index 7220f570..cbc382fe 100644 --- a/pkg/h264/annexb/annexb_test.go +++ b/pkg/h264/annexb/annexb_test.go @@ -83,3 +83,15 @@ func TestDahua(t *testing.T) { n := naluTypes(b) require.Equal(t, []byte{0x40, 0x42, 0x44, 0x26}, n) } + +func TestUSB(t *testing.T) { + s := "00 00 00 01 67 4D 00 1F 8D 8D 40 28 02 DD 37 01 01 01 40 00 01 C2 00 00 57 E4 01 00 00 00 01 68 EE 3C 80 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 65 88 80 00" + b := EncodeToAVCC(decode(s)) + n := naluTypes(b) + require.Equal(t, []byte{0x67, 0x68, 0x65}, n) + + s = "00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 41 9A 00 4C" + b = EncodeToAVCC(decode(s)) + n = naluTypes(b) + require.Equal(t, []byte{0x41}, n) +} diff --git a/pkg/v4l2/device/device.go b/pkg/v4l2/device/device.go index 7f16fd23..c77d60f5 100644 --- a/pkg/v4l2/device/device.go +++ b/pkg/v4l2/device/device.go @@ -11,8 +11,9 @@ import ( ) type Device struct { - fd int - bufs [][]byte + fd int + bufs [][]byte + pixFmt uint32 } func Open(path string) (*Device, error) { @@ -119,6 +120,8 @@ func (d *Device) ListFrameRates(pixFmt, width, height uint32) ([]uint32, error) } func (d *Device) SetFormat(width, height, pixFmt uint32) error { + d.pixFmt = pixFmt + f := v4l2_format{ typ: V4L2_BUF_TYPE_VIDEO_CAPTURE, pix: v4l2_pix_format{ @@ -196,7 +199,7 @@ func (d *Device) StreamOff() (err error) { return ioctl(d.fd, VIDIOC_REQBUFS, unsafe.Pointer(&rb)) } -func (d *Device) Capture(planarYUV bool) ([]byte, error) { +func (d *Device) Capture() ([]byte, error) { dec := v4l2_buffer{ typ: V4L2_BUF_TYPE_VIDEO_CAPTURE, memory: V4L2_MEMORY_MMAP, @@ -205,11 +208,16 @@ func (d *Device) Capture(planarYUV bool) ([]byte, error) { return nil, err } - buf := make([]byte, dec.bytesused) - if planarYUV { - YUYV2YUV(buf, d.bufs[dec.index][:dec.bytesused]) - } else { - copy(buf, d.bufs[dec.index][:dec.bytesused]) + src := d.bufs[dec.index][:dec.bytesused] + dst := make([]byte, dec.bytesused) + + switch d.pixFmt { + case V4L2_PIX_FMT_YUYV: + YUYVtoYUV(dst, src) + case V4L2_PIX_FMT_NV12: + NV12toYUV(dst, src) + default: + copy(dst, d.bufs[dec.index][:dec.bytesused]) } enc := v4l2_buffer{ @@ -221,7 +229,7 @@ func (d *Device) Capture(planarYUV bool) ([]byte, error) { return nil, err } - return buf, nil + return dst, nil } func (d *Device) Close() error { diff --git a/pkg/v4l2/device/formats.go b/pkg/v4l2/device/formats.go index fb54bbd1..a0b41082 100644 --- a/pkg/v4l2/device/formats.go +++ b/pkg/v4l2/device/formats.go @@ -2,7 +2,10 @@ package device const ( V4L2_PIX_FMT_YUYV = 'Y' | 'U'<<8 | 'Y'<<16 | 'V'<<24 + V4L2_PIX_FMT_NV12 = 'N' | 'V'<<8 | '1'<<16 | '2'<<24 V4L2_PIX_FMT_MJPEG = 'M' | 'J'<<8 | 'P'<<16 | 'G'<<24 + V4L2_PIX_FMT_H264 = 'H' | '2'<<8 | '6'<<16 | '4'<<24 + V4L2_PIX_FMT_HEVC = 'H' | 'E'<<8 | 'V'<<16 | 'C'<<24 ) type Format struct { @@ -13,11 +16,13 @@ type Format struct { var Formats = []Format{ {V4L2_PIX_FMT_YUYV, "YUV 4:2:2", "yuyv422"}, + {V4L2_PIX_FMT_NV12, "Y/UV 4:2:0", "nv12"}, {V4L2_PIX_FMT_MJPEG, "Motion-JPEG", "mjpeg"}, + {V4L2_PIX_FMT_H264, "H.264", "h264"}, + {V4L2_PIX_FMT_HEVC, "HEVC", "hevc"}, } -// YUYV2YUV convert packed YUV to planar YUV -func YUYV2YUV(dst, src []byte) { +func YUYVtoYUV(dst, src []byte) { n := len(src) i0 := 0 iy := 0 @@ -38,3 +43,20 @@ func YUYV2YUV(dst, src []byte) { iv++ } } + +func NV12toYUV(dst, src []byte) { + n := len(src) + k := n / 6 + i0 := k * 4 + iu := i0 + iv := i0 + k + copy(dst, src[:i0]) // copy Y + for i0 < n { + dst[iu] = src[i0] + i0++ + iu++ + dst[iv] = src[i0] + i0++ + iv++ + } +} diff --git a/pkg/v4l2/producer.go b/pkg/v4l2/producer.go index 87199762..663d0a9e 100644 --- a/pkg/v4l2/producer.go +++ b/pkg/v4l2/producer.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/AlexxIT/go2rtc/pkg/h264/annexb" "github.com/AlexxIT/go2rtc/pkg/v4l2/device" "github.com/pion/rtp" ) @@ -46,17 +47,29 @@ func Open(rawURL string) (*Producer, error) { } switch query.Get("input_format") { - case "mjpeg": - codec.Name = core.CodecJPEG - pixFmt = device.V4L2_PIX_FMT_MJPEG case "yuyv422": if codec.FmtpLine == "" { return nil, errors.New("v4l2: invalid video_size") } - codec.Name = core.CodecRAW codec.FmtpLine += ";colorspace=422" pixFmt = device.V4L2_PIX_FMT_YUYV + case "nv12": + if codec.FmtpLine == "" { + return nil, errors.New("v4l2: invalid video_size") + } + codec.Name = core.CodecRAW + codec.FmtpLine += ";colorspace=420mpeg2" // maybe 420jpeg + pixFmt = device.V4L2_PIX_FMT_NV12 + case "mjpeg": + codec.Name = core.CodecJPEG + pixFmt = device.V4L2_PIX_FMT_MJPEG + case "h264": + codec.Name = core.CodecH264 + pixFmt = device.V4L2_PIX_FMT_H264 + case "hevc": + codec.Name = core.CodecH265 + pixFmt = device.V4L2_PIX_FMT_HEVC default: return nil, errors.New("v4l2: invalid input_format") } @@ -93,10 +106,14 @@ func (c *Producer) Start() error { return err } - planarYUV := c.Medias[0].Codecs[0].Name == core.CodecRAW + var bitstream bool + switch c.Medias[0].Codecs[0].Name { + case core.CodecH264, core.CodecH265: + bitstream = true + } for { - buf, err := c.dev.Capture(planarYUV) + buf, err := c.dev.Capture() if err != nil { return err } @@ -107,6 +124,10 @@ func (c *Producer) Start() error { continue } + if bitstream { + buf = annexb.EncodeToAVCC(buf) + } + pkt := &rtp.Packet{ Header: rtp.Header{Timestamp: core.Now90000()}, Payload: buf, From 35cf82f11ca6e85e680f59e07d340f45376911e1 Mon Sep 17 00:00:00 2001 From: Alex X Date: Sun, 2 Feb 2025 11:01:44 +0300 Subject: [PATCH 140/166] Ignore unknown NAL unit types for RTP/H264 #1570 --- pkg/h264/rtp.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/h264/rtp.go b/pkg/h264/rtp.go index b4a9dafb..d093254f 100644 --- a/pkg/h264/rtp.go +++ b/pkg/h264/rtp.go @@ -22,7 +22,7 @@ func RTPDepay(codec *core.Codec, handler core.HandlerFunc) core.HandlerFunc { buf := make([]byte, 0, 512*1024) // 512K return func(packet *rtp.Packet) { - //log.Printf("[RTP] codec: %s, nalu: %2d, size: %6d, ts: %10d, pt: %2d, ssrc: %d, seq: %d, %v", track.Codec.Name, packet.Payload[0]&0x1F, len(packet.Payload), packet.Timestamp, packet.PayloadType, packet.SSRC, packet.SequenceNumber, packet.Marker) + //log.Printf("[RTP] codec: %s, nalu: %2d, size: %6d, ts: %10d, pt: %2d, ssrc: %d, seq: %d, %v", codec.Name, packet.Payload[0]&0x1F, len(packet.Payload), packet.Timestamp, packet.PayloadType, packet.SSRC, packet.SequenceNumber, packet.Marker) payload, err := depack.Unmarshal(packet.Payload) if len(payload) == 0 || err != nil { @@ -68,6 +68,9 @@ func RTPDepay(codec *core.Codec, handler core.HandlerFunc) core.HandlerFunc { payload = payload[i:] continue + case NALUTypePFrame, NALUTypeSPS, NALUTypePPS: // pass + default: + return // skip any unknown NAL unit type } break } From eeb0012e7f01e00539fef4efff25cdff86093071 Mon Sep 17 00:00:00 2001 From: Alex X Date: Sun, 2 Feb 2025 14:46:37 +0300 Subject: [PATCH 141/166] Improve delay for MSE player --- www/video-rtc.js | 38 ++++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/www/video-rtc.js b/www/video-rtc.js index 52fb5dda..fb872b45 100644 --- a/www/video-rtc.js +++ b/www/video-rtc.js @@ -439,24 +439,30 @@ export class VideoRTC extends HTMLElement { const sb = ms.addSourceBuffer(msg.value); sb.mode = 'segments'; // segments or sequence sb.addEventListener('updateend', () => { - if (sb.updating) return; - - try { - if (bufLen > 0) { + if (!sb.updating && bufLen > 0) { + try { const data = buf.slice(0, bufLen); - bufLen = 0; sb.appendBuffer(data); - } else if (sb.buffered && sb.buffered.length) { - const end = sb.buffered.end(sb.buffered.length - 1) - 15; - const start = sb.buffered.start(0); - if (end > start) { - sb.remove(start, end); - ms.setLiveSeekableRange(end, end + 15); - } - // console.debug("VideoRTC.buffered", start, end); + bufLen = 0; + } catch (e) { + // console.debug(e); } - } catch (e) { - // console.debug(e); + } + + if (!sb.updating && sb.buffered && sb.buffered.length) { + const end = sb.buffered.end(sb.buffered.length - 1); + const start = end - 5; + const start0 = sb.buffered.start(0); + if (start > start0) { + sb.remove(start0, start); + ms.setLiveSeekableRange(start, end); + } + if (this.video.currentTime < start) { + this.video.currentTime = start; + } + const gap = end - this.video.currentTime; + this.video.playbackRate = gap > 0.1 ? gap : 0.1; + // console.debug('VideoRTC.buffered', gap, this.video.playbackRate, this.video.readyState); } }); @@ -468,7 +474,7 @@ export class VideoRTC extends HTMLElement { const b = new Uint8Array(data); buf.set(b, bufLen); bufLen += b.byteLength; - // console.debug("VideoRTC.buffer", b.byteLength, bufLen); + // console.debug('VideoRTC.buffer', b.byteLength, bufLen); } else { try { sb.appendBuffer(data); From 297ecfbae3290b555941b23fbf0ce2404efd7e22 Mon Sep 17 00:00:00 2001 From: Alex X Date: Sun, 2 Feb 2025 15:41:30 +0300 Subject: [PATCH 142/166] Add readme for V4L2 module --- internal/v4l2/README.md | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 internal/v4l2/README.md diff --git a/internal/v4l2/README.md b/internal/v4l2/README.md new file mode 100644 index 00000000..1c5dd390 --- /dev/null +++ b/internal/v4l2/README.md @@ -0,0 +1,39 @@ +# V4L2 + +What you should to know about [V4L2](https://en.wikipedia.org/wiki/Video4Linux): + +- V4L2 (Video for Linux API version 2) works only in Linux +- supports USB cameras and other similar devices +- one device can only be connected to one software simultaneously +- cameras support a fixed list of formats, resolutions and frame rates +- basic cameras supports only RAW (non-compressed) pixel formats +- regular cameras supports MJPEG format (series of JPEG frames) +- advances cameras support H264 format (MSE/MP4, WebRTC compatible) +- using MJPEG and H264 formats (if the camera supports them) won't cost you the CPU usage +- transcoding RAW format to MJPEG or H264 - will cost you a significant CPU usage +- H265 (HEVC) format is also supported (if the camera supports it) + +Tests show that the basic Keenetic router with MIPS processor can broadcast three MJPEG cameras in the following resolutions: 1600х1200 + 640х480 + 640х480. The USB bus bandwidth is no more enough for larger resolutions. CPU consumption is no more than 5%. + +Supported formats for your camera can be found here: **Go2rtc > WebUI > Add > V4L2**. + +## RAW format + +Example: + +```yaml +streams: + camera1: v4l2:device?video=/dev/video0&input_format=yuyv422&video_size=1280x720&framerate=10 +``` + +Go2rtc supports built-in transcoding of RAW to MJPEG format. This does not need to be additionally configured. + +``` +ffplay http://localhost:1984/api/stream.mjpeg?src=camera1 +``` + +**Important.** You don't have to transcode the RAW format to transmit it over the network. You can stream it in `y4m` format, which is perfectly supported by ffmpeg. It won't cost you a CPU usage. But will require high network bandwidth. + +``` +ffplay http://localhost:1984/api/stream.y4m?src=camera1 +``` From 876390aa680940a2f9e9b12711955398bbb94820 Mon Sep 17 00:00:00 2001 From: Julian Date: Sun, 2 Feb 2025 22:15:19 +0000 Subject: [PATCH 143/166] Update build.yml Fix Build binaries This request has been automatically failed because it uses a deprecated version of `actions/upload-artifact: v3`. Learn more: https://github.blog/changelog/2024-04-16-deprecation-notice-v3-of-the-artifact-actions/ --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1bb01ca7..0bc21d11 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -102,14 +102,14 @@ jobs: env: { GOOS: freebsd, GOARCH: amd64 } run: go build -ldflags "-s -w" -trimpath - name: Upload go2rtc_freebsd_amd64 - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: { name: go2rtc_freebsd_amd64, path: go2rtc } - name: Build go2rtc_freebsd_arm64 env: { GOOS: freebsd, GOARCH: arm64 } run: go build -ldflags "-s -w" -trimpath - name: Upload go2rtc_freebsd_arm64 - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: { name: go2rtc_freebsd_arm64, path: go2rtc } docker-master: From 36547a7343ff80c5167cc739ebce6dc67a766e43 Mon Sep 17 00:00:00 2001 From: Alex X Date: Sun, 26 Jan 2025 16:09:50 +0300 Subject: [PATCH 144/166] Add support H264, H265, NV12 for V4L2 source #1546 --- pkg/h264/annexb/annexb_test.go | 12 ++++++++++++ pkg/v4l2/device/device.go | 26 +++++++++++++++++--------- pkg/v4l2/device/formats.go | 26 ++++++++++++++++++++++++-- pkg/v4l2/producer.go | 33 +++++++++++++++++++++++++++------ 4 files changed, 80 insertions(+), 17 deletions(-) diff --git a/pkg/h264/annexb/annexb_test.go b/pkg/h264/annexb/annexb_test.go index 7220f570..cbc382fe 100644 --- a/pkg/h264/annexb/annexb_test.go +++ b/pkg/h264/annexb/annexb_test.go @@ -83,3 +83,15 @@ func TestDahua(t *testing.T) { n := naluTypes(b) require.Equal(t, []byte{0x40, 0x42, 0x44, 0x26}, n) } + +func TestUSB(t *testing.T) { + s := "00 00 00 01 67 4D 00 1F 8D 8D 40 28 02 DD 37 01 01 01 40 00 01 C2 00 00 57 E4 01 00 00 00 01 68 EE 3C 80 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 65 88 80 00" + b := EncodeToAVCC(decode(s)) + n := naluTypes(b) + require.Equal(t, []byte{0x67, 0x68, 0x65}, n) + + s = "00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 41 9A 00 4C" + b = EncodeToAVCC(decode(s)) + n = naluTypes(b) + require.Equal(t, []byte{0x41}, n) +} diff --git a/pkg/v4l2/device/device.go b/pkg/v4l2/device/device.go index 7f16fd23..c77d60f5 100644 --- a/pkg/v4l2/device/device.go +++ b/pkg/v4l2/device/device.go @@ -11,8 +11,9 @@ import ( ) type Device struct { - fd int - bufs [][]byte + fd int + bufs [][]byte + pixFmt uint32 } func Open(path string) (*Device, error) { @@ -119,6 +120,8 @@ func (d *Device) ListFrameRates(pixFmt, width, height uint32) ([]uint32, error) } func (d *Device) SetFormat(width, height, pixFmt uint32) error { + d.pixFmt = pixFmt + f := v4l2_format{ typ: V4L2_BUF_TYPE_VIDEO_CAPTURE, pix: v4l2_pix_format{ @@ -196,7 +199,7 @@ func (d *Device) StreamOff() (err error) { return ioctl(d.fd, VIDIOC_REQBUFS, unsafe.Pointer(&rb)) } -func (d *Device) Capture(planarYUV bool) ([]byte, error) { +func (d *Device) Capture() ([]byte, error) { dec := v4l2_buffer{ typ: V4L2_BUF_TYPE_VIDEO_CAPTURE, memory: V4L2_MEMORY_MMAP, @@ -205,11 +208,16 @@ func (d *Device) Capture(planarYUV bool) ([]byte, error) { return nil, err } - buf := make([]byte, dec.bytesused) - if planarYUV { - YUYV2YUV(buf, d.bufs[dec.index][:dec.bytesused]) - } else { - copy(buf, d.bufs[dec.index][:dec.bytesused]) + src := d.bufs[dec.index][:dec.bytesused] + dst := make([]byte, dec.bytesused) + + switch d.pixFmt { + case V4L2_PIX_FMT_YUYV: + YUYVtoYUV(dst, src) + case V4L2_PIX_FMT_NV12: + NV12toYUV(dst, src) + default: + copy(dst, d.bufs[dec.index][:dec.bytesused]) } enc := v4l2_buffer{ @@ -221,7 +229,7 @@ func (d *Device) Capture(planarYUV bool) ([]byte, error) { return nil, err } - return buf, nil + return dst, nil } func (d *Device) Close() error { diff --git a/pkg/v4l2/device/formats.go b/pkg/v4l2/device/formats.go index fb54bbd1..a0b41082 100644 --- a/pkg/v4l2/device/formats.go +++ b/pkg/v4l2/device/formats.go @@ -2,7 +2,10 @@ package device const ( V4L2_PIX_FMT_YUYV = 'Y' | 'U'<<8 | 'Y'<<16 | 'V'<<24 + V4L2_PIX_FMT_NV12 = 'N' | 'V'<<8 | '1'<<16 | '2'<<24 V4L2_PIX_FMT_MJPEG = 'M' | 'J'<<8 | 'P'<<16 | 'G'<<24 + V4L2_PIX_FMT_H264 = 'H' | '2'<<8 | '6'<<16 | '4'<<24 + V4L2_PIX_FMT_HEVC = 'H' | 'E'<<8 | 'V'<<16 | 'C'<<24 ) type Format struct { @@ -13,11 +16,13 @@ type Format struct { var Formats = []Format{ {V4L2_PIX_FMT_YUYV, "YUV 4:2:2", "yuyv422"}, + {V4L2_PIX_FMT_NV12, "Y/UV 4:2:0", "nv12"}, {V4L2_PIX_FMT_MJPEG, "Motion-JPEG", "mjpeg"}, + {V4L2_PIX_FMT_H264, "H.264", "h264"}, + {V4L2_PIX_FMT_HEVC, "HEVC", "hevc"}, } -// YUYV2YUV convert packed YUV to planar YUV -func YUYV2YUV(dst, src []byte) { +func YUYVtoYUV(dst, src []byte) { n := len(src) i0 := 0 iy := 0 @@ -38,3 +43,20 @@ func YUYV2YUV(dst, src []byte) { iv++ } } + +func NV12toYUV(dst, src []byte) { + n := len(src) + k := n / 6 + i0 := k * 4 + iu := i0 + iv := i0 + k + copy(dst, src[:i0]) // copy Y + for i0 < n { + dst[iu] = src[i0] + i0++ + iu++ + dst[iv] = src[i0] + i0++ + iv++ + } +} diff --git a/pkg/v4l2/producer.go b/pkg/v4l2/producer.go index 87199762..663d0a9e 100644 --- a/pkg/v4l2/producer.go +++ b/pkg/v4l2/producer.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/AlexxIT/go2rtc/pkg/h264/annexb" "github.com/AlexxIT/go2rtc/pkg/v4l2/device" "github.com/pion/rtp" ) @@ -46,17 +47,29 @@ func Open(rawURL string) (*Producer, error) { } switch query.Get("input_format") { - case "mjpeg": - codec.Name = core.CodecJPEG - pixFmt = device.V4L2_PIX_FMT_MJPEG case "yuyv422": if codec.FmtpLine == "" { return nil, errors.New("v4l2: invalid video_size") } - codec.Name = core.CodecRAW codec.FmtpLine += ";colorspace=422" pixFmt = device.V4L2_PIX_FMT_YUYV + case "nv12": + if codec.FmtpLine == "" { + return nil, errors.New("v4l2: invalid video_size") + } + codec.Name = core.CodecRAW + codec.FmtpLine += ";colorspace=420mpeg2" // maybe 420jpeg + pixFmt = device.V4L2_PIX_FMT_NV12 + case "mjpeg": + codec.Name = core.CodecJPEG + pixFmt = device.V4L2_PIX_FMT_MJPEG + case "h264": + codec.Name = core.CodecH264 + pixFmt = device.V4L2_PIX_FMT_H264 + case "hevc": + codec.Name = core.CodecH265 + pixFmt = device.V4L2_PIX_FMT_HEVC default: return nil, errors.New("v4l2: invalid input_format") } @@ -93,10 +106,14 @@ func (c *Producer) Start() error { return err } - planarYUV := c.Medias[0].Codecs[0].Name == core.CodecRAW + var bitstream bool + switch c.Medias[0].Codecs[0].Name { + case core.CodecH264, core.CodecH265: + bitstream = true + } for { - buf, err := c.dev.Capture(planarYUV) + buf, err := c.dev.Capture() if err != nil { return err } @@ -107,6 +124,10 @@ func (c *Producer) Start() error { continue } + if bitstream { + buf = annexb.EncodeToAVCC(buf) + } + pkt := &rtp.Packet{ Header: rtp.Header{Timestamp: core.Now90000()}, Payload: buf, From 9b392a22e1a19992fe9baa8ae4c8977d3a7fb78d Mon Sep 17 00:00:00 2001 From: Alex X Date: Sun, 2 Feb 2025 11:01:44 +0300 Subject: [PATCH 145/166] Ignore unknown NAL unit types for RTP/H264 #1570 --- pkg/h264/rtp.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/h264/rtp.go b/pkg/h264/rtp.go index b4a9dafb..d093254f 100644 --- a/pkg/h264/rtp.go +++ b/pkg/h264/rtp.go @@ -22,7 +22,7 @@ func RTPDepay(codec *core.Codec, handler core.HandlerFunc) core.HandlerFunc { buf := make([]byte, 0, 512*1024) // 512K return func(packet *rtp.Packet) { - //log.Printf("[RTP] codec: %s, nalu: %2d, size: %6d, ts: %10d, pt: %2d, ssrc: %d, seq: %d, %v", track.Codec.Name, packet.Payload[0]&0x1F, len(packet.Payload), packet.Timestamp, packet.PayloadType, packet.SSRC, packet.SequenceNumber, packet.Marker) + //log.Printf("[RTP] codec: %s, nalu: %2d, size: %6d, ts: %10d, pt: %2d, ssrc: %d, seq: %d, %v", codec.Name, packet.Payload[0]&0x1F, len(packet.Payload), packet.Timestamp, packet.PayloadType, packet.SSRC, packet.SequenceNumber, packet.Marker) payload, err := depack.Unmarshal(packet.Payload) if len(payload) == 0 || err != nil { @@ -68,6 +68,9 @@ func RTPDepay(codec *core.Codec, handler core.HandlerFunc) core.HandlerFunc { payload = payload[i:] continue + case NALUTypePFrame, NALUTypeSPS, NALUTypePPS: // pass + default: + return // skip any unknown NAL unit type } break } From b14aa4f0dcf9cb2a577195b4abb529004adef40d Mon Sep 17 00:00:00 2001 From: Alex X Date: Sun, 2 Feb 2025 14:46:37 +0300 Subject: [PATCH 146/166] Improve delay for MSE player --- www/video-rtc.js | 38 ++++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/www/video-rtc.js b/www/video-rtc.js index 52fb5dda..fb872b45 100644 --- a/www/video-rtc.js +++ b/www/video-rtc.js @@ -439,24 +439,30 @@ export class VideoRTC extends HTMLElement { const sb = ms.addSourceBuffer(msg.value); sb.mode = 'segments'; // segments or sequence sb.addEventListener('updateend', () => { - if (sb.updating) return; - - try { - if (bufLen > 0) { + if (!sb.updating && bufLen > 0) { + try { const data = buf.slice(0, bufLen); - bufLen = 0; sb.appendBuffer(data); - } else if (sb.buffered && sb.buffered.length) { - const end = sb.buffered.end(sb.buffered.length - 1) - 15; - const start = sb.buffered.start(0); - if (end > start) { - sb.remove(start, end); - ms.setLiveSeekableRange(end, end + 15); - } - // console.debug("VideoRTC.buffered", start, end); + bufLen = 0; + } catch (e) { + // console.debug(e); } - } catch (e) { - // console.debug(e); + } + + if (!sb.updating && sb.buffered && sb.buffered.length) { + const end = sb.buffered.end(sb.buffered.length - 1); + const start = end - 5; + const start0 = sb.buffered.start(0); + if (start > start0) { + sb.remove(start0, start); + ms.setLiveSeekableRange(start, end); + } + if (this.video.currentTime < start) { + this.video.currentTime = start; + } + const gap = end - this.video.currentTime; + this.video.playbackRate = gap > 0.1 ? gap : 0.1; + // console.debug('VideoRTC.buffered', gap, this.video.playbackRate, this.video.readyState); } }); @@ -468,7 +474,7 @@ export class VideoRTC extends HTMLElement { const b = new Uint8Array(data); buf.set(b, bufLen); bufLen += b.byteLength; - // console.debug("VideoRTC.buffer", b.byteLength, bufLen); + // console.debug('VideoRTC.buffer', b.byteLength, bufLen); } else { try { sb.appendBuffer(data); From b139b8fdd6f69f716d5ac8421501d0c8b9ab6031 Mon Sep 17 00:00:00 2001 From: Alex X Date: Sun, 2 Feb 2025 15:41:30 +0300 Subject: [PATCH 147/166] Add readme for V4L2 module --- internal/v4l2/README.md | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 internal/v4l2/README.md diff --git a/internal/v4l2/README.md b/internal/v4l2/README.md new file mode 100644 index 00000000..1c5dd390 --- /dev/null +++ b/internal/v4l2/README.md @@ -0,0 +1,39 @@ +# V4L2 + +What you should to know about [V4L2](https://en.wikipedia.org/wiki/Video4Linux): + +- V4L2 (Video for Linux API version 2) works only in Linux +- supports USB cameras and other similar devices +- one device can only be connected to one software simultaneously +- cameras support a fixed list of formats, resolutions and frame rates +- basic cameras supports only RAW (non-compressed) pixel formats +- regular cameras supports MJPEG format (series of JPEG frames) +- advances cameras support H264 format (MSE/MP4, WebRTC compatible) +- using MJPEG and H264 formats (if the camera supports them) won't cost you the CPU usage +- transcoding RAW format to MJPEG or H264 - will cost you a significant CPU usage +- H265 (HEVC) format is also supported (if the camera supports it) + +Tests show that the basic Keenetic router with MIPS processor can broadcast three MJPEG cameras in the following resolutions: 1600х1200 + 640х480 + 640х480. The USB bus bandwidth is no more enough for larger resolutions. CPU consumption is no more than 5%. + +Supported formats for your camera can be found here: **Go2rtc > WebUI > Add > V4L2**. + +## RAW format + +Example: + +```yaml +streams: + camera1: v4l2:device?video=/dev/video0&input_format=yuyv422&video_size=1280x720&framerate=10 +``` + +Go2rtc supports built-in transcoding of RAW to MJPEG format. This does not need to be additionally configured. + +``` +ffplay http://localhost:1984/api/stream.mjpeg?src=camera1 +``` + +**Important.** You don't have to transcode the RAW format to transmit it over the network. You can stream it in `y4m` format, which is perfectly supported by ffmpeg. It won't cost you a CPU usage. But will require high network bandwidth. + +``` +ffplay http://localhost:1984/api/stream.y4m?src=camera1 +``` From ece49a158e0f326decbafc1a1172d38ad844ed96 Mon Sep 17 00:00:00 2001 From: Alex X Date: Sun, 26 Jan 2025 16:09:50 +0300 Subject: [PATCH 148/166] Add support H264, H265, NV12 for V4L2 source #1546 --- pkg/h264/annexb/annexb_test.go | 12 ++++++++++++ pkg/v4l2/device/device.go | 26 +++++++++++++++++--------- pkg/v4l2/device/formats.go | 26 ++++++++++++++++++++++++-- pkg/v4l2/producer.go | 33 +++++++++++++++++++++++++++------ 4 files changed, 80 insertions(+), 17 deletions(-) diff --git a/pkg/h264/annexb/annexb_test.go b/pkg/h264/annexb/annexb_test.go index 7220f570..cbc382fe 100644 --- a/pkg/h264/annexb/annexb_test.go +++ b/pkg/h264/annexb/annexb_test.go @@ -83,3 +83,15 @@ func TestDahua(t *testing.T) { n := naluTypes(b) require.Equal(t, []byte{0x40, 0x42, 0x44, 0x26}, n) } + +func TestUSB(t *testing.T) { + s := "00 00 00 01 67 4D 00 1F 8D 8D 40 28 02 DD 37 01 01 01 40 00 01 C2 00 00 57 E4 01 00 00 00 01 68 EE 3C 80 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 65 88 80 00" + b := EncodeToAVCC(decode(s)) + n := naluTypes(b) + require.Equal(t, []byte{0x67, 0x68, 0x65}, n) + + s = "00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 41 9A 00 4C" + b = EncodeToAVCC(decode(s)) + n = naluTypes(b) + require.Equal(t, []byte{0x41}, n) +} diff --git a/pkg/v4l2/device/device.go b/pkg/v4l2/device/device.go index 7f16fd23..c77d60f5 100644 --- a/pkg/v4l2/device/device.go +++ b/pkg/v4l2/device/device.go @@ -11,8 +11,9 @@ import ( ) type Device struct { - fd int - bufs [][]byte + fd int + bufs [][]byte + pixFmt uint32 } func Open(path string) (*Device, error) { @@ -119,6 +120,8 @@ func (d *Device) ListFrameRates(pixFmt, width, height uint32) ([]uint32, error) } func (d *Device) SetFormat(width, height, pixFmt uint32) error { + d.pixFmt = pixFmt + f := v4l2_format{ typ: V4L2_BUF_TYPE_VIDEO_CAPTURE, pix: v4l2_pix_format{ @@ -196,7 +199,7 @@ func (d *Device) StreamOff() (err error) { return ioctl(d.fd, VIDIOC_REQBUFS, unsafe.Pointer(&rb)) } -func (d *Device) Capture(planarYUV bool) ([]byte, error) { +func (d *Device) Capture() ([]byte, error) { dec := v4l2_buffer{ typ: V4L2_BUF_TYPE_VIDEO_CAPTURE, memory: V4L2_MEMORY_MMAP, @@ -205,11 +208,16 @@ func (d *Device) Capture(planarYUV bool) ([]byte, error) { return nil, err } - buf := make([]byte, dec.bytesused) - if planarYUV { - YUYV2YUV(buf, d.bufs[dec.index][:dec.bytesused]) - } else { - copy(buf, d.bufs[dec.index][:dec.bytesused]) + src := d.bufs[dec.index][:dec.bytesused] + dst := make([]byte, dec.bytesused) + + switch d.pixFmt { + case V4L2_PIX_FMT_YUYV: + YUYVtoYUV(dst, src) + case V4L2_PIX_FMT_NV12: + NV12toYUV(dst, src) + default: + copy(dst, d.bufs[dec.index][:dec.bytesused]) } enc := v4l2_buffer{ @@ -221,7 +229,7 @@ func (d *Device) Capture(planarYUV bool) ([]byte, error) { return nil, err } - return buf, nil + return dst, nil } func (d *Device) Close() error { diff --git a/pkg/v4l2/device/formats.go b/pkg/v4l2/device/formats.go index fb54bbd1..a0b41082 100644 --- a/pkg/v4l2/device/formats.go +++ b/pkg/v4l2/device/formats.go @@ -2,7 +2,10 @@ package device const ( V4L2_PIX_FMT_YUYV = 'Y' | 'U'<<8 | 'Y'<<16 | 'V'<<24 + V4L2_PIX_FMT_NV12 = 'N' | 'V'<<8 | '1'<<16 | '2'<<24 V4L2_PIX_FMT_MJPEG = 'M' | 'J'<<8 | 'P'<<16 | 'G'<<24 + V4L2_PIX_FMT_H264 = 'H' | '2'<<8 | '6'<<16 | '4'<<24 + V4L2_PIX_FMT_HEVC = 'H' | 'E'<<8 | 'V'<<16 | 'C'<<24 ) type Format struct { @@ -13,11 +16,13 @@ type Format struct { var Formats = []Format{ {V4L2_PIX_FMT_YUYV, "YUV 4:2:2", "yuyv422"}, + {V4L2_PIX_FMT_NV12, "Y/UV 4:2:0", "nv12"}, {V4L2_PIX_FMT_MJPEG, "Motion-JPEG", "mjpeg"}, + {V4L2_PIX_FMT_H264, "H.264", "h264"}, + {V4L2_PIX_FMT_HEVC, "HEVC", "hevc"}, } -// YUYV2YUV convert packed YUV to planar YUV -func YUYV2YUV(dst, src []byte) { +func YUYVtoYUV(dst, src []byte) { n := len(src) i0 := 0 iy := 0 @@ -38,3 +43,20 @@ func YUYV2YUV(dst, src []byte) { iv++ } } + +func NV12toYUV(dst, src []byte) { + n := len(src) + k := n / 6 + i0 := k * 4 + iu := i0 + iv := i0 + k + copy(dst, src[:i0]) // copy Y + for i0 < n { + dst[iu] = src[i0] + i0++ + iu++ + dst[iv] = src[i0] + i0++ + iv++ + } +} diff --git a/pkg/v4l2/producer.go b/pkg/v4l2/producer.go index 87199762..663d0a9e 100644 --- a/pkg/v4l2/producer.go +++ b/pkg/v4l2/producer.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/AlexxIT/go2rtc/pkg/h264/annexb" "github.com/AlexxIT/go2rtc/pkg/v4l2/device" "github.com/pion/rtp" ) @@ -46,17 +47,29 @@ func Open(rawURL string) (*Producer, error) { } switch query.Get("input_format") { - case "mjpeg": - codec.Name = core.CodecJPEG - pixFmt = device.V4L2_PIX_FMT_MJPEG case "yuyv422": if codec.FmtpLine == "" { return nil, errors.New("v4l2: invalid video_size") } - codec.Name = core.CodecRAW codec.FmtpLine += ";colorspace=422" pixFmt = device.V4L2_PIX_FMT_YUYV + case "nv12": + if codec.FmtpLine == "" { + return nil, errors.New("v4l2: invalid video_size") + } + codec.Name = core.CodecRAW + codec.FmtpLine += ";colorspace=420mpeg2" // maybe 420jpeg + pixFmt = device.V4L2_PIX_FMT_NV12 + case "mjpeg": + codec.Name = core.CodecJPEG + pixFmt = device.V4L2_PIX_FMT_MJPEG + case "h264": + codec.Name = core.CodecH264 + pixFmt = device.V4L2_PIX_FMT_H264 + case "hevc": + codec.Name = core.CodecH265 + pixFmt = device.V4L2_PIX_FMT_HEVC default: return nil, errors.New("v4l2: invalid input_format") } @@ -93,10 +106,14 @@ func (c *Producer) Start() error { return err } - planarYUV := c.Medias[0].Codecs[0].Name == core.CodecRAW + var bitstream bool + switch c.Medias[0].Codecs[0].Name { + case core.CodecH264, core.CodecH265: + bitstream = true + } for { - buf, err := c.dev.Capture(planarYUV) + buf, err := c.dev.Capture() if err != nil { return err } @@ -107,6 +124,10 @@ func (c *Producer) Start() error { continue } + if bitstream { + buf = annexb.EncodeToAVCC(buf) + } + pkt := &rtp.Packet{ Header: rtp.Header{Timestamp: core.Now90000()}, Payload: buf, From 645c11f0bd8dc8308be566d0e5ab0af8a520b609 Mon Sep 17 00:00:00 2001 From: Alex X Date: Sun, 2 Feb 2025 11:01:44 +0300 Subject: [PATCH 149/166] Ignore unknown NAL unit types for RTP/H264 #1570 --- pkg/h264/rtp.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/h264/rtp.go b/pkg/h264/rtp.go index b4a9dafb..d093254f 100644 --- a/pkg/h264/rtp.go +++ b/pkg/h264/rtp.go @@ -22,7 +22,7 @@ func RTPDepay(codec *core.Codec, handler core.HandlerFunc) core.HandlerFunc { buf := make([]byte, 0, 512*1024) // 512K return func(packet *rtp.Packet) { - //log.Printf("[RTP] codec: %s, nalu: %2d, size: %6d, ts: %10d, pt: %2d, ssrc: %d, seq: %d, %v", track.Codec.Name, packet.Payload[0]&0x1F, len(packet.Payload), packet.Timestamp, packet.PayloadType, packet.SSRC, packet.SequenceNumber, packet.Marker) + //log.Printf("[RTP] codec: %s, nalu: %2d, size: %6d, ts: %10d, pt: %2d, ssrc: %d, seq: %d, %v", codec.Name, packet.Payload[0]&0x1F, len(packet.Payload), packet.Timestamp, packet.PayloadType, packet.SSRC, packet.SequenceNumber, packet.Marker) payload, err := depack.Unmarshal(packet.Payload) if len(payload) == 0 || err != nil { @@ -68,6 +68,9 @@ func RTPDepay(codec *core.Codec, handler core.HandlerFunc) core.HandlerFunc { payload = payload[i:] continue + case NALUTypePFrame, NALUTypeSPS, NALUTypePPS: // pass + default: + return // skip any unknown NAL unit type } break } From f9a8c1969c76aeb3840c4dbf643475df2081198b Mon Sep 17 00:00:00 2001 From: Alex X Date: Sun, 2 Feb 2025 14:46:37 +0300 Subject: [PATCH 150/166] Improve delay for MSE player --- www/video-rtc.js | 38 ++++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/www/video-rtc.js b/www/video-rtc.js index 52fb5dda..fb872b45 100644 --- a/www/video-rtc.js +++ b/www/video-rtc.js @@ -439,24 +439,30 @@ export class VideoRTC extends HTMLElement { const sb = ms.addSourceBuffer(msg.value); sb.mode = 'segments'; // segments or sequence sb.addEventListener('updateend', () => { - if (sb.updating) return; - - try { - if (bufLen > 0) { + if (!sb.updating && bufLen > 0) { + try { const data = buf.slice(0, bufLen); - bufLen = 0; sb.appendBuffer(data); - } else if (sb.buffered && sb.buffered.length) { - const end = sb.buffered.end(sb.buffered.length - 1) - 15; - const start = sb.buffered.start(0); - if (end > start) { - sb.remove(start, end); - ms.setLiveSeekableRange(end, end + 15); - } - // console.debug("VideoRTC.buffered", start, end); + bufLen = 0; + } catch (e) { + // console.debug(e); } - } catch (e) { - // console.debug(e); + } + + if (!sb.updating && sb.buffered && sb.buffered.length) { + const end = sb.buffered.end(sb.buffered.length - 1); + const start = end - 5; + const start0 = sb.buffered.start(0); + if (start > start0) { + sb.remove(start0, start); + ms.setLiveSeekableRange(start, end); + } + if (this.video.currentTime < start) { + this.video.currentTime = start; + } + const gap = end - this.video.currentTime; + this.video.playbackRate = gap > 0.1 ? gap : 0.1; + // console.debug('VideoRTC.buffered', gap, this.video.playbackRate, this.video.readyState); } }); @@ -468,7 +474,7 @@ export class VideoRTC extends HTMLElement { const b = new Uint8Array(data); buf.set(b, bufLen); bufLen += b.byteLength; - // console.debug("VideoRTC.buffer", b.byteLength, bufLen); + // console.debug('VideoRTC.buffer', b.byteLength, bufLen); } else { try { sb.appendBuffer(data); From 1b0db3c8b00449920c81d304f7de5b55745688ca Mon Sep 17 00:00:00 2001 From: Alex X Date: Sun, 2 Feb 2025 15:41:30 +0300 Subject: [PATCH 151/166] Add readme for V4L2 module --- internal/v4l2/README.md | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 internal/v4l2/README.md diff --git a/internal/v4l2/README.md b/internal/v4l2/README.md new file mode 100644 index 00000000..1c5dd390 --- /dev/null +++ b/internal/v4l2/README.md @@ -0,0 +1,39 @@ +# V4L2 + +What you should to know about [V4L2](https://en.wikipedia.org/wiki/Video4Linux): + +- V4L2 (Video for Linux API version 2) works only in Linux +- supports USB cameras and other similar devices +- one device can only be connected to one software simultaneously +- cameras support a fixed list of formats, resolutions and frame rates +- basic cameras supports only RAW (non-compressed) pixel formats +- regular cameras supports MJPEG format (series of JPEG frames) +- advances cameras support H264 format (MSE/MP4, WebRTC compatible) +- using MJPEG and H264 formats (if the camera supports them) won't cost you the CPU usage +- transcoding RAW format to MJPEG or H264 - will cost you a significant CPU usage +- H265 (HEVC) format is also supported (if the camera supports it) + +Tests show that the basic Keenetic router with MIPS processor can broadcast three MJPEG cameras in the following resolutions: 1600х1200 + 640х480 + 640х480. The USB bus bandwidth is no more enough for larger resolutions. CPU consumption is no more than 5%. + +Supported formats for your camera can be found here: **Go2rtc > WebUI > Add > V4L2**. + +## RAW format + +Example: + +```yaml +streams: + camera1: v4l2:device?video=/dev/video0&input_format=yuyv422&video_size=1280x720&framerate=10 +``` + +Go2rtc supports built-in transcoding of RAW to MJPEG format. This does not need to be additionally configured. + +``` +ffplay http://localhost:1984/api/stream.mjpeg?src=camera1 +``` + +**Important.** You don't have to transcode the RAW format to transmit it over the network. You can stream it in `y4m` format, which is perfectly supported by ffmpeg. It won't cost you a CPU usage. But will require high network bandwidth. + +``` +ffplay http://localhost:1984/api/stream.y4m?src=camera1 +``` From ad8c025393e47454b59a398d7877360b9ed447ff Mon Sep 17 00:00:00 2001 From: seydx Date: Sun, 3 Nov 2024 16:33:08 +0100 Subject: [PATCH 152/166] Add backchannel support for rtsp server --- pkg/rtsp/server.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pkg/rtsp/server.go b/pkg/rtsp/server.go index df2ebdb5..b7e65dac 100644 --- a/pkg/rtsp/server.go +++ b/pkg/rtsp/server.go @@ -139,6 +139,16 @@ func (c *Conn) Accept() error { medias = append(medias, media) } + for i, track := range c.Receivers { + media := &core.Media{ + Kind: core.GetKind(track.Codec.Name), + Direction: core.DirectionSendonly, + Codecs: []*core.Codec{track.Codec}, + ID: "trackID=" + strconv.Itoa(i+len(c.Senders)), + } + medias = append(medias, media) + } + res.Body, err = core.MarshalSDP(c.SessionName, medias) if err != nil { return err From ad61662cc4f56012cc5b3a0bd5e4836e72270456 Mon Sep 17 00:00:00 2001 From: Alex X Date: Fri, 7 Feb 2025 10:12:32 +0300 Subject: [PATCH 153/166] Update general H265 support for WebRTC #1439 --- pkg/webrtc/consumer.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/webrtc/consumer.go b/pkg/webrtc/consumer.go index fb90442c..e9d7b2e5 100644 --- a/pkg/webrtc/consumer.go +++ b/pkg/webrtc/consumer.go @@ -56,11 +56,11 @@ func (c *Conn) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiv } case core.CodecH265: - // SafariPay because it is the only browser in the world - // that supports WebRTC + H265 - sender.Handler = h265.SafariPay(1200, sender.Handler) + sender.Handler = h265.RTPPay(1200, sender.Handler) if track.Codec.IsRTP() { sender.Handler = h265.RTPDepay(track.Codec, sender.Handler) + } else { + sender.Handler = h265.RepairAVCC(track.Codec, sender.Handler) } case core.CodecPCMA, core.CodecPCMU, core.CodecPCM, core.CodecPCML: From c39c9aa1da7bc75a17d4d96dd779f51b5314805b Mon Sep 17 00:00:00 2001 From: Thomas Purchas Date: Thu, 6 Feb 2025 23:56:03 +0000 Subject: [PATCH 154/166] Handle malformed fmtp lines --- pkg/rtsp/helpers.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/pkg/rtsp/helpers.go b/pkg/rtsp/helpers.go index 346ecf73..3445b1d8 100644 --- a/pkg/rtsp/helpers.go +++ b/pkg/rtsp/helpers.go @@ -75,6 +75,23 @@ func UnmarshalSDP(rawSDP []byte) ([]*core.Media, error) { if codec.FmtpLine == "" { codec.FmtpLine = findFmtpLine(codec.PayloadType, sd.MediaDescriptions) } + case core.CodecH265: + if codec.FmtpLine != "" { + // All three parameters are needed for a valid fmtp line. If we're missing one + // then discard the entire line. The bitstream should contain the data in NAL units + // + // Some camera brands (notable Hikvision) don't include the vps property, rendering the entire + // line invalid, because the sps property references the non-existent vps proper. This invalid + // data will cause FFmpeg to crash with a `Could not write header (incorrect codec parameters ?): Invalid data found when processing input` + // error when attempting to repackage the HEVC stream into outgoing RTSP stream. Removing the + // fmtp line forces FFmpeg to rely on the bitstream directly, fixing this issue. + valid := strings.Contains(codec.FmtpLine, "sprop-vps=") + valid = valid && strings.Contains(codec.FmtpLine, "sprop-sps=") + valid = valid && strings.Contains(codec.FmtpLine, "sprop-pps=") + if !valid { + codec.FmtpLine = "" + } + } case core.CodecOpus: // fix OPUS for some cameras https://datatracker.ietf.org/doc/html/rfc7587 codec.ClockRate = 48000 From da809bb9d74a846ac66715aef91a9badc1421f93 Mon Sep 17 00:00:00 2001 From: Julian Date: Sun, 2 Feb 2025 22:15:19 +0000 Subject: [PATCH 155/166] Update build.yml Fix Build binaries This request has been automatically failed because it uses a deprecated version of `actions/upload-artifact: v3`. Learn more: https://github.blog/changelog/2024-04-16-deprecation-notice-v3-of-the-artifact-actions/ --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1bb01ca7..0bc21d11 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -102,14 +102,14 @@ jobs: env: { GOOS: freebsd, GOARCH: amd64 } run: go build -ldflags "-s -w" -trimpath - name: Upload go2rtc_freebsd_amd64 - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: { name: go2rtc_freebsd_amd64, path: go2rtc } - name: Build go2rtc_freebsd_arm64 env: { GOOS: freebsd, GOARCH: arm64 } run: go build -ldflags "-s -w" -trimpath - name: Upload go2rtc_freebsd_arm64 - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: { name: go2rtc_freebsd_arm64, path: go2rtc } docker-master: From e935885cd346bb4e57e9c94ef37001d908a0a1bb Mon Sep 17 00:00:00 2001 From: Alex X Date: Fri, 7 Feb 2025 10:12:32 +0300 Subject: [PATCH 156/166] Update general H265 support for WebRTC #1439 --- pkg/webrtc/consumer.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/webrtc/consumer.go b/pkg/webrtc/consumer.go index fb90442c..e9d7b2e5 100644 --- a/pkg/webrtc/consumer.go +++ b/pkg/webrtc/consumer.go @@ -56,11 +56,11 @@ func (c *Conn) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiv } case core.CodecH265: - // SafariPay because it is the only browser in the world - // that supports WebRTC + H265 - sender.Handler = h265.SafariPay(1200, sender.Handler) + sender.Handler = h265.RTPPay(1200, sender.Handler) if track.Codec.IsRTP() { sender.Handler = h265.RTPDepay(track.Codec, sender.Handler) + } else { + sender.Handler = h265.RepairAVCC(track.Codec, sender.Handler) } case core.CodecPCMA, core.CodecPCMU, core.CodecPCM, core.CodecPCML: From be2864c34b42be22da73ab8a31d1a98f103843f2 Mon Sep 17 00:00:00 2001 From: Alex X Date: Mon, 17 Feb 2025 17:07:36 +0300 Subject: [PATCH 157/166] Code refactoring after #1588 --- pkg/rtsp/helpers.go | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/pkg/rtsp/helpers.go b/pkg/rtsp/helpers.go index 3445b1d8..952730bb 100644 --- a/pkg/rtsp/helpers.go +++ b/pkg/rtsp/helpers.go @@ -77,18 +77,11 @@ func UnmarshalSDP(rawSDP []byte) ([]*core.Media, error) { } case core.CodecH265: if codec.FmtpLine != "" { - // All three parameters are needed for a valid fmtp line. If we're missing one - // then discard the entire line. The bitstream should contain the data in NAL units - // - // Some camera brands (notable Hikvision) don't include the vps property, rendering the entire - // line invalid, because the sps property references the non-existent vps proper. This invalid - // data will cause FFmpeg to crash with a `Could not write header (incorrect codec parameters ?): Invalid data found when processing input` - // error when attempting to repackage the HEVC stream into outgoing RTSP stream. Removing the - // fmtp line forces FFmpeg to rely on the bitstream directly, fixing this issue. - valid := strings.Contains(codec.FmtpLine, "sprop-vps=") - valid = valid && strings.Contains(codec.FmtpLine, "sprop-sps=") - valid = valid && strings.Contains(codec.FmtpLine, "sprop-pps=") - if !valid { + // all three parameters are needed for a valid fmtp line + // https://github.com/AlexxIT/go2rtc/pull/1588 + if !strings.Contains(codec.FmtpLine, "sprop-vps=") || + !strings.Contains(codec.FmtpLine, "sprop-sps=") || + !strings.Contains(codec.FmtpLine, "sprop-pps=") { codec.FmtpLine = "" } } From 65c87d5e0f599c2698d0515b6f9efae0fa6587c6 Mon Sep 17 00:00:00 2001 From: Felipe Santos Date: Mon, 17 Feb 2025 18:07:31 -0300 Subject: [PATCH 158/166] Fix typo in RTMP docs --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e87e35a0..d30768d0 100644 --- a/README.md +++ b/README.md @@ -881,7 +881,7 @@ Read more about [codecs filters](#codecs-filters). You can get any stream as RTMP-stream: `rtmp://192.168.1.123/{stream_name}`. Only H264/AAC codecs supported right now. -[Incoming stream](#incoming-sources) in RTMP-format tested only with [OBS Studio](https://obsproject.com/) and Dahua camera. Different FFmpeg versions has differnt problems with this format. +[Incoming stream](#incoming-sources) in RTMP-format tested only with [OBS Studio](https://obsproject.com/) and Dahua camera. Different FFmpeg versions has different problems with this format. ```yaml rtmp: From 02ac3a681432aec38ac3dbf8dcbd8db1e2fee9f5 Mon Sep 17 00:00:00 2001 From: Alex X Date: Tue, 18 Feb 2025 12:01:55 +0300 Subject: [PATCH 159/166] Code refactoring for RTSP auth --- internal/rtsp/rtsp.go | 3 ++- pkg/rtsp/server.go | 7 +++---- pkg/tcp/auth.go | 8 ++++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/internal/rtsp/rtsp.go b/internal/rtsp/rtsp.go index 2e1d04e8..c680dd07 100644 --- a/internal/rtsp/rtsp.go +++ b/internal/rtsp/rtsp.go @@ -1,6 +1,7 @@ package rtsp import ( + "errors" "io" "net" "net/url" @@ -237,7 +238,7 @@ func tcpHandler(conn *rtsp.Conn) { }) if err := conn.Accept(); err != nil { - if err == rtsp.FailedAuth { + if errors.Is(err, rtsp.FailedAuth) { log.Warn().Str("remote_addr", conn.Connection.RemoteAddr).Msg("[rtsp] failed authentication") } else if err != io.EOF { log.WithLevel(level).Err(err).Caller().Send() diff --git a/pkg/rtsp/server.go b/pkg/rtsp/server.go index 9527e155..d7e89f5f 100644 --- a/pkg/rtsp/server.go +++ b/pkg/rtsp/server.go @@ -47,7 +47,7 @@ func (c *Conn) Accept() error { c.Fire(req) - if !c.auth.Validate(req) { + if valid, empty := c.auth.Validate(req); !valid { res := &tcp.Response{ Status: "401 Unauthorized", Header: map[string][]string{"Www-Authenticate": {`Basic realm="go2rtc"`}}, @@ -56,13 +56,12 @@ func (c *Conn) Accept() error { if err = c.WriteResponse(res); err != nil { return err } - if req.Header.Get("Authorization") != "" { + if empty { // eliminate false positive: ffmpeg sends first request without // authorization header even if the user provides credentials - return FailedAuth - } else { continue } + return FailedAuth } // Receiver: OPTIONS > DESCRIBE > SETUP... > PLAY > TEARDOWN diff --git a/pkg/tcp/auth.go b/pkg/tcp/auth.go index ac212fcf..3eb26024 100644 --- a/pkg/tcp/auth.go +++ b/pkg/tcp/auth.go @@ -85,14 +85,14 @@ func (a *Auth) Write(req *Request) { } } -func (a *Auth) Validate(req *Request) bool { +func (a *Auth) Validate(req *Request) (valid, empty bool) { if a == nil { - return true + return true, true } header := req.Header.Get("Authorization") if header == "" { - return false + return false, true } if a.Method == AuthUnknown { @@ -100,7 +100,7 @@ func (a *Auth) Validate(req *Request) bool { a.header = "Basic " + B64(a.user, a.pass) } - return header == a.header + return header == a.header, false } func (a *Auth) ReadNone(res *Response) bool { From 637e65e5a059defcdb9ce80eaffec00a01ae3d5d Mon Sep 17 00:00:00 2001 From: Alex X Date: Tue, 18 Feb 2025 12:49:33 +0300 Subject: [PATCH 160/166] Code refactoring for RTSP transport header processing --- pkg/rtsp/server.go | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/pkg/rtsp/server.go b/pkg/rtsp/server.go index cefaef1d..29f97b5c 100644 --- a/pkg/rtsp/server.go +++ b/pkg/rtsp/server.go @@ -141,17 +141,14 @@ func (c *Conn) Accept() error { } case MethodSetup: - tr := req.Header.Get("Transport") - res := &tcp.Response{ Header: map[string][]string{}, Request: req, } // Test if client requests TCP transport, otherwise return 461 Transport not supported - // This allows smart clients who initially requested UDP to fall back on TCP transport. - if strings.HasPrefix(tr, "RTP/AVP/TCP") { - + // This allows smart clients who initially requested UDP to fall back on TCP transport + if tr := req.Header.Get("Transport"); strings.HasPrefix(tr, "RTP/AVP/TCP") { c.session = core.RandString(8, 10) c.state = StateSetup @@ -159,16 +156,8 @@ func (c *Conn) Accept() error { if i := reqTrackID(req); i >= 0 && i < len(c.Senders) { // mark sender as SETUP c.Senders[i].Media.ID = MethodSetup - interleaved := fmt.Sprintf("%d-%d", i*2, i*2+1) - - // Check if tr already contains the 'interleaved' parameter - if strings.Contains(tr, "interleaved=") { - // If so, just update the interleaved value - res.Header.Set("Transport", strings.Replace(tr, "interleaved=[^;]*", "interleaved="+interleaved, 1)) - } else { - // Otherwise, append the interleaved parameter - res.Header.Set("Transport", tr+";interleaved="+interleaved) - } + tr = fmt.Sprintf("RTP/AVP/TCP;unicast;interleaved=%d-%d", i*2, i*2+1) + res.Header.Set("Transport", tr) } else { res.Status = "400 Bad Request" } From b34d970076de676b0cc69b7c8716b8b2df4a00aa Mon Sep 17 00:00:00 2001 From: seydx Date: Tue, 18 Feb 2025 11:52:57 +0100 Subject: [PATCH 161/166] remove duplicated code --- pkg/rtsp/server.go | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/pkg/rtsp/server.go b/pkg/rtsp/server.go index b7e65dac..df2ebdb5 100644 --- a/pkg/rtsp/server.go +++ b/pkg/rtsp/server.go @@ -139,16 +139,6 @@ func (c *Conn) Accept() error { medias = append(medias, media) } - for i, track := range c.Receivers { - media := &core.Media{ - Kind: core.GetKind(track.Codec.Name), - Direction: core.DirectionSendonly, - Codecs: []*core.Codec{track.Codec}, - ID: "trackID=" + strconv.Itoa(i+len(c.Senders)), - } - medias = append(medias, media) - } - res.Body, err = core.MarshalSDP(c.SessionName, medias) if err != nil { return err From 0a773c82aff1bc8a5cc2c058f845b14b7ca34f3e Mon Sep 17 00:00:00 2001 From: Alex X Date: Tue, 18 Feb 2025 16:59:00 +0300 Subject: [PATCH 162/166] Code refactoring for RTSP backchannel --- internal/rtsp/rtsp.go | 18 ++++++++---------- pkg/rtsp/producer.go | 41 ++++++++++++++++++----------------------- pkg/rtsp/server.go | 18 ++++++------------ 3 files changed, 32 insertions(+), 45 deletions(-) diff --git a/internal/rtsp/rtsp.go b/internal/rtsp/rtsp.go index 5c023b71..4c9ca162 100644 --- a/internal/rtsp/rtsp.go +++ b/internal/rtsp/rtsp.go @@ -8,7 +8,6 @@ import ( "github.com/AlexxIT/go2rtc/internal/app" "github.com/AlexxIT/go2rtc/internal/streams" - "github.com/AlexxIT/go2rtc/pkg/aac" "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/rtsp" "github.com/AlexxIT/go2rtc/pkg/tcp" @@ -186,11 +185,11 @@ func tcpHandler(conn *rtsp.Conn) { } } - if query.Get("backchannel") == "1" { - conn.Medias = append(conn.Medias, &core.Media{ - Kind: core.KindAudio, - Direction: core.DirectionRecvonly, - Codecs: []*core.Codec{ + if query.Get("backchannel") == "1" { + conn.Medias = append(conn.Medias, &core.Media{ + Kind: core.KindAudio, + Direction: core.DirectionRecvonly, + Codecs: []*core.Codec{ {Name: core.CodecOpus, ClockRate: 48000, Channels: 2}, {Name: core.CodecPCM, ClockRate: 16000}, {Name: core.CodecPCMA, ClockRate: 16000}, @@ -198,10 +197,9 @@ func tcpHandler(conn *rtsp.Conn) { {Name: core.CodecPCM, ClockRate: 8000}, {Name: core.CodecPCMA, ClockRate: 8000}, {Name: core.CodecPCMU, ClockRate: 8000}, - {Name: core.CodecAAC, ClockRate: 16000, FmtpLine: aac.FMTP + "1408"}, - }, - }) - } + }, + }) + } if s := query.Get("pkt_size"); s != "" { conn.PacketSize = uint16(core.Atoi(s)) diff --git a/pkg/rtsp/producer.go b/pkg/rtsp/producer.go index 323d9197..3d818b62 100644 --- a/pkg/rtsp/producer.go +++ b/pkg/rtsp/producer.go @@ -16,43 +16,38 @@ func (c *Conn) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, e } } - switch c.mode { - case core.ModeActiveProducer: - c.stateMu.Lock() - defer c.stateMu.Unlock() + c.stateMu.Lock() + defer c.stateMu.Unlock() + var channel byte + + switch c.mode { + case core.ModeActiveProducer: if c.state == StatePlay { if err := c.Reconnect(); err != nil { return nil, err } } - channel, err := c.SetupMedia(media) + var err error + channel, err = c.SetupMedia(media) if err != nil { return nil, err } c.state = StateSetup + case core.ModePassiveConsumer: + // Backchannel + channel = byte(len(c.Senders)) * 2 + default: + return nil, errors.New("rtsp: wrong mode for GetTrack") + } - track := core.NewReceiver(media, codec) - track.ID = channel - c.Receivers = append(c.Receivers, track) + track := core.NewReceiver(media, codec) + track.ID = channel + c.Receivers = append(c.Receivers, track) - return track, nil - case core.ModePassiveConsumer: - // Backchannel - c.stateMu.Lock() - defer c.stateMu.Unlock() - - channel := byte(len(c.Senders)) * 2 - track := core.NewReceiver(media, codec) - track.ID = channel - c.Receivers = append(c.Receivers, track) - - return track, nil - default: - return nil, errors.New("rtsp: wrong mode for GetTrack") - } + return track, nil } func (c *Conn) Start() (err error) { diff --git a/pkg/rtsp/server.go b/pkg/rtsp/server.go index df2ebdb5..f4aea614 100644 --- a/pkg/rtsp/server.go +++ b/pkg/rtsp/server.go @@ -164,20 +164,14 @@ func (c *Conn) Accept() error { c.state = StateSetup if c.mode == core.ModePassiveConsumer { - trackID := reqTrackID(req) - - if trackID >= 0 { - if trackID < len(c.Senders) { - c.Senders[trackID].Media.ID = MethodSetup - tr = fmt.Sprintf("%d-%d", trackID*2, trackID*2+1) - res.Header.Set("Transport", transport+tr) - } else if trackID >= len(c.Senders) && trackID < len(c.Senders)+len(c.Receivers) { - c.Receivers[trackID-len(c.Senders)].Media.ID = MethodSetup - tr = fmt.Sprintf("%d-%d", trackID*2, trackID*2+1) - res.Header.Set("Transport", transport+tr) + if i := reqTrackID(req); i >= 0 && i < len(c.Senders)+len(c.Receivers) { + if i < len(c.Senders) { + c.Senders[i].Media.ID = MethodSetup } else { - res.Status = "400 Bad Request" + c.Receivers[i-len(c.Senders)].Media.ID = MethodSetup } + tr = fmt.Sprintf("%d-%d", i*2, i*2+1) + res.Header.Set("Transport", transport+tr) } else { res.Status = "400 Bad Request" } From 1abb3c8c22ab20fd483c8628ccedb938f4ff7c73 Mon Sep 17 00:00:00 2001 From: Alex X Date: Sat, 22 Feb 2025 11:39:32 +0300 Subject: [PATCH 163/166] Code refactoring for Nest RTSP source --- pkg/nest/api.go | 18 ++++++------------ pkg/nest/client.go | 19 +++++-------------- 2 files changed, 11 insertions(+), 26 deletions(-) diff --git a/pkg/nest/api.go b/pkg/nest/api.go index 9e32cbcf..4ca1e8b8 100644 --- a/pkg/nest/api.go +++ b/pkg/nest/api.go @@ -120,21 +120,11 @@ func (a *API) GetDevices(projectID string) ([]DeviceInfo, error) { devices := make([]DeviceInfo, 0, len(resv.Devices)) for _, device := range resv.Devices { + // only RTSP and WEB_RTC available (both supported) if len(device.Traits.SdmDevicesTraitsCameraLiveStream.SupportedProtocols) == 0 { continue } - supported := false - for _, protocol := range device.Traits.SdmDevicesTraitsCameraLiveStream.SupportedProtocols { - if protocol == "WEB_RTC" || protocol == "RTSP" { - supported = true - break - } - } - if !supported { - continue - } - i := strings.LastIndexByte(device.Name, '/') if i <= 0 { continue @@ -146,7 +136,11 @@ func (a *API) GetDevices(projectID string) ([]DeviceInfo, error) { name = device.ParentRelations[0].DisplayName } - devices = append(devices, DeviceInfo{Name: name, DeviceID: device.Name[i+1:], Protocols: device.Traits.SdmDevicesTraitsCameraLiveStream.SupportedProtocols}) + devices = append(devices, DeviceInfo{ + Name: name, + DeviceID: device.Name[i+1:], + Protocols: device.Traits.SdmDevicesTraitsCameraLiveStream.SupportedProtocols, + }) } return devices, nil diff --git a/pkg/nest/client.go b/pkg/nest/client.go index e692359c..93c4ce64 100644 --- a/pkg/nest/client.go +++ b/pkg/nest/client.go @@ -33,12 +33,6 @@ func Dial(rawURL string) (core.Producer, error) { refreshToken := query.Get("refresh_token") projectID := query.Get("project_id") deviceID := query.Get("device_id") - protocols := strings.Split(query.Get("protocols"), ",") - - // Default to WEB_RTC for backwards compataiility - if len(protocols) == 0 { - protocols = append(protocols, "WEB_RTC") - } if cliendID == "" || cliendSecret == "" || refreshToken == "" || projectID == "" || deviceID == "" { return nil, errors.New("nest: wrong query") @@ -49,16 +43,13 @@ func Dial(rawURL string) (core.Producer, error) { return nil, err } - // Pick the first supported protocol in order of priority (WEB_RTC, RTSP) - for _, proto := range protocols { - if proto == "WEB_RTC" { - return rtcConn(nestAPI, rawURL, projectID, deviceID) - } else if proto == "RTSP" { - return rtspConn(nestAPI, rawURL, projectID, deviceID) - } + protocols := strings.Split(query.Get("protocols"), ",") + if len(protocols) > 0 && protocols[0] == "RTSP" { + return rtspConn(nestAPI, rawURL, projectID, deviceID) } - return nil, errors.New("nest: unsupported camera") + // Default to WEB_RTC for backwards compataiility + return rtcConn(nestAPI, rawURL, projectID, deviceID) } func (c *WebRTCClient) GetMedias() []*core.Media { From 6fb59949a24ddd782dbbdebcdc2381a0105fc96e Mon Sep 17 00:00:00 2001 From: Alex X Date: Sun, 23 Feb 2025 20:56:48 +0300 Subject: [PATCH 164/166] Rewrite exec handler --- internal/exec/closer.go | 39 ------------------------ internal/exec/exec.go | 60 +++++++++++++++++++++++-------------- pkg/shell/command.go | 59 ++++++++++++++++++++++++++++++++++++ pkg/shell/procattr.go | 7 +++++ pkg/shell/procattr_linux.go | 6 ++++ pkg/stdin/backchannel.go | 6 +--- pkg/stdin/client.go | 7 ++--- 7 files changed, 113 insertions(+), 71 deletions(-) delete mode 100644 internal/exec/closer.go create mode 100644 pkg/shell/command.go create mode 100644 pkg/shell/procattr.go create mode 100644 pkg/shell/procattr_linux.go diff --git a/internal/exec/closer.go b/internal/exec/closer.go deleted file mode 100644 index 66d0e3ac..00000000 --- a/internal/exec/closer.go +++ /dev/null @@ -1,39 +0,0 @@ -package exec - -import ( - "errors" - "net/url" - "os" - "os/exec" - "syscall" - "time" - - "github.com/AlexxIT/go2rtc/pkg/core" -) - -// closer support custom killsignal with custom killtimeout -type closer struct { - cmd *exec.Cmd - query url.Values -} - -func (c *closer) Close() (err error) { - sig := os.Kill - if s := c.query.Get("killsignal"); s != "" { - sig = syscall.Signal(core.Atoi(s)) - } - - log.Trace().Msgf("[exec] kill with signal=%d", sig) - err = c.cmd.Process.Signal(sig) - - if s := c.query.Get("killtimeout"); s != "" { - timeout := time.Duration(core.Atoi(s)) * time.Second - timer := time.AfterFunc(timeout, func() { - log.Trace().Msgf("[exec] kill after timeout=%s", s) - _ = c.cmd.Process.Kill() - }) - defer timer.Stop() // stop timer if Wait ends before timeout - } - - return errors.Join(err, c.cmd.Wait()) -} diff --git a/internal/exec/exec.go b/internal/exec/exec.go index bce166e8..89add393 100644 --- a/internal/exec/exec.go +++ b/internal/exec/exec.go @@ -9,9 +9,9 @@ import ( "io" "net/url" "os" - "os/exec" "strings" "sync" + "syscall" "time" "github.com/AlexxIT/go2rtc/internal/app" @@ -49,7 +49,7 @@ func Init() { log = app.GetLogger("exec") } -func execHandle(rawURL string) (core.Producer, error) { +func execHandle(rawURL string) (prod core.Producer, err error) { rawURL, rawQuery, _ := strings.Cut(rawURL, "#") query := streams.ParseQuery(rawQuery) @@ -67,39 +67,55 @@ func execHandle(rawURL string) (core.Producer, error) { rawURL = rawURL[:i] + "rtsp://127.0.0.1:" + rtsp.Port + path + rawURL[i+8:] } - args := shell.QuoteSplit(rawURL[5:]) // remove `exec:` - cmd := exec.Command(args[0], args[1:]...) + cmd := shell.NewCommand(rawURL[5:]) // remove `exec:` cmd.Stderr = &logWriter{ buf: make([]byte, 512), debug: log.Debug().Enabled(), } + if s := query.Get("killsignal"); s != "" { + sig := syscall.Signal(core.Atoi(s)) + cmd.Cancel = func() error { + log.Debug().Msgf("[exec] kill with signal=%d", sig) + return cmd.Process.Signal(sig) + } + } + + if s := query.Get("killtimeout"); s != "" { + cmd.WaitDelay = time.Duration(core.Atoi(s)) * time.Second + } + if query.Get("backchannel") == "1" { return stdin.NewClient(cmd) } - cl := &closer{cmd: cmd, query: query} - if path == "" { - return handlePipe(rawURL, cmd, cl) + prod, err = handlePipe(rawURL, cmd) + } else { + prod, err = handleRTSP(rawURL, cmd, path) } - return handleRTSP(rawURL, cmd, cl, path) + if err != nil { + _ = cmd.Close() + } + + return } -func handlePipe(source string, cmd *exec.Cmd, cl io.Closer) (core.Producer, error) { +func handlePipe(source string, cmd *shell.Command) (core.Producer, error) { stdout, err := cmd.StdoutPipe() if err != nil { return nil, err } - rc := struct { + rd := struct { io.Reader io.Closer }{ // add buffer for pipe reader to reduce syscall bufio.NewReaderSize(stdout, core.BufferSize), - cl, + // stop cmd on close pipe call + cmd, } log.Debug().Strs("args", cmd.Args).Msg("[exec] run pipe") @@ -110,9 +126,8 @@ func handlePipe(source string, cmd *exec.Cmd, cl io.Closer) (core.Producer, erro return nil, err } - prod, err := magic.Open(rc) + prod, err := magic.Open(rd) if err != nil { - _ = rc.Close() return nil, fmt.Errorf("exec/pipe: %w\n%s", err, cmd.Stderr) } @@ -126,7 +141,7 @@ func handlePipe(source string, cmd *exec.Cmd, cl io.Closer) (core.Producer, erro return prod, nil } -func handleRTSP(source string, cmd *exec.Cmd, cl io.Closer, path string) (core.Producer, error) { +func handleRTSP(source string, cmd *shell.Command, path string) (core.Producer, error) { if log.Trace().Enabled() { cmd.Stdout = os.Stdout } @@ -152,23 +167,22 @@ func handleRTSP(source string, cmd *exec.Cmd, cl io.Closer, path string) (core.P return nil, err } - done := make(chan error, 1) - go func() { - done <- cmd.Wait() - }() + timeout := time.NewTimer(30 * time.Second) + defer timeout.Stop() select { - case <-time.After(time.Minute): + case <-timeout.C: + // haven't received data from app in timeout log.Error().Str("source", source).Msg("[exec] timeout") - _ = cl.Close() return nil, errors.New("exec: timeout") - case <-done: - // limit message size + case <-cmd.Done(): + // app fail before we receive any data return nil, fmt.Errorf("exec/rtsp\n%s", cmd.Stderr) case prod := <-waiter: + // app started successfully log.Debug().Stringer("launch", time.Since(ts)).Msg("[exec] run rtsp") setRemoteInfo(prod, source, cmd.Args) - prod.OnClose = cl.Close + prod.OnClose = cmd.Close return prod, nil } } diff --git a/pkg/shell/command.go b/pkg/shell/command.go new file mode 100644 index 00000000..b7c81899 --- /dev/null +++ b/pkg/shell/command.go @@ -0,0 +1,59 @@ +package shell + +import ( + "context" + "os/exec" +) + +// Command like exec.Cmd, but with support: +// - io.Closer interface +// - Wait from multiple places +// - Done channel +type Command struct { + *exec.Cmd + ctx context.Context + cancel context.CancelFunc + err error +} + +func NewCommand(s string) *Command { + ctx, cancel := context.WithCancel(context.Background()) + args := QuoteSplit(s) + cmd := exec.CommandContext(ctx, args[0], args[1:]...) + cmd.SysProcAttr = procAttr + return &Command{cmd, ctx, cancel, nil} +} + +func (c *Command) Start() error { + if err := c.Cmd.Start(); err != nil { + return err + } + + go func() { + c.err = c.Cmd.Wait() + c.cancel() // release context resources + }() + + return nil +} + +func (c *Command) Wait() error { + <-c.ctx.Done() + return c.err +} + +func (c *Command) Run() error { + if err := c.Start(); err != nil { + return err + } + return c.Wait() +} + +func (c *Command) Done() <-chan struct{} { + return c.ctx.Done() +} + +func (c *Command) Close() error { + c.cancel() + return nil +} diff --git a/pkg/shell/procattr.go b/pkg/shell/procattr.go new file mode 100644 index 00000000..fffdc2a4 --- /dev/null +++ b/pkg/shell/procattr.go @@ -0,0 +1,7 @@ +//go:build !linux + +package shell + +import "syscall" + +var procAttr *syscall.SysProcAttr diff --git a/pkg/shell/procattr_linux.go b/pkg/shell/procattr_linux.go new file mode 100644 index 00000000..cef1d152 --- /dev/null +++ b/pkg/shell/procattr_linux.go @@ -0,0 +1,6 @@ +package shell + +import "syscall" + +// will stop child if parent died (even with SIGKILL) +var procAttr = &syscall.SysProcAttr{Pdeathsig: syscall.SIGTERM} diff --git a/pkg/stdin/backchannel.go b/pkg/stdin/backchannel.go index b9a4a6d4..b154a291 100644 --- a/pkg/stdin/backchannel.go +++ b/pkg/stdin/backchannel.go @@ -2,7 +2,6 @@ package stdin import ( "encoding/json" - "errors" "github.com/AlexxIT/go2rtc/pkg/core" "github.com/pion/rtp" @@ -42,10 +41,7 @@ func (c *Client) Stop() (err error) { if c.sender != nil { c.sender.Close() } - if c.cmd.Process == nil { - return nil - } - return errors.Join(c.cmd.Process.Kill(), c.cmd.Wait()) + return c.cmd.Close() } func (c *Client) MarshalJSON() ([]byte, error) { diff --git a/pkg/stdin/client.go b/pkg/stdin/client.go index 09e525ad..a77d4459 100644 --- a/pkg/stdin/client.go +++ b/pkg/stdin/client.go @@ -1,21 +1,20 @@ package stdin import ( - "os/exec" - "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/AlexxIT/go2rtc/pkg/shell" ) // Deprecated: should be rewritten to core.Connection type Client struct { - cmd *exec.Cmd + cmd *shell.Command medias []*core.Media sender *core.Sender send int } -func NewClient(cmd *exec.Cmd) (*Client, error) { +func NewClient(cmd *shell.Command) (*Client, error) { c := &Client{ cmd: cmd, medias: []*core.Media{ From b881c52118b46bc9c1503a21a2beb9e08cf6405c Mon Sep 17 00:00:00 2001 From: Alex X Date: Mon, 24 Feb 2025 12:44:09 +0300 Subject: [PATCH 165/166] Code refactoring for FreeBSD binaries --- .gitignore | 1 + README.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 04ae894a..52fe9c86 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ go2rtc.yaml go2rtc.json +go2rtc_freebsd* go2rtc_linux* go2rtc_mac* go2rtc_win* diff --git a/README.md b/README.md index 21e760a6..926b046b 100644 --- a/README.md +++ b/README.md @@ -126,7 +126,7 @@ Download binary for your OS from [latest release](https://github.com/AlexxIT/go2 - `go2rtc_linux_mipsel` - Linux MIPS (ex. [Xiaomi Gateway 3](https://github.com/AlexxIT/XiaomiGateway3), [Wyze cameras](https://github.com/gtxaspec/wz_mini_hacks)) - `go2rtc_mac_amd64.zip` - macOS 10.13+ Intel 64-bit - `go2rtc_mac_arm64.zip` - macOS ARM 64-bit -- `go2rtc_freebsd_amd64.zip` - FreeBSD Intel 64-bit +- `go2rtc_freebsd_amd64.zip` - FreeBSD 64-bit - `go2rtc_freebsd_arm64.zip` - FreeBSD ARM 64-bit Don't forget to fix the rights `chmod +x go2rtc_xxx_xxx` on Linux and Mac. From 4b4a1644ff06566ebb4c67492bba6dee8c5c2446 Mon Sep 17 00:00:00 2001 From: Alex X Date: Mon, 24 Feb 2025 16:21:12 +0300 Subject: [PATCH 166/166] Code refactoring for network view --- www/network.html | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/www/network.html b/www/network.html index 180c9711..79875012 100644 --- a/www/network.html +++ b/www/network.html @@ -58,18 +58,16 @@ const viewPosition = network.getViewPosition(); const scale = network.getScale(); const selectedNodes = network.getSelectedNodes(); - const selectedEdges = network.getSelectedEdges(); network.setData(data); - network.selectNodes(selectedNodes); - network.selectEdges(selectedEdges); - for (const nodeId in positions) { network.moveNode(nodeId, positions[nodeId].x, positions[nodeId].y); } network.moveTo({position: viewPosition, scale: scale}); + + network.selectNodes(selectedNodes); } } catch (error) { console.error('Error fetching or updating network data:', error);