diff --git a/internal/gopro/gopro.go b/internal/gopro/gopro.go new file mode 100644 index 00000000..55d2641b --- /dev/null +++ b/internal/gopro/gopro.go @@ -0,0 +1,30 @@ +package gopro + +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/gopro" +) + +func Init() { + streams.HandleFunc("gopro", handleGoPro) + + 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 + + for _, host := range gopro.Discovery() { + items = append(items, &api.Source{Name: host, URL: "gopro://" + host}) + } + + api.ResponseSources(w, items) +} diff --git a/main.go b/main.go index 7467135d..0e292139 100644 --- a/main.go +++ b/main.go @@ -11,6 +11,7 @@ import ( "github.com/AlexxIT/go2rtc/internal/exec" "github.com/AlexxIT/go2rtc/internal/expr" "github.com/AlexxIT/go2rtc/internal/ffmpeg" + "github.com/AlexxIT/go2rtc/internal/gopro" "github.com/AlexxIT/go2rtc/internal/hass" "github.com/AlexxIT/go2rtc/internal/hls" "github.com/AlexxIT/go2rtc/internal/homekit" @@ -78,6 +79,7 @@ func main() { nest.Init() // nest source bubble.Init() // bubble source expr.Init() // expr source + gopro.Init() // gopro source // 6. Helper modules diff --git a/pkg/gopro/discovery.go b/pkg/gopro/discovery.go new file mode 100644 index 00000000..19ed8023 --- /dev/null +++ b/pkg/gopro/discovery.go @@ -0,0 +1,43 @@ +package gopro + +import ( + "net" + "net/http" + "regexp" +) + +func Discovery() (urls []string) { + ints, err := net.Interfaces() + if err != nil { + return nil + } + + // The socket address for USB connections is 172.2X.1YZ.51:8080 + // https://gopro.github.io/OpenGoPro/http_2_0#socket-address + re := regexp.MustCompile(`^172\.2\d\.1\d\d\.`) + + for _, itf := range ints { + addrs, err := itf.Addrs() + if err != nil { + continue + } + + for _, addr := range addrs { + host := addr.String() + if !re.MatchString(host) { + continue + } + + host = host[:11] + "51" // 172.2x.1xx.xxx + res, err := http.Get("http://" + host + ":8080/gopro/webcam/status") + if err != nil { + continue + } + _ = res.Body.Close() + + urls = append(urls, host) + } + } + + return +} diff --git a/pkg/gopro/gopro.go b/pkg/gopro/gopro.go new file mode 100644 index 00000000..2d6a098b --- /dev/null +++ b/pkg/gopro/gopro.go @@ -0,0 +1,117 @@ +package gopro + +import ( + "errors" + "io" + "net" + "net/http" + "net/url" + "time" + + "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/AlexxIT/go2rtc/pkg/mpegts" +) + +func Dial(rawURL string) (core.Producer, error) { + u, err := url.Parse(rawURL) + if err != nil { + return nil, err + } + + r := &listener{host: u.Host} + + if err = r.command("/gopro/webcam/stop"); err != nil { + return nil, err + } + + if err = r.listen(); err != nil { + return nil, err + } + + if err = r.command("/gopro/webcam/start"); err != nil { + return nil, err + } + + return mpegts.Open(r) +} + +type listener struct { + conn net.PacketConn + host string + packet []byte + packets chan []byte +} + +func (r *listener) Read(p []byte) (n int, err error) { + if r.packet == nil { + var ok bool + if r.packet, ok = <-r.packets; !ok { + return 0, io.EOF // channel closed + } + } + + n = copy(p, r.packet) + + if n < len(r.packet) { + r.packet = r.packet[n:] + } else { + r.packet = nil + } + + return +} + +func (r *listener) Close() error { + return r.conn.Close() +} + +func (r *listener) command(api string) error { + client := &http.Client{Timeout: 5 * time.Second} + + res, err := client.Get("http://" + r.host + ":8080" + api) + if err != nil { + return err + } + + _ = res.Body.Close() + + if res.StatusCode != http.StatusOK { + return errors.New("gopro: wrong response: " + res.Status) + } + + return nil +} + +func (r *listener) listen() (err error) { + if r.conn, err = net.ListenPacket("udp4", ":8554"); err != nil { + return + } + + r.packets = make(chan []byte, 1024) + go r.worker() + + return +} + +func (r *listener) worker() { + b := make([]byte, 1500) + for { + if err := r.conn.SetReadDeadline(time.Now().Add(3 * time.Second)); err != nil { + break + } + + n, _, err := r.conn.ReadFrom(b) + if err != nil { + break + } + + packet := make([]byte, n) + copy(packet, b) + + r.packets <- packet + } + + close(r.packets) + + _ = r.command("/gopro/webcam/stop") +} diff --git a/www/add.html b/www/add.html index bcb9e1c7..2b1bb9d7 100644 --- a/www/add.html +++ b/www/add.html @@ -246,6 +246,18 @@ + +