From 7d3fbf2ee0a0dbc9996efc1334d342d36d2a6658 Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Sat, 15 Apr 2023 07:33:22 +0300 Subject: [PATCH 01/80] Add trace logs for media matching --- cmd/streams/stream.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/cmd/streams/stream.go b/cmd/streams/stream.go index 3f60ad37..52997a24 100644 --- a/cmd/streams/stream.go +++ b/cmd/streams/stream.go @@ -50,7 +50,7 @@ func (s *Stream) SetSource(source string) { func (s *Stream) AddConsumer(cons core.Consumer) (err error) { // support for multiple simultaneous requests from different consumers - atomic.AddInt32(&s.requests, 1) + consN := atomic.AddInt32(&s.requests, 1) var producers []*Producer // matched producers for consumer @@ -58,15 +58,19 @@ func (s *Stream) AddConsumer(cons core.Consumer) (err error) { // Step 1. Get consumer medias for _, consMedia := range cons.GetMedias() { + log.Trace().Msgf("[streams] check cons=%d media=%s", consN, consMedia) producers: - for _, prod := range s.producers { + for prodN, prod := range s.producers { if err = prod.Dial(); err != nil { + log.Trace().Err(err).Msgf("[streams] skip prod=%s", prod.url) continue } // Step 2. Get producer medias (not tracks yet) for _, prodMedia := range prod.GetMedias() { + log.Trace().Msgf("[streams] check prod=%d media=%s", prodN, prodMedia) + collectCodecs(prodMedia, &codecs) // Step 3. Match consumer/producer codecs list @@ -79,6 +83,8 @@ func (s *Stream) AddConsumer(cons core.Consumer) (err error) { switch prodMedia.Direction { case core.DirectionRecvonly: + log.Trace().Msgf("[streams] match prod=%d => cons=%d", prodN, consN) + // Step 4. Get recvonly track from producer if track, err = prod.GetTrack(prodMedia, prodCodec); err != nil { log.Info().Err(err).Msg("[streams] can't get track") @@ -91,6 +97,8 @@ func (s *Stream) AddConsumer(cons core.Consumer) (err error) { } case core.DirectionSendonly: + log.Trace().Msgf("[streams] match cons=%d => prod=%d", consN, prodN) + // Step 4. Get recvonly track from consumer (backchannel) if track, err = cons.(core.Producer).GetTrack(consMedia, consCodec); err != nil { log.Info().Err(err).Msg("[streams] can't get track") From d633d331bbe87b13c9223a580ffaf1e4d0e53025 Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Sat, 15 Apr 2023 07:34:38 +0300 Subject: [PATCH 02/80] Fix new stream from camera entity from Hass --- cmd/hass/api.go | 61 ++------------------------------------ cmd/streams/init.go | 24 ++++++++++++--- cmd/streams/stream.go | 2 -- cmd/streams/stream_test.go | 19 ++++++++++++ 4 files changed, 41 insertions(+), 65 deletions(-) create mode 100644 cmd/streams/stream_test.go diff --git a/cmd/hass/api.go b/cmd/hass/api.go index 54a20c94..f5ff5572 100644 --- a/cmd/hass/api.go +++ b/cmd/hass/api.go @@ -8,7 +8,6 @@ import ( "github.com/AlexxIT/go2rtc/cmd/webrtc" "net" "net/http" - "net/url" "strings" ) @@ -25,6 +24,7 @@ func initAPI() { api.HandleFunc("/streams", ok) + // api from RTSPtoWeb api.HandleFunc("/stream/", func(w http.ResponseWriter, r *http.Request) { switch { // /stream/{id}/add @@ -40,13 +40,7 @@ func initAPI() { // 3. dynamic link to Hass camera stream := streams.Get(v.Name) if stream == nil { - // check if it is rtsp link to go2rtc - stream = rtspStream(v.Channels.First.Url) - if stream != nil { - streams.New(v.Name, stream) - } else { - stream = streams.New(v.Name, "{input}") - } + stream = streams.NewTemplate(v.Name, v.Channels.First.Url) } stream.SetSource(v.Channels.First.Url) @@ -90,48 +84,6 @@ func initAPI() { _, _ = w.Write([]byte(s)) } }) - - // api from RTSPtoWebRTC - api.HandleFunc("/stream", func(w http.ResponseWriter, r *http.Request) { - if err := r.ParseForm(); err != nil { - return - } - - str := r.FormValue("sdp64") - offer, err := base64.StdEncoding.DecodeString(str) - if err != nil { - return - } - - src := r.FormValue("url") - src, err = url.QueryUnescape(src) - if err != nil { - return - } - - stream := streams.Get(src) - if stream == nil { - if stream = rtspStream(src); stream != nil { - streams.New(src, stream) - } else { - stream = streams.New(src, src) - } - } - - str, err = webrtc.ExchangeSDP(stream, string(offer), "WebRTC/Hass sync", r.UserAgent()) - if err != nil { - return - } - - v := struct { - Answer string `json:"sdp64"` - }{ - Answer: base64.StdEncoding.EncodeToString([]byte(str)), - } - - w.Header().Set("Content-Type", "application/json") - _ = json.NewEncoder(w).Encode(v) - }) } func HassioAddr() string { @@ -153,15 +105,6 @@ func HassioAddr() string { return "" } -func rtspStream(url string) *streams.Stream { - if strings.HasPrefix(url, "rtsp://") { - if i := strings.IndexByte(url[7:], '/'); i > 0 { - return streams.Get(url[8+i:]) - } - } - return nil -} - type addJSON struct { Name string `json:"name"` Channels struct { diff --git a/cmd/streams/init.go b/cmd/streams/init.go index 8c5a454a..ca5ff58e 100644 --- a/cmd/streams/init.go +++ b/cmd/streams/init.go @@ -7,6 +7,7 @@ import ( "github.com/AlexxIT/go2rtc/cmd/app/store" "github.com/rs/zerolog" "net/http" + "net/url" ) func Init() { @@ -39,6 +40,20 @@ func New(name string, source any) *Stream { return stream } +func NewTemplate(name string, source any) *Stream { + // check if source links to some stream name from go2rtc + if rawURL, ok := source.(string); ok { + if u, err := url.Parse(rawURL); err == nil && u.Scheme == "rtsp" { + if stream, ok := streams[u.Path[1:]]; ok { + streams[name] = stream + return stream + } + } + } + + return New(name, "{input}") +} + func GetOrNew(src string) *Stream { if stream, ok := streams[src]; ok { return stream @@ -85,11 +100,12 @@ func streamsHandler(w http.ResponseWriter, r *http.Request) { return } - if stream := Get(name); stream != nil { - stream.SetSource(src) - } else { - New(name, src) + // support {input} templates: https://github.com/AlexxIT/go2rtc#module-hass + stream := Get(name) + if stream == nil { + stream = NewTemplate(name, src) } + stream.SetSource(src) case "POST": // with dst - redirect source to dst diff --git a/cmd/streams/stream.go b/cmd/streams/stream.go index 52997a24..2300b53e 100644 --- a/cmd/streams/stream.go +++ b/cmd/streams/stream.go @@ -31,8 +31,6 @@ func NewStream(source any) *Stream { s.producers = append(s.producers, prod) } return s - case *Stream: - return source case map[string]any: return NewStream(source["url"]) case nil: diff --git a/cmd/streams/stream_test.go b/cmd/streams/stream_test.go new file mode 100644 index 00000000..86dc92c2 --- /dev/null +++ b/cmd/streams/stream_test.go @@ -0,0 +1,19 @@ +package streams + +import ( + "github.com/stretchr/testify/require" + "testing" +) + +func TestTemplate(t *testing.T) { + source1 := "does not matter" + + stream1 := New("from_yaml", source1) + require.Len(t, streams, 1) + + stream2 := NewTemplate("camera.from_hass", "rtsp://localhost:8554/from_yaml?video") + + require.Equal(t, stream1, stream2) + require.Equal(t, stream2.producers[0].url, source1) + require.Len(t, streams, 2) +} From 8dd9991268c387ef216d5c14bed1c06c228162c5 Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Sat, 15 Apr 2023 07:53:26 +0300 Subject: [PATCH 03/80] Fix mutex lock after #339 --- pkg/rtsp/client.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/rtsp/client.go b/pkg/rtsp/client.go index 29757046..f040139b 100644 --- a/pkg/rtsp/client.go +++ b/pkg/rtsp/client.go @@ -310,7 +310,8 @@ func (c *Conn) SetupMedia(media *core.Media, first bool) (byte, error) { // some Dahua/Amcrest cameras fail here because two simultaneous // backchannel connections if c.Backchannel { - c.Close() + _ = c.conn.Close() + c.Backchannel = false if err := c.Dial(); err != nil { return 0, err From 25dc3664fdbe2e444c0fa7ac19139e674e422998 Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Sat, 15 Apr 2023 12:50:50 +0300 Subject: [PATCH 04/80] Set random session for RTSP server --- 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 e32009bf..d2be609c 100644 --- a/pkg/rtsp/server.go +++ b/pkg/rtsp/server.go @@ -136,7 +136,7 @@ func (c *Conn) Accept() error { const transport = "RTP/AVP/TCP;unicast;interleaved=" if strings.HasPrefix(tr, transport) { - c.Session = "1" // TODO: fixme + c.Session = core.RandString(8, 10) c.state = StateSetup res.Header.Set("Transport", tr[:len(transport)+3]) } else { From 553f5ff0d888471b55baf2fd6030d41cbc93a6bf Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Sat, 15 Apr 2023 12:51:15 +0300 Subject: [PATCH 05/80] Add timeout to RTSP client requests --- pkg/rtsp/client.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pkg/rtsp/client.go b/pkg/rtsp/client.go index f040139b..d790e9bb 100644 --- a/pkg/rtsp/client.go +++ b/pkg/rtsp/client.go @@ -16,6 +16,8 @@ import ( "github.com/AlexxIT/go2rtc/pkg/tcp" ) +var Timeout = time.Second * 5 + func NewClient(uri string) *Conn { return &Conn{uri: uri} } @@ -93,6 +95,11 @@ func (c *Conn) Request(req *tcp.Request) error { // Do send Request and receive and process Response func (c *Conn) Do(req *tcp.Request) (*tcp.Response, error) { + // https://blog.cloudflare.com/the-complete-guide-to-golang-net-http-timeouts/ + if err := c.conn.SetDeadline(time.Now().Add(Timeout)); err != nil { + return nil, err + } + if err := c.Request(req); err != nil { return nil, err } From 4b4deaaaf25e4c9fd147330ac1f37e518ff822e0 Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Sat, 15 Apr 2023 12:52:52 +0300 Subject: [PATCH 06/80] Fix missed control in SDP --- pkg/core/media.go | 7 +++ pkg/rtsp/client.go | 8 ++-- pkg/rtsp/client_test.go | 94 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 105 insertions(+), 4 deletions(-) create mode 100644 pkg/rtsp/client_test.go diff --git a/pkg/core/media.go b/pkg/core/media.go index 1e972350..697e9806 100644 --- a/pkg/core/media.go +++ b/pkg/core/media.go @@ -82,6 +82,13 @@ func (m *Media) MatchAll() bool { return false } +func (m *Media) Equal(media *Media) bool { + if media.ID != "" { + return m.ID == media.ID + } + return m.String() == media.String() +} + func GetKind(name string) string { switch name { case CodecH264, CodecH265, CodecVP8, CodecVP9, CodecAV1, CodecJPEG: diff --git a/pkg/rtsp/client.go b/pkg/rtsp/client.go index d790e9bb..304203ae 100644 --- a/pkg/rtsp/client.go +++ b/pkg/rtsp/client.go @@ -276,7 +276,7 @@ func (c *Conn) SetupMedia(media *core.Media, first bool) (byte, error) { // try to use media position as channel number for i, m := range c.Medias { - if m.ID == media.ID { + if m.Equal(media) { transport = fmt.Sprintf( // i - RTP (data channel) // i+1 - RTCP (control channel) @@ -327,9 +327,9 @@ func (c *Conn) SetupMedia(media *core.Media, first bool) (byte, error) { return 0, err } - for _, newMedia := range c.Medias { - if newMedia.ID == media.ID { - return c.SetupMedia(newMedia, false) + for _, m := range c.Medias { + if m.Equal(media) { + return c.SetupMedia(m, false) } } } diff --git a/pkg/rtsp/client_test.go b/pkg/rtsp/client_test.go new file mode 100644 index 00000000..11bb3049 --- /dev/null +++ b/pkg/rtsp/client_test.go @@ -0,0 +1,94 @@ +package rtsp + +import ( + "github.com/stretchr/testify/require" + "net" + "os" + "testing" + "time" +) + +func TestTimeout(t *testing.T) { + Timeout = time.Millisecond + + ln, err := net.Listen("tcp", "localhost:0") + require.Nil(t, err) + + client := NewClient("rtsp://" + ln.Addr().String() + "/stream") + client.Backchannel = true + + err = client.Dial() + require.Nil(t, err) + + err = client.Describe() + require.ErrorIs(t, err, os.ErrDeadlineExceeded) +} + +func TestMissedControl(t *testing.T) { + Timeout = time.Millisecond + + ln, err := net.Listen("tcp", "localhost:0") + require.Nil(t, err) + + go func() { + conn, err := ln.Accept() + require.Nil(t, err) + + b := make([]byte, 8192) + for { + n, err := conn.Read(b) + require.Nil(t, err) + + req := string(b[:n]) + + switch req[:4] { + case "DESC": + _, _ = conn.Write([]byte(`RTSP/1.0 200 OK +Cseq: 1 +Content-Length: 495 +Content-Type: application/sdp + +v=0 +o=- 1 1 IN IP4 0.0.0.0 +s=go2rtc/1.2.0 +c=IN IP4 0.0.0.0 +t=0 0 +m=audio 0 RTP/AVP 96 +a=rtpmap:96 MPEG4-GENERIC/48000/2 +a=fmtp:96 profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3; config=119056E500 +m=audio 0 RTP/AVP 97 +a=rtpmap:97 OPUS/48000/2 +a=fmtp:97 sprop-stereo=1 +m=video 0 RTP/AVP 98 +a=rtpmap:98 H264/90000 +a=fmtp:98 packetization-mode=1; sprop-parameter-sets=Z2QAKaw0yAeAIn5cBagICAoAAAfQAAE4gdDAAjhAACOEF3lxoYAEcIAARwgu8uFA,aO48MAA=; profile-level-id=640029 +`)) + + case "SETU": + _, _ = conn.Write([]byte(`RTSP/1.0 200 OK +Transport: RTP/AVP/TCP;unicast;interleaved=4-5 +Cseq: 3 +Session: 1 + +`)) + + default: + t.Fail() + } + } + }() + + client := NewClient("rtsp://" + ln.Addr().String() + "/stream") + client.Backchannel = true + + err = client.Dial() + require.Nil(t, err) + + err = client.Describe() + require.Nil(t, err) + require.Len(t, client.Medias, 3) + + ch, err := client.SetupMedia(client.Medias[2], true) + require.Nil(t, err) + require.Equal(t, ch, byte(4)) +} From a5c4854aeb8ee5ec9c3d7f36bedc562dbefd8f41 Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Sun, 16 Apr 2023 13:57:16 +0300 Subject: [PATCH 07/80] Add reconnect logic to RTSP client --- pkg/rtsp/client.go | 163 +++++++------------------------------------ pkg/rtsp/conn.go | 128 ++++++++++++++++++++++++--------- pkg/rtsp/consumer.go | 11 ++- pkg/rtsp/producer.go | 97 +++++++++++++++++++------ pkg/rtsp/server.go | 20 +++--- 5 files changed, 214 insertions(+), 205 deletions(-) diff --git a/pkg/rtsp/client.go b/pkg/rtsp/client.go index 304203ae..4ed228f0 100644 --- a/pkg/rtsp/client.go +++ b/pkg/rtsp/client.go @@ -31,10 +31,6 @@ func (c *Conn) Dial() (err error) { c.URL.Host += ":554" } - // remove UserInfo from URL - c.auth = tcp.NewAuth(c.URL.User) - c.URL.User = nil - c.conn, err = net.DialTimeout("tcp", c.URL.Host, time.Second*5) if err != nil { return @@ -56,55 +52,24 @@ func (c *Conn) Dial() (err error) { c.conn = tlsConn } + // remove UserInfo from URL + c.auth = tcp.NewAuth(c.URL.User) + c.URL.User = nil + c.reader = bufio.NewReader(c.conn) + c.session = "" c.state = StateConn return nil } -// Request sends only Request -func (c *Conn) Request(req *tcp.Request) error { - if req.Proto == "" { - req.Proto = ProtoRTSP - } - - if req.Header == nil { - req.Header = make(map[string][]string) - } - - c.sequence++ - // important to send case sensitive CSeq - // https://github.com/AlexxIT/go2rtc/issues/7 - req.Header["CSeq"] = []string{strconv.Itoa(c.sequence)} - - c.auth.Write(req) - - if c.Session != "" { - req.Header.Set("Session", c.Session) - } - - if req.Body != nil { - val := strconv.Itoa(len(req.Body)) - req.Header.Set("Content-Length", val) - } - - c.Fire(req) - - return req.Write(c.conn) -} - -// Do send Request and receive and process Response +// Do send WriteRequest and receive and process WriteResponse func (c *Conn) Do(req *tcp.Request) (*tcp.Response, error) { - // https://blog.cloudflare.com/the-complete-guide-to-golang-net-http-timeouts/ - if err := c.conn.SetDeadline(time.Now().Add(Timeout)); err != nil { + if err := c.WriteRequest(req); err != nil { return nil, err } - if err := c.Request(req); err != nil { - return nil, err - } - - res, err := tcp.ReadResponse(c.reader) + res, err := c.ReadResponse() if err != nil { return nil, err } @@ -134,40 +99,6 @@ func (c *Conn) Do(req *tcp.Request) (*tcp.Response, error) { return res, nil } -func (c *Conn) Response(res *tcp.Response) error { - if res.Proto == "" { - res.Proto = ProtoRTSP - } - - if res.Status == "" { - res.Status = "200 OK" - } - - if res.Header == nil { - res.Header = make(map[string][]string) - } - - if res.Request != nil && res.Request.Header != nil { - seq := res.Request.Header.Get("CSeq") - if seq != "" { - res.Header.Set("CSeq", seq) - } - } - - if c.Session != "" { - res.Header.Set("Session", c.Session) - } - - if res.Body != nil { - val := strconv.Itoa(len(res.Body)) - res.Header.Set("Content-Length", val) - } - - c.Fire(res) - - return res.Write(c.conn) -} - func (c *Conn) Options() error { req := &tcp.Request{Method: MethodOptions, URL: c.URL} @@ -219,11 +150,18 @@ func (c *Conn) Describe() error { } } - c.Medias, err = UnmarshalSDP(res.Body) + medias, err := UnmarshalSDP(res.Body) if err != nil { return err } + // TODO: rewrite more smart + if c.Medias == nil { + c.Medias = medias + } else if len(c.Medias) > len(medias) { + c.Medias = c.Medias[:len(medias)] + } + c.mode = core.ModeActiveProducer return nil @@ -250,28 +188,7 @@ func (c *Conn) Announce() (err error) { return } -func (c *Conn) Setup() error { - for _, media := range c.Medias { - _, err := c.SetupMedia(media, true) - if err != nil { - return err - } - } - - return nil -} - -func (c *Conn) SetupMedia(media *core.Media, first bool) (byte, error) { - // TODO: rewrite recoonection and first flag - if first { - c.stateMu.Lock() - defer c.stateMu.Unlock() - } - - if c.state != StateConn && c.state != StateSetup { - return 0, fmt.Errorf("RTSP SETUP from wrong state: %s", c.state) - } - +func (c *Conn) SetupMedia(media *core.Media) (byte, error) { var transport string // try to use media position as channel number @@ -311,39 +228,28 @@ func (c *Conn) SetupMedia(media *core.Media, first bool) (byte, error) { }, } - var res *tcp.Response - res, err = c.Do(req) + res, err := c.Do(req) if err != nil { // some Dahua/Amcrest cameras fail here because two simultaneous // backchannel connections if c.Backchannel { - _ = c.conn.Close() - c.Backchannel = false - if err := c.Dial(); err != nil { + if err = c.Reconnect(); err != nil { return 0, err } - if err := c.Describe(); err != nil { - return 0, err - } - - for _, m := range c.Medias { - if m.Equal(media) { - return c.SetupMedia(m, false) - } - } + return c.SetupMedia(media) } return 0, err } - if c.Session == "" { + if c.session == "" { // Session: 216525287999;timeout=60 if s := res.Header.Get("Session"); s != "" { if j := strings.IndexByte(s, ';'); j > 0 { s = s[:j] } - c.Session = s + c.session = s } } @@ -361,8 +267,6 @@ func (c *Conn) SetupMedia(media *core.Media, first bool) (byte, error) { } } - c.state = StateSetup - channel := core.Between(transport, "interleaved=", "-") i, err := strconv.Atoi(channel) if err != nil { @@ -373,36 +277,17 @@ func (c *Conn) SetupMedia(media *core.Media, first bool) (byte, error) { } func (c *Conn) Play() (err error) { - c.stateMu.Lock() - defer c.stateMu.Unlock() - - if c.state != StateSetup { - return fmt.Errorf("RTSP PLAY from wrong state: %s", c.state) - } - req := &tcp.Request{Method: MethodPlay, URL: c.URL} - if err = c.Request(req); err == nil { - c.state = StatePlay - } - - return + return c.WriteRequest(req) } func (c *Conn) Teardown() (err error) { // allow TEARDOWN from any state (ex. ANNOUNCE > SETUP) req := &tcp.Request{Method: MethodTeardown, URL: c.URL} - return c.Request(req) + return c.WriteRequest(req) } func (c *Conn) Close() error { - c.stateMu.Lock() - defer c.stateMu.Unlock() - - if c.state == StateNone { - return nil - } - _ = c.Teardown() - c.state = StateNone return c.conn.Close() } diff --git a/pkg/rtsp/conn.go b/pkg/rtsp/conn.go index 2a0add62..ebc127bc 100644 --- a/pkg/rtsp/conn.go +++ b/pkg/rtsp/conn.go @@ -25,7 +25,6 @@ type Conn struct { SessionName string Medias []*core.Media - Session string UserAgent string URL *url.URL @@ -34,12 +33,14 @@ type Conn struct { auth *tcp.Auth conn net.Conn mode core.Mode - state State - stateMu sync.Mutex reader *bufio.Reader sequence int + session string uri string + state State + stateMu sync.Mutex + receivers []*core.Receiver senders []*core.Sender @@ -68,13 +69,12 @@ func (s State) String() string { case StateNone: return "NONE" case StateConn: + return "CONN" case StateSetup: return "SETUP" case StatePlay: return "PLAY" - case StateHandle: - return "HANDLE" } return strconv.Itoa(int(s)) } @@ -84,31 +84,9 @@ const ( StateConn StateSetup StatePlay - StateHandle ) func (c *Conn) Handle() (err error) { - c.stateMu.Lock() - - switch c.state { - case StateNone: // Close after PLAY and before Handle is OK (because SETUP after PLAY) - case StatePlay: - c.state = StateHandle - default: - err = fmt.Errorf("RTSP HANDLE from wrong state: %s", c.state) - - c.state = StateNone - _ = c.conn.Close() - } - - ok := c.state == StateHandle - - c.stateMu.Unlock() - - if !ok { - return - } - var timeout time.Duration switch c.mode { @@ -158,7 +136,7 @@ func (c *Conn) Handle() (err error) { switch string(buf4) { case "RTSP": var res *tcp.Response - if res, err = tcp.ReadResponse(c.reader); err != nil { + if res, err = c.ReadResponse(); err != nil { return } c.Fire(res) @@ -166,13 +144,15 @@ func (c *Conn) Handle() (err error) { case "OPTI", "TEAR", "DESC", "SETU", "PLAY", "PAUS", "RECO", "ANNO", "GET_", "SET_": var req *tcp.Request - if req, err = tcp.ReadRequest(c.reader); err != nil { + if req, err = c.ReadRequest(); err != nil { return } c.Fire(req) continue default: + c.Fire("RTSP wrong input") + for i := 0; ; i++ { // search next start symbol if _, err = c.reader.ReadBytes('$'); err != nil { @@ -204,8 +184,6 @@ func (c *Conn) Handle() (err error) { return fmt.Errorf("RTSP wrong input") } } - - c.Fire("RTSP wrong input") } } else { // hope that the odd channels are always RTCP @@ -259,6 +237,92 @@ func (c *Conn) Handle() (err error) { return } +func (c *Conn) WriteRequest(req *tcp.Request) error { + if req.Proto == "" { + req.Proto = ProtoRTSP + } + + if req.Header == nil { + req.Header = make(map[string][]string) + } + + c.sequence++ + // important to send case sensitive CSeq + // https://github.com/AlexxIT/go2rtc/issues/7 + req.Header["CSeq"] = []string{strconv.Itoa(c.sequence)} + + c.auth.Write(req) + + if c.session != "" { + req.Header.Set("Session", c.session) + } + + if req.Body != nil { + val := strconv.Itoa(len(req.Body)) + req.Header.Set("Content-Length", val) + } + + c.Fire(req) + + if err := c.conn.SetWriteDeadline(time.Now().Add(Timeout)); err != nil { + return err + } + + return req.Write(c.conn) +} + +func (c *Conn) ReadRequest() (*tcp.Request, error) { + if err := c.conn.SetReadDeadline(time.Now().Add(Timeout)); err != nil { + return nil, err + } + return tcp.ReadRequest(c.reader) +} + +func (c *Conn) WriteResponse(res *tcp.Response) error { + if res.Proto == "" { + res.Proto = ProtoRTSP + } + + if res.Status == "" { + res.Status = "200 OK" + } + + if res.Header == nil { + res.Header = make(map[string][]string) + } + + if res.Request != nil && res.Request.Header != nil { + seq := res.Request.Header.Get("CSeq") + if seq != "" { + res.Header.Set("CSeq", seq) + } + } + + if c.session != "" { + res.Header.Set("Session", c.session) + } + + if res.Body != nil { + val := strconv.Itoa(len(res.Body)) + res.Header.Set("Content-Length", val) + } + + c.Fire(res) + + if err := c.conn.SetWriteDeadline(time.Now().Add(Timeout)); err != nil { + return err + } + + return res.Write(c.conn) +} + +func (c *Conn) ReadResponse() (*tcp.Response, error) { + if err := c.conn.SetReadDeadline(time.Now().Add(Timeout)); err != nil { + return nil, err + } + return tcp.ReadResponse(c.reader) +} + func (c *Conn) keepalive() { // TODO: rewrite to RTCP req := &tcp.Request{Method: MethodOptions, URL: c.URL} @@ -267,7 +331,7 @@ func (c *Conn) keepalive() { if c.state == StateNone { return } - if err := c.Request(req); err != nil { + if err := c.WriteRequest(req); err != nil { return } } diff --git a/pkg/rtsp/consumer.go b/pkg/rtsp/consumer.go index b0eaf7ce..7f7fece2 100644 --- a/pkg/rtsp/consumer.go +++ b/pkg/rtsp/consumer.go @@ -28,7 +28,16 @@ func (c *Conn) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiv switch c.mode { case core.ModeActiveProducer: // backchannel - if channel, err = c.SetupMedia(media, true); err != nil { + c.stateMu.Lock() + defer c.stateMu.Unlock() + + if c.state == StatePlay { + if err = c.Reconnect(); err != nil { + return + } + } + + if channel, err = c.SetupMedia(media); err != nil { return } diff --git a/pkg/rtsp/producer.go b/pkg/rtsp/producer.go index ea7aa3ea..f7772c54 100644 --- a/pkg/rtsp/producer.go +++ b/pkg/rtsp/producer.go @@ -2,7 +2,7 @@ package rtsp import ( "encoding/json" - "fmt" + "errors" "github.com/AlexxIT/go2rtc/pkg/core" ) @@ -15,51 +15,78 @@ func (c *Conn) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, e } } - switch c.state { - case StateConn, StateSetup: - default: - return nil, fmt.Errorf("RTSP GetTrack from wrong state: %s", c.state) + c.stateMu.Lock() + defer c.stateMu.Unlock() + + if c.state == StatePlay { + if err := c.Reconnect(); err != nil { + return nil, err + } } - channel, err := c.SetupMedia(media, true) + channel, err := c.SetupMedia(media) if err != nil { return nil, err } + c.state = StateSetup + track := core.NewReceiver(media, codec) - track.ID = byte(channel) + track.ID = channel c.receivers = append(c.receivers, track) return track, nil } -func (c *Conn) Start() error { - switch c.mode { - case core.ModeActiveProducer: - if err := c.Play(); err != nil { - return err +func (c *Conn) Start() (err error) { + core.Assert(c.mode == core.ModeActiveProducer || c.mode == core.ModePassiveProducer) + + for { + ok := false + + c.stateMu.Lock() + switch c.state { + case StateNone: + err = nil + case StateConn: + err = errors.New("start from CONN state") + case StateSetup: + if err = c.Play(); err == nil { + c.state = StatePlay + ok = true + } + case StatePlay: } - case core.ModePassiveProducer: - default: - return fmt.Errorf("start wrong mode: %d", c.mode) - } + c.stateMu.Unlock() - if err := c.Handle(); c.state != StateNone { - _ = c.conn.Close() - return err - } + if !ok { + return + } - return nil + // Handler can return different states: + // 1. None after PLAY should exit without error + // 2. Play after PLAY should exit from Start with error + // 3. Setup after PLAY should Play once again + err = c.Handle() + } } -func (c *Conn) Stop() error { +func (c *Conn) Stop() (err error) { for _, receiver := range c.receivers { receiver.Close() } for _, sender := range c.senders { sender.Close() } - return c.Close() + + c.stateMu.Lock() + if c.state != StateNone { + c.state = StateNone + err = c.Close() + } + c.stateMu.Unlock() + + return } func (c *Conn) MarshalJSON() ([]byte, error) { @@ -82,3 +109,27 @@ func (c *Conn) MarshalJSON() ([]byte, error) { return json.Marshal(info) } + +func (c *Conn) Reconnect() error { + c.Fire("RTSP reconnect") + + // close current session + _ = c.Close() + + // start new session + if err := c.Dial(); err != nil { + return err + } + if err := c.Describe(); err != nil { + return err + } + + // restore previous medias + for _, receiver := range c.receivers { + if _, err := c.SetupMedia(receiver.Media); err != nil { + return err + } + } + + return nil +} diff --git a/pkg/rtsp/server.go b/pkg/rtsp/server.go index d2be609c..f707f728 100644 --- a/pkg/rtsp/server.go +++ b/pkg/rtsp/server.go @@ -25,7 +25,7 @@ func (c *Conn) Auth(username, password string) { func (c *Conn) Accept() error { for { - req, err := tcp.ReadRequest(c.reader) + req, err := c.ReadRequest() if err != nil { return err } @@ -42,7 +42,7 @@ func (c *Conn) Accept() error { Status: "401 Unauthorized", Header: map[string][]string{"Www-Authenticate": {`Basic realm="go2rtc"`}}, } - if err = c.Response(res); err != nil { + if err = c.WriteResponse(res); err != nil { return err } continue @@ -58,7 +58,7 @@ func (c *Conn) Accept() error { }, Request: req, } - if err = c.Response(res); err != nil { + if err = c.WriteResponse(res); err != nil { return err } @@ -83,7 +83,7 @@ func (c *Conn) Accept() error { c.Fire(MethodAnnounce) res := &tcp.Response{Request: req} - if err = c.Response(res); err != nil { + if err = c.WriteResponse(res); err != nil { return err } @@ -96,7 +96,7 @@ func (c *Conn) Accept() error { Status: "404 Not Found", Request: req, } - return c.Response(res) + return c.WriteResponse(res) } res := &tcp.Response{ @@ -122,7 +122,7 @@ func (c *Conn) Accept() error { return err } - if err = c.Response(res); err != nil { + if err = c.WriteResponse(res); err != nil { return err } @@ -136,27 +136,27 @@ func (c *Conn) Accept() error { const transport = "RTP/AVP/TCP;unicast;interleaved=" if strings.HasPrefix(tr, transport) { - c.Session = core.RandString(8, 10) + c.session = core.RandString(8, 10) c.state = StateSetup res.Header.Set("Transport", tr[:len(transport)+3]) } else { res.Status = "461 Unsupported transport" } - if err = c.Response(res); err != nil { + if err = c.WriteResponse(res); err != nil { return err } case MethodRecord, MethodPlay: res := &tcp.Response{Request: req} - if err = c.Response(res); err == nil { + if err = c.WriteResponse(res); err == nil { c.state = StatePlay } return err case MethodTeardown: res := &tcp.Response{Request: req} - _ = c.Response(res) + _ = c.WriteResponse(res) c.state = StateNone return c.conn.Close() From 757091e43dd9bc038da45d42ce1733d31954ff64 Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Sun, 16 Apr 2023 14:47:07 +0300 Subject: [PATCH 08/80] Rewrite RTSP keepalive --- pkg/rtsp/conn.go | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/pkg/rtsp/conn.go b/pkg/rtsp/conn.go index ebc127bc..142bc77d 100644 --- a/pkg/rtsp/conn.go +++ b/pkg/rtsp/conn.go @@ -89,11 +89,13 @@ const ( func (c *Conn) Handle() (err error) { var timeout time.Duration + var keepalive time.Time + switch c.mode { case core.ModeActiveProducer: - // polling frames from remote RTSP Server (ex Camera) - go c.keepalive() + keepalive = time.Now().Add(time.Second * 25) + // polling frames from remote RTSP Server (ex Camera) if len(c.receivers) > 0 { // if we receiving video/audio from camera timeout = time.Second * 5 @@ -115,7 +117,9 @@ func (c *Conn) Handle() (err error) { } for c.state != StateNone { - if err = c.conn.SetReadDeadline(time.Now().Add(timeout)); err != nil { + ts := time.Now() + + if err = c.conn.SetReadDeadline(ts.Add(timeout)); err != nil { return } @@ -232,6 +236,15 @@ func (c *Conn) Handle() (err error) { c.Fire(msg) } + + if !keepalive.IsZero() && ts.After(keepalive) { + req := &tcp.Request{Method: MethodOptions, URL: c.URL} + if err = c.WriteRequest(req); err != nil { + return + } + + keepalive = ts.Add(time.Second * 25) + } } return @@ -322,17 +335,3 @@ func (c *Conn) ReadResponse() (*tcp.Response, error) { } return tcp.ReadResponse(c.reader) } - -func (c *Conn) keepalive() { - // TODO: rewrite to RTCP - req := &tcp.Request{Method: MethodOptions, URL: c.URL} - for { - time.Sleep(time.Second * 25) - if c.state == StateNone { - return - } - if err := c.WriteRequest(req); err != nil { - return - } - } -} From da08d8e973e053072f16f23c8af42b50a68fe611 Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Sun, 16 Apr 2023 14:47:49 +0300 Subject: [PATCH 09/80] Fix RTSP backchannel processing --- pkg/rtsp/consumer.go | 7 +++++++ pkg/rtsp/producer.go | 5 +++++ 2 files changed, 12 insertions(+) diff --git a/pkg/rtsp/consumer.go b/pkg/rtsp/consumer.go index 7f7fece2..78182f97 100644 --- a/pkg/rtsp/consumer.go +++ b/pkg/rtsp/consumer.go @@ -7,6 +7,7 @@ import ( "github.com/AlexxIT/go2rtc/pkg/h265" "github.com/AlexxIT/go2rtc/pkg/mjpeg" "github.com/pion/rtp" + "time" ) func (c *Conn) GetMedias() []*core.Media { @@ -41,6 +42,8 @@ func (c *Conn) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiv return } + c.state = StateSetup + case core.ModePassiveConsumer: channel = byte(len(c.senders)) * 2 @@ -85,6 +88,10 @@ func (c *Conn) packetWriter(codec *core.Codec, channel uint8) core.HandlerFunc { return } + if err := c.conn.SetWriteDeadline(time.Now().Add(Timeout)); err != nil { + return + } + n, err := c.conn.Write(data) if err != nil { return diff --git a/pkg/rtsp/producer.go b/pkg/rtsp/producer.go index f7772c54..0cd5efc7 100644 --- a/pkg/rtsp/producer.go +++ b/pkg/rtsp/producer.go @@ -130,6 +130,11 @@ func (c *Conn) Reconnect() error { return err } } + for _, sender := range c.senders { + if _, err := c.SetupMedia(sender.Media); err != nil { + return err + } + } return nil } From 35087e08124aa9b839e009ed38711a6b542c0aaa Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Sun, 16 Apr 2023 14:48:26 +0300 Subject: [PATCH 10/80] Remove mutex from MP4 --- cmd/mp4/mp4.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/cmd/mp4/mp4.go b/cmd/mp4/mp4.go index 6f3fa1f7..0db0c004 100644 --- a/cmd/mp4/mp4.go +++ b/cmd/mp4/mp4.go @@ -4,7 +4,6 @@ import ( "net/http" "strconv" "strings" - "sync" "time" "github.com/AlexxIT/go2rtc/cmd/api" @@ -116,11 +115,8 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) { Medias: core.ParseQuery(r.URL.Query()), } - mu := &sync.Mutex{} cons.Listen(func(msg any) { if data, ok := msg.([]byte); ok { - mu.Lock() - defer mu.Unlock() if _, err := w.Write(data); err != nil && exit != nil { select { case exit <- err: From 235f2fde0d4ce7658310f05b5ff4d5e5c2b3831d Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Sun, 16 Apr 2023 14:52:02 +0300 Subject: [PATCH 11/80] Add control attr to RTSP server SDP --- pkg/rtsp/server.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/rtsp/server.go b/pkg/rtsp/server.go index f707f728..574f445d 100644 --- a/pkg/rtsp/server.go +++ b/pkg/rtsp/server.go @@ -8,6 +8,7 @@ import ( "github.com/AlexxIT/go2rtc/pkg/tcp" "net" "net/url" + "strconv" "strings" ) @@ -108,11 +109,12 @@ func (c *Conn) Accept() error { // convert tracks to real output medias medias var medias []*core.Media - for _, track := range c.senders { + for i, track := range c.senders { media := &core.Media{ Kind: core.GetKind(track.Codec.Name), Direction: core.DirectionRecvonly, Codecs: []*core.Codec{track.Codec}, + ID: "trackID=" + strconv.Itoa(i), } medias = append(medias, media) } From 1837e7c86ca15b541b8a5ce9683ba3baa08c674b Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Mon, 17 Apr 2023 10:08:42 +0300 Subject: [PATCH 12/80] Fix cons number in trace logs --- cmd/streams/stream.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/streams/stream.go b/cmd/streams/stream.go index 2300b53e..8c7be19d 100644 --- a/cmd/streams/stream.go +++ b/cmd/streams/stream.go @@ -48,7 +48,7 @@ func (s *Stream) SetSource(source string) { func (s *Stream) AddConsumer(cons core.Consumer) (err error) { // support for multiple simultaneous requests from different consumers - consN := atomic.AddInt32(&s.requests, 1) + consN := atomic.AddInt32(&s.requests, 1) - 1 var producers []*Producer // matched producers for consumer From fd580b6f2c35bf77d5a9095e7da445ad49cd5b3a Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Mon, 17 Apr 2023 10:09:38 +0300 Subject: [PATCH 13/80] Fix RTSP passive producer --- pkg/rtsp/producer.go | 12 ++++++++++-- pkg/rtsp/server.go | 5 +---- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/pkg/rtsp/producer.go b/pkg/rtsp/producer.go index 0cd5efc7..764fb6ef 100644 --- a/pkg/rtsp/producer.go +++ b/pkg/rtsp/producer.go @@ -51,11 +51,19 @@ func (c *Conn) Start() (err error) { case StateConn: err = errors.New("start from CONN state") case StateSetup: - if err = c.Play(); err == nil { + switch c.mode { + case core.ModeActiveProducer: + err = c.Play() + case core.ModePassiveProducer: + err = nil + default: + err = errors.New("start from wrong mode: " + c.mode.String()) + } + + if err == nil { c.state = StatePlay ok = true } - case StatePlay: } c.stateMu.Unlock() diff --git a/pkg/rtsp/server.go b/pkg/rtsp/server.go index 574f445d..ce6658b6 100644 --- a/pkg/rtsp/server.go +++ b/pkg/rtsp/server.go @@ -151,10 +151,7 @@ func (c *Conn) Accept() error { case MethodRecord, MethodPlay: res := &tcp.Response{Request: req} - if err = c.WriteResponse(res); err == nil { - c.state = StatePlay - } - return err + return c.WriteResponse(res) case MethodTeardown: res := &tcp.Response{Request: req} From a0e6005598a7266e257e69d87d9edcac2fbd56e0 Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Mon, 17 Apr 2023 14:17:21 +0300 Subject: [PATCH 14/80] Remove Range header check for MP4 for Chrome --- cmd/mp4/mp4.go | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/cmd/mp4/mp4.go b/cmd/mp4/mp4.go index 0db0c004..e7762f63 100644 --- a/cmd/mp4/mp4.go +++ b/cmd/mp4/mp4.go @@ -81,15 +81,8 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) { query := r.URL.Query() - // Chrome has Safari in UA, so check first Chrome and later Safari ua := r.UserAgent() - if strings.Contains(ua, " Chrome/") { - if r.Header.Values("Range") == nil { - w.Header().Set("Content-Type", "video/mp4") - w.WriteHeader(http.StatusOK) - return - } - } else if strings.Contains(ua, " Safari/") && !query.Has("duration") { + if strings.Contains(ua, " Safari/") && !strings.Contains(ua, " Chrome/") && !query.Has("duration") { // auto redirect to HLS/fMP4 format, because Safari not support MP4 stream url := "stream.m3u8?" + r.URL.RawQuery if !query.Has("mp4") { From 116319f8766664784baf68e867db1cc8bee3d9ac Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Mon, 17 Apr 2023 14:17:45 +0300 Subject: [PATCH 15/80] Restore mutex for MP4 --- cmd/mp4/mp4.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/cmd/mp4/mp4.go b/cmd/mp4/mp4.go index e7762f63..2e47d770 100644 --- a/cmd/mp4/mp4.go +++ b/cmd/mp4/mp4.go @@ -4,6 +4,7 @@ import ( "net/http" "strconv" "strings" + "sync" "time" "github.com/AlexxIT/go2rtc/cmd/api" @@ -108,9 +109,14 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) { Medias: core.ParseQuery(r.URL.Query()), } + var mu sync.Mutex cons.Listen(func(msg any) { if data, ok := msg.([]byte); ok { - if _, err := w.Write(data); err != nil && exit != nil { + mu.Lock() + _, err := w.Write(data) + mu.Unlock() + + if err != nil && exit != nil { select { case exit <- err: default: From edb4e6eaad483aaa6469ea048acc3e35e6f6c2e7 Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Mon, 17 Apr 2023 15:04:45 +0300 Subject: [PATCH 16/80] Update error msg for stream start --- cmd/streams/producer.go | 2 -- cmd/streams/stream.go | 80 ++++++++++++++++++++++++----------------- 2 files changed, 47 insertions(+), 35 deletions(-) diff --git a/cmd/streams/producer.go b/cmd/streams/producer.go index 95f2ac65..6eed311b 100644 --- a/cmd/streams/producer.go +++ b/cmd/streams/producer.go @@ -30,8 +30,6 @@ type Producer struct { receivers []*core.Receiver senders []*core.Receiver - lastErr error - state state mu sync.Mutex workerID int diff --git a/cmd/streams/stream.go b/cmd/streams/stream.go index 8c7be19d..468f8889 100644 --- a/cmd/streams/stream.go +++ b/cmd/streams/stream.go @@ -3,7 +3,6 @@ package streams import ( "encoding/json" "errors" - "fmt" "github.com/AlexxIT/go2rtc/pkg/core" "strings" "sync" @@ -50,9 +49,9 @@ func (s *Stream) AddConsumer(cons core.Consumer) (err error) { // support for multiple simultaneous requests from different consumers consN := atomic.AddInt32(&s.requests, 1) - 1 - var producers []*Producer // matched producers for consumer - - var codecs string + var statErrors []error + var statMedias []*core.Media + var statProds []*Producer // matched producers for consumer // Step 1. Get consumer medias for _, consMedia := range cons.GetMedias() { @@ -62,14 +61,14 @@ func (s *Stream) AddConsumer(cons core.Consumer) (err error) { for prodN, prod := range s.producers { if err = prod.Dial(); err != nil { log.Trace().Err(err).Msgf("[streams] skip prod=%s", prod.url) + statErrors = append(statErrors, err) continue } // Step 2. Get producer medias (not tracks yet) for _, prodMedia := range prod.GetMedias() { log.Trace().Msgf("[streams] check prod=%d media=%s", prodN, prodMedia) - - collectCodecs(prodMedia, &codecs) + statMedias = append(statMedias, prodMedia) // Step 3. Match consumer/producer codecs list prodCodec, consCodec := prodMedia.MatchMedia(consMedia) @@ -109,7 +108,7 @@ func (s *Stream) AddConsumer(cons core.Consumer) (err error) { } } - producers = append(producers, prod) + statProds = append(statProds, prod) if !consMedia.MatchAll() { break producers @@ -123,18 +122,8 @@ func (s *Stream) AddConsumer(cons core.Consumer) (err error) { s.stopProducers() } - if len(producers) == 0 { - if len(codecs) > 0 { - return errors.New("codecs not match: " + codecs) - } - - for i, producer := range s.producers { - if producer.lastErr != nil { - return fmt.Errorf("source %d error: %w", i, producer.lastErr) - } - } - - return fmt.Errorf("sources unavailable: %d", len(s.producers)) + if len(statProds) == 0 { + return formatError(statMedias, statErrors) } s.mu.Lock() @@ -142,7 +131,7 @@ func (s *Stream) AddConsumer(cons core.Consumer) (err error) { s.mu.Unlock() // there may be duplicates, but that's not a problem - for _, prod := range producers { + for _, prod := range statProds { prod.start() } @@ -219,22 +208,47 @@ func (s *Stream) MarshalJSON() ([]byte, error) { return json.Marshal(info) } -func collectCodecs(media *core.Media, codecs *string) { - if media.Direction == core.DirectionRecvonly { - return - } +func formatError(statMedias []*core.Media, statErrors []error) error { + var text string - for _, codec := range media.Codecs { - name := codec.Name - if name == core.CodecAAC { - name = "AAC" - } - if strings.Contains(*codecs, name) { + for _, media := range statMedias { + if media.Direction == core.DirectionRecvonly { continue } - if len(*codecs) > 0 { - *codecs += "," + + for _, codec := range media.Codecs { + name := codec.Name + if name == core.CodecAAC { + name = "AAC" + } + if strings.Contains(text, name) { + continue + } + if len(text) > 0 { + text += "," + } + text += name } - *codecs += name } + + if text != "" { + return errors.New(text) + } + + for _, err := range statErrors { + s := err.Error() + if strings.Contains(text, s) { + continue + } + if len(text) > 0 { + text += "," + } + text += s + } + + if text != "" { + return errors.New(text) + } + + return errors.New("unknown error") } From 6cef5faf27dc3b14cef74d95c220bccae90fe72e Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Mon, 17 Apr 2023 15:12:03 +0300 Subject: [PATCH 17/80] Add timeout value to RTSP SETUP response #289 --- pkg/rtsp/conn.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/pkg/rtsp/conn.go b/pkg/rtsp/conn.go index 142bc77d..69e67faa 100644 --- a/pkg/rtsp/conn.go +++ b/pkg/rtsp/conn.go @@ -72,9 +72,9 @@ func (s State) String() string { return "CONN" case StateSetup: - return "SETUP" + return MethodSetup case StatePlay: - return "PLAY" + return MethodPlay } return strconv.Itoa(int(s)) } @@ -312,7 +312,11 @@ func (c *Conn) WriteResponse(res *tcp.Response) error { } if c.session != "" { - res.Header.Set("Session", c.session) + if res.Request != nil && res.Request.Method == MethodSetup { + res.Header.Set("Session", c.session+";timeout=60") + } else { + res.Header.Set("Session", c.session) + } } if res.Body != nil { From 76ec70d2a0d63e20dbdc5912aedbe15a6f1677d2 Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Mon, 17 Apr 2023 16:54:02 +0300 Subject: [PATCH 18/80] Adds RTSP client custom keepalive timeout --- pkg/rtsp/client.go | 6 +++--- pkg/rtsp/conn.go | 29 ++++++++++++++++++----------- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/pkg/rtsp/client.go b/pkg/rtsp/client.go index 4ed228f0..68e06d10 100644 --- a/pkg/rtsp/client.go +++ b/pkg/rtsp/client.go @@ -246,10 +246,10 @@ func (c *Conn) SetupMedia(media *core.Media) (byte, error) { if c.session == "" { // Session: 216525287999;timeout=60 if s := res.Header.Get("Session"); s != "" { - if j := strings.IndexByte(s, ';'); j > 0 { - s = s[:j] + c.session, s, _ = strings.Cut(s, ";timeout=") + if s != "" { + c.keepalive, _ = strconv.Atoi(s) } - c.session = s } } diff --git a/pkg/rtsp/conn.go b/pkg/rtsp/conn.go index 69e67faa..1a00ac8b 100644 --- a/pkg/rtsp/conn.go +++ b/pkg/rtsp/conn.go @@ -30,13 +30,14 @@ type Conn struct { // internal - auth *tcp.Auth - conn net.Conn - mode core.Mode - reader *bufio.Reader - sequence int - session string - uri string + auth *tcp.Auth + conn net.Conn + keepalive int + mode core.Mode + reader *bufio.Reader + sequence int + session string + uri string state State stateMu sync.Mutex @@ -89,11 +90,17 @@ const ( func (c *Conn) Handle() (err error) { var timeout time.Duration - var keepalive time.Time + var keepaliveDT time.Duration + var keepaliveTS time.Time switch c.mode { case core.ModeActiveProducer: - keepalive = time.Now().Add(time.Second * 25) + if c.keepalive > 5 { + keepaliveDT = time.Duration(c.keepalive-5) * time.Second + } else { + keepaliveDT = 25 * time.Second + } + keepaliveTS = time.Now().Add(keepaliveDT) // polling frames from remote RTSP Server (ex Camera) if len(c.receivers) > 0 { @@ -237,13 +244,13 @@ func (c *Conn) Handle() (err error) { c.Fire(msg) } - if !keepalive.IsZero() && ts.After(keepalive) { + if keepaliveDT != 0 && ts.After(keepaliveTS) { req := &tcp.Request{Method: MethodOptions, URL: c.URL} if err = c.WriteRequest(req); err != nil { return } - keepalive = ts.Add(time.Second * 25) + keepaliveTS = ts.Add(keepaliveDT) } } From 3feaf852afae780588c495a865e38998c672e417 Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Mon, 17 Apr 2023 17:02:24 +0300 Subject: [PATCH 19/80] Fix panic for wrong ffmpeg device in linux --- cmd/ffmpeg/device/device_linux.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cmd/ffmpeg/device/device_linux.go b/cmd/ffmpeg/device/device_linux.go index 3ce29a86..b9aa6b64 100644 --- a/cmd/ffmpeg/device/device_linux.go +++ b/cmd/ffmpeg/device/device_linux.go @@ -12,8 +12,10 @@ import ( const deviceInputPrefix = "-f v4l2" func deviceInputSuffix(videoIdx, audioIdx int) string { - video := findMedia(core.KindVideo, videoIdx) - return video.ID + if video := findMedia(core.KindVideo, videoIdx); video != nil { + return video.ID + } + return "" } func loadMedias() { From 79f1dcfea3a45d0cde6699420921dbd435b3c5e6 Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Mon, 17 Apr 2023 17:03:12 +0300 Subject: [PATCH 20/80] Update version to 1.3.2 --- cmd/app/app.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/app/app.go b/cmd/app/app.go index 9a66a535..6bbab359 100644 --- a/cmd/app/app.go +++ b/cmd/app/app.go @@ -16,7 +16,7 @@ import ( "gopkg.in/yaml.v3" ) -var Version = "1.3.1" +var Version = "1.3.2" var UserAgent = "go2rtc/" + Version var ConfigPath string From c07ddb83096c999ed51d3e838b1182c94c9cc075 Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Thu, 20 Apr 2023 13:16:15 +0300 Subject: [PATCH 21/80] Add HTTP 500 error response for MP4 API --- cmd/mp4/mp4.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cmd/mp4/mp4.go b/cmd/mp4/mp4.go index 2e47d770..c0fc343f 100644 --- a/cmd/mp4/mp4.go +++ b/cmd/mp4/mp4.go @@ -61,6 +61,7 @@ func handlerKeyframe(w http.ResponseWriter, r *http.Request) { if err := stream.AddConsumer(cons); err != nil { log.Error().Err(err).Caller().Send() + http.Error(w, err.Error(), http.StatusInternalServerError) return } @@ -128,6 +129,7 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) { if err := stream.AddConsumer(cons); err != nil { log.Error().Err(err).Caller().Send() + http.Error(w, err.Error(), http.StatusInternalServerError) return } @@ -138,11 +140,13 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) { data, err := cons.Init() if err != nil { log.Error().Err(err).Caller().Send() + http.Error(w, err.Error(), http.StatusInternalServerError) return } if _, err = w.Write(data); err != nil { log.Error().Err(err).Caller().Send() + http.Error(w, err.Error(), http.StatusInternalServerError) return } From 5f9788209d0ad50d3183893e43821ec1ec9ab67a Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Thu, 20 Apr 2023 13:20:52 +0300 Subject: [PATCH 22/80] Move MP4 mutex from HTTP to Muxer --- cmd/mp4/mp4.go | 8 +------- pkg/mp4/consumer.go | 15 +++++++++++---- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/cmd/mp4/mp4.go b/cmd/mp4/mp4.go index c0fc343f..825cb647 100644 --- a/cmd/mp4/mp4.go +++ b/cmd/mp4/mp4.go @@ -4,7 +4,6 @@ import ( "net/http" "strconv" "strings" - "sync" "time" "github.com/AlexxIT/go2rtc/cmd/api" @@ -110,14 +109,9 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) { Medias: core.ParseQuery(r.URL.Query()), } - var mu sync.Mutex cons.Listen(func(msg any) { if data, ok := msg.([]byte); ok { - mu.Lock() - _, err := w.Write(data) - mu.Unlock() - - if err != nil && exit != nil { + if _, err := w.Write(data); err != nil && exit != nil { select { case exit <- err: default: diff --git a/pkg/mp4/consumer.go b/pkg/mp4/consumer.go index 47555540..a720c8ac 100644 --- a/pkg/mp4/consumer.go +++ b/pkg/mp4/consumer.go @@ -7,6 +7,7 @@ import ( "github.com/AlexxIT/go2rtc/pkg/h264" "github.com/AlexxIT/go2rtc/pkg/h265" "github.com/pion/rtp" + "sync" ) type Consumer struct { @@ -19,6 +20,7 @@ type Consumer struct { senders []*core.Sender muxer *Muxer + mu sync.Mutex wait byte send int @@ -70,10 +72,12 @@ func (c *Consumer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiv c.wait = waitNone } + // important to use Mutex because right fragment order + c.mu.Lock() buf := c.muxer.Marshal(trackID, packet) c.Fire(buf) - c.send += len(buf) + c.mu.Unlock() } if track.Codec.IsRTP() { @@ -97,10 +101,11 @@ func (c *Consumer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiv c.wait = waitNone } + c.mu.Lock() buf := c.muxer.Marshal(trackID, packet) c.Fire(buf) - c.send += len(buf) + c.mu.Unlock() } if track.Codec.IsRTP() { @@ -113,10 +118,11 @@ func (c *Consumer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiv return } + c.mu.Lock() buf := c.muxer.Marshal(trackID, packet) c.Fire(buf) - c.send += len(buf) + c.mu.Unlock() } if track.Codec.IsRTP() { @@ -129,10 +135,11 @@ func (c *Consumer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiv return } + c.mu.Lock() buf := c.muxer.Marshal(trackID, packet) c.Fire(buf) - c.send += len(buf) + c.mu.Unlock() } default: From 7452eb5e05dca11e0d8f8e2bef693bef9d5b22ca Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Thu, 20 Apr 2023 16:18:38 +0300 Subject: [PATCH 23/80] Add support FLAC codec to MP4/MSE --- cmd/ffmpeg/ffmpeg.go | 3 +- cmd/mp4/ws.go | 6 ++ go.mod | 2 + go.sum | 4 ++ pkg/core/codec.go | 8 +++ pkg/core/core.go | 3 +- pkg/core/media.go | 2 +- pkg/iso/atoms.go | 21 ++++-- pkg/iso/codecs.go | 16 ++++- pkg/mp4/consumer.go | 61 ++++++++-------- pkg/mp4/muxer.go | 62 ++++++++++++----- pkg/pcm/flac.go | 138 ++++++++++++++++++++++++++++++++++++ pkg/pcm/pcma.go | 53 ++++++++++++++ pkg/pcm/pcmu.go | 51 ++++++++++++++ pkg/pcm/v1/pcm.go | 155 +++++++++++++++++++++++++++++++++++++++++ pkg/pcm/v1/pcm_test.go | 39 +++++++++++ www/links.html | 4 +- www/video-rtc.js | 3 +- 18 files changed, 566 insertions(+), 65 deletions(-) create mode 100644 pkg/pcm/flac.go create mode 100644 pkg/pcm/pcma.go create mode 100644 pkg/pcm/pcmu.go create mode 100644 pkg/pcm/v1/pcm.go create mode 100644 pkg/pcm/v1/pcm_test.go diff --git a/cmd/ffmpeg/ffmpeg.go b/cmd/ffmpeg/ffmpeg.go index 1e61122f..016603a2 100644 --- a/cmd/ffmpeg/ffmpeg.go +++ b/cmd/ffmpeg/ffmpeg.go @@ -70,8 +70,7 @@ var defaults = map[string]string{ "aac": "-c:a aac", // keep sample rate and channels "aac/16000": "-c:a aac -ar:a 16000 -ac:a 1", "mp3": "-c:a libmp3lame -q:a 8", - "pcm": "-c:a pcm_s16be", - "pcm/8000": "-c:a pcm_s16be -ar:a 8000 -ac:a 1", + "pcm": "-c:a pcm_s16be -ar:a 8000 -ac:a 1", "pcm/16000": "-c:a pcm_s16be -ar:a 16000 -ac:a 1", "pcm/48000": "-c:a pcm_s16be -ar:a 48000 -ac:a 1", diff --git a/cmd/mp4/ws.go b/cmd/mp4/ws.go index 1ea4d235..8366916c 100644 --- a/cmd/mp4/ws.go +++ b/cmd/mp4/ws.go @@ -110,6 +110,12 @@ func parseMedias(codecs string, parseAudio bool) (medias []*core.Media) { case mp4.MimeAAC: codec := &core.Codec{Name: core.CodecAAC} audios = append(audios, codec) + case mp4.MimeFlac: + audios = append(audios, + &core.Codec{Name: core.CodecPCMA}, + &core.Codec{Name: core.CodecPCMU}, + &core.Codec{Name: core.CodecPCM}, + ) case mp4.MimeOpus: codec := &core.Codec{Name: core.CodecOpus} audios = append(audios, codec) diff --git a/go.mod b/go.mod index 6a0a8307..beae7f79 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,8 @@ require ( github.com/pion/stun v0.4.0 github.com/pion/webrtc/v3 v3.1.58 github.com/rs/zerolog v1.29.0 + github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3 + github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f github.com/stretchr/testify v1.8.2 github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9 gopkg.in/yaml.v3 v3.0.1 diff --git a/go.sum b/go.sum index f62d2bc1..20a2e4ee 100644 --- a/go.sum +++ b/go.sum @@ -106,6 +106,10 @@ github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.29.0 h1:Zes4hju04hjbvkVkOhdl2HpZa+0PmVwigmo8XoORE5w= github.com/rs/zerolog v1.29.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0= github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw= +github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3 h1:aQKxg3+2p+IFXXg97McgDGT5zcMrQoi0EICZs8Pgchs= +github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3/go.mod h1:9/etS5gpQq9BJsJMWg1wpLbfuSnkm8dPF6FdW2JXVhA= +github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f h1:1R9KdKjCNSd7F8iGTxIpoID9prlYH8nuNYKt0XvweHA= +github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f/go.mod h1:vQhwQ4meQEDfahT5kd61wLAF5AAeh5ZPLVI4JJ/tYo8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= diff --git a/pkg/core/codec.go b/pkg/core/codec.go index 67c6d2cb..f3de809f 100644 --- a/pkg/core/codec.go +++ b/pkg/core/codec.go @@ -99,6 +99,14 @@ func UnmarshalCodec(md *sdp.MediaDescription, payloadType string) *Codec { case "8": c.Name = CodecPCMA c.ClockRate = 8000 + case "10": + c.Name = CodecPCM + c.ClockRate = 44100 + c.Channels = 2 + case "11": + c.Name = CodecPCM + c.ClockRate = 44100 + c.Channels = 1 case "14": c.Name = CodecMP3 c.ClockRate = 44100 diff --git a/pkg/core/core.go b/pkg/core/core.go index 1a429d98..72d32b78 100644 --- a/pkg/core/core.go +++ b/pkg/core/core.go @@ -27,7 +27,8 @@ const ( CodecMP3 = "MPA" // payload: 14, aka MPEG-1 Layer III CodecPCM = "L16" // Linear PCM - CodecELD = "ELD" // AAC-ELD + CodecELD = "ELD" // AAC-ELD + CodecFLAC = "FLAC" CodecAll = "ALL" CodecAny = "ANY" diff --git a/pkg/core/media.go b/pkg/core/media.go index 697e9806..8c15b5b5 100644 --- a/pkg/core/media.go +++ b/pkg/core/media.go @@ -93,7 +93,7 @@ func GetKind(name string) string { switch name { case CodecH264, CodecH265, CodecVP8, CodecVP9, CodecAV1, CodecJPEG: return KindVideo - case CodecPCMU, CodecPCMA, CodecAAC, CodecOpus, CodecG722, CodecMP3, CodecELD: + case CodecPCMU, CodecPCMA, CodecAAC, CodecOpus, CodecG722, CodecMP3, CodecPCM, CodecELD, CodecFLAC: return KindAudio } return "" diff --git a/pkg/iso/atoms.go b/pkg/iso/atoms.go index 919e6c22..6a4c9fe7 100644 --- a/pkg/iso/atoms.go +++ b/pkg/iso/atoms.go @@ -32,6 +32,16 @@ const ( Mdat = "mdat" ) +const ( + sampleIsNonSync = 0x10000 + sampleDependsOn1 = 0x1000000 + sampleDependsOn2 = 0x2000000 + + SampleVideoIFrame = sampleDependsOn2 + SampleVideoNonIFrame = sampleDependsOn1 | sampleIsNonSync + SampleAudio = sampleIsNonSync +) + func (m *Movie) WriteFileType() { m.StartAtom(Ftyp) m.WriteString("iso5") @@ -250,7 +260,7 @@ func (m *Movie) WriteAudioTrack(id uint32, codec string, timescale uint32, chann m.EndAtom() // TRAK } -func (m *Movie) WriteMovieFragment(seq, tid, duration, size uint32, time uint64) { +func (m *Movie) WriteMovieFragment(seq, tid, duration, size, flags uint32, time uint64) { m.StartAtom(Moof) m.StartAtom(MoofMfhd) @@ -276,10 +286,10 @@ func (m *Movie) WriteMovieFragment(seq, tid, duration, size uint32, time uint64) TfhdDefaultSampleFlags | TfhdDefaultBaseIsMoof, ) - m.WriteUint32(tid) // track id - m.WriteUint32(duration) // default sample duration - m.WriteUint32(size) // default sample size - m.WriteUint32(0x2000000) // default sample flags + m.WriteUint32(tid) // track id + m.WriteUint32(duration) // default sample duration + m.WriteUint32(size) // default sample size + m.WriteUint32(flags) // default sample flags m.EndAtom() m.StartAtom(MoofTrafTfdt) @@ -314,5 +324,4 @@ func (m *Movie) WriteData(b []byte) { m.StartAtom(Mdat) m.Write(b) m.EndAtom() - } diff --git a/pkg/iso/codecs.go b/pkg/iso/codecs.go index fe1d6093..1ddd28f3 100644 --- a/pkg/iso/codecs.go +++ b/pkg/iso/codecs.go @@ -2,6 +2,7 @@ package iso import ( "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/AlexxIT/go2rtc/pkg/pcm" ) func (m *Movie) WriteVideo(codec string, width, height uint16, conf []byte) { @@ -46,9 +47,11 @@ func (m *Movie) WriteVideo(codec string, width, height uint16, conf []byte) { func (m *Movie) WriteAudio(codec string, channels uint16, sampleRate uint32, conf []byte) { switch codec { case core.CodecAAC, core.CodecMP3: - m.StartAtom("mp4a") + m.StartAtom("mp4a") // supported in all players and browsers + case core.CodecFLAC: + m.StartAtom("fLaC") // supported in all players and browsers case core.CodecOpus: - m.StartAtom("Opus") + m.StartAtom("Opus") // supported in Chrome and Firefox case core.CodecPCMU: m.StartAtom("ulaw") case core.CodecPCMA: @@ -56,6 +59,11 @@ func (m *Movie) WriteAudio(codec string, channels uint16, sampleRate uint32, con default: panic("unsupported iso audio: " + codec) } + + if channels == 0 { + channels = 1 + } + m.Skip(6) m.WriteUint16(1) // data_reference_index m.Skip(2) // version @@ -72,6 +80,10 @@ func (m *Movie) WriteAudio(codec string, channels uint16, sampleRate uint32, con m.WriteEsdsAAC(conf) case core.CodecMP3: m.WriteEsdsMP3() + case core.CodecFLAC: + m.StartAtom("dfLa") + m.Write(pcm.FLACHeader(false, sampleRate)) + m.EndAtom() case core.CodecOpus: // don't know what means this magic m.StartAtom("dOps") diff --git a/pkg/mp4/consumer.go b/pkg/mp4/consumer.go index a720c8ac..13542c58 100644 --- a/pkg/mp4/consumer.go +++ b/pkg/mp4/consumer.go @@ -6,6 +6,7 @@ import ( "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/h264" "github.com/AlexxIT/go2rtc/pkg/h265" + "github.com/AlexxIT/go2rtc/pkg/pcm" "github.com/pion/rtp" "sync" ) @@ -54,7 +55,8 @@ func (c *Consumer) GetMedias() []*core.Media { func (c *Consumer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiver) error { trackID := byte(len(c.senders)) - handler := core.NewSender(media, track.Codec) + codec := track.Codec.Clone() + handler := core.NewSender(media, codec) switch track.Codec.Name { case core.CodecH264: @@ -112,38 +114,33 @@ func (c *Consumer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiv handler.Handler = h265.RTPDepay(track.Codec, handler.Handler) } - case core.CodecAAC: - handler.Handler = func(packet *rtp.Packet) { - if c.wait != waitNone { - return - } - - c.mu.Lock() - buf := c.muxer.Marshal(trackID, packet) - c.Fire(buf) - c.send += len(buf) - c.mu.Unlock() - } - - if track.Codec.IsRTP() { - handler.Handler = aac.RTPDepay(handler.Handler) - } - - case core.CodecOpus, core.CodecMP3, core.CodecPCMU, core.CodecPCMA: - handler.Handler = func(packet *rtp.Packet) { - if c.wait != waitNone { - return - } - - c.mu.Lock() - buf := c.muxer.Marshal(trackID, packet) - c.Fire(buf) - c.send += len(buf) - c.mu.Unlock() - } - default: - panic("unsupported codec") + handler.Handler = func(packet *rtp.Packet) { + if c.wait != waitNone { + return + } + + c.mu.Lock() + buf := c.muxer.Marshal(trackID, packet) + c.Fire(buf) + c.send += len(buf) + c.mu.Unlock() + } + + switch track.Codec.Name { + case core.CodecAAC: + if track.Codec.IsRTP() { + handler.Handler = aac.RTPDepay(handler.Handler) + } + case core.CodecOpus, core.CodecMP3: // no changes + case core.CodecPCMA, core.CodecPCMU, core.CodecPCM: + handler.Handler = pcm.FLACEncoder(track.Codec, handler.Handler) + codec.Name = core.CodecFLAC + + default: + println("ERROR: MP4 unsupported codec: " + track.Codec.Name) + return nil + } } handler.HandleRTP(track) diff --git a/pkg/mp4/muxer.go b/pkg/mp4/muxer.go index 067902a8..485d8fc6 100644 --- a/pkg/mp4/muxer.go +++ b/pkg/mp4/muxer.go @@ -15,12 +15,14 @@ type Muxer struct { fragIndex uint32 dts []uint64 pts []uint32 + codecs []*core.Codec } const ( MimeH264 = "avc1.640029" MimeH265 = "hvc1.1.6.L153.B0" MimeAAC = "mp4a.40.2" + MimeFlac = "flac" MimeOpus = "opus" ) @@ -43,6 +45,8 @@ func (m *Muxer) MimeCodecs(codecs []*core.Codec) string { s += MimeAAC case core.CodecOpus: s += MimeOpus + case core.CodecFLAC: + s += MimeFlac } } @@ -108,14 +112,15 @@ func (m *Muxer) GetInit(codecs []*core.Codec) ([]byte, error) { uint32(i+1), codec.Name, codec.ClockRate, codec.Channels, b, ) - case core.CodecOpus, core.CodecMP3, core.CodecPCMU, core.CodecPCMA: + case core.CodecOpus, core.CodecMP3, core.CodecPCMA, core.CodecPCMU, core.CodecPCM, core.CodecFLAC: mv.WriteAudioTrack( uint32(i+1), codec.Name, codec.ClockRate, codec.Channels, nil, ) } - m.pts = append(m.pts, 0) m.dts = append(m.dts, 0) + m.pts = append(m.pts, 0) + m.codecs = append(m.codecs, codec) } mv.StartAtom(iso.MoovMvex) @@ -138,28 +143,49 @@ func (m *Muxer) Reset() { } func (m *Muxer) Marshal(trackID byte, packet *rtp.Packet) []byte { - // important before increment - time := m.dts[trackID] + codec := m.codecs[trackID] + + duration := packet.Timestamp - m.pts[trackID] + m.pts[trackID] = packet.Timestamp + + // minumum duration important for MSE in Apple Safari + if duration == 0 || duration > codec.ClockRate { + duration = codec.ClockRate/1000 + 1 + m.pts[trackID] += duration + } + + size := len(packet.Payload) + + // flags important for Apple Finder video preview + var flags uint32 + switch codec.Name { + case core.CodecH264: + if h264.IsKeyframe(packet.Payload) { + flags = iso.SampleVideoIFrame + } else { + flags = iso.SampleVideoNonIFrame + } + case core.CodecH265: + if h265.IsKeyframe(packet.Payload) { + flags = iso.SampleVideoIFrame + } else { + flags = iso.SampleVideoNonIFrame + } + default: + flags = iso.SampleAudio // not important + } m.fragIndex++ - var duration uint32 - newTime := packet.Timestamp - if m.pts[trackID] > 0 { - duration = newTime - m.pts[trackID] - m.dts[trackID] += uint64(duration) - } else { - // important, or Safari will fail with first frame - duration = 1 - } - m.pts[trackID] = newTime - - mv := iso.NewMovie(1024 + len(packet.Payload)) + mv := iso.NewMovie(1024 + size) mv.WriteMovieFragment( - m.fragIndex, uint32(trackID+1), duration, - uint32(len(packet.Payload)), time, + m.fragIndex, uint32(trackID+1), duration, uint32(size), flags, m.dts[trackID], ) mv.WriteData(packet.Payload) + //log.Printf("[MP4] track=%d ts=%6d dur=%5d idx=%3d len=%d", trackID+1, m.dts[trackID], duration, m.fragIndex, len(packet.Payload)) + + m.dts[trackID] += uint64(duration) + return mv.Bytes() } diff --git a/pkg/pcm/flac.go b/pkg/pcm/flac.go new file mode 100644 index 00000000..cd72016d --- /dev/null +++ b/pkg/pcm/flac.go @@ -0,0 +1,138 @@ +// Package pcm - support raw (verbatim) PCM 16 bit in the FLAC container: +// - only 1 channel +// - only 16 bit per sample +// - only 8000, 16000, 24000, 48000 sample rate +package pcm + +import ( + "encoding/binary" + "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/pion/rtp" + "github.com/sigurn/crc16" + "github.com/sigurn/crc8" + "unicode/utf8" +) + +func FLACHeader(magic bool, sampleRate uint32) []byte { + b := make([]byte, 42) + + if magic { + copy(b, "fLaC") // [0..3] + } + + // https://xiph.org/flac/format.html#metadata_block_header + b[4] = 0x80 // [4] lastMetadata=1 (1 bit), blockType=0 - STREAMINFO (7 bit) + b[7] = 0x22 // [5..7] blockLength=34 (24 bit) + + // Important for Apple QuickTime player: + // 1. Both values should be same + // 2. Maximum value = 32768 + binary.BigEndian.PutUint16(b[8:], 32768) // [8..9] info.BlockSizeMin=16 (16 bit) + binary.BigEndian.PutUint16(b[10:], 32768) // [10..11] info.BlockSizeMin=65535 (16 bit) + + // [12..14] info.FrameSizeMin=0 (24 bit) + // [15..17] info.FrameSizeMax=0 (24 bit) + + b[18] = byte(sampleRate >> 12) + b[19] = byte(sampleRate >> 4) + b[20] = byte(sampleRate << 4) // [18..20] info.SampleRate=8000 (20 bit), info.NChannels=1-1 (3 bit) + + b[21] = 0xF0 // [21..25] info.BitsPerSample=16-1 (5 bit), info.NSamples (36 bit) + + // [26..41] MD5sum (16 bytes) + + return b +} + +var table8 *crc8.Table +var table16 *crc16.Table + +func FLACEncoder(codec *core.Codec, handler core.HandlerFunc) core.HandlerFunc { + if codec.Channels >= 2 { + return nil + } + + var sr byte + switch codec.ClockRate { + case 8000: + sr = 0b0100 + case 16000: + sr = 0b0101 + case 24000: + sr = 0b0111 + case 48000: + sr = 0b1010 + default: + return nil + } + + if table8 == nil { + table8 = crc8.MakeTable(crc8.CRC8) + } + if table16 == nil { + table16 = crc16.MakeTable(crc16.CRC16_BUYPASS) + } + + var sampleNumber int32 + + return func(packet *rtp.Packet) { + samples := uint16(len(packet.Payload)) + + if codec.Name == core.CodecPCM { + samples /= 2 + } + + // https://xiph.org/flac/format.html#frame_header + buf := make([]byte, samples*2+30) + + // 1. Frame header + buf[0] = 0xFF + buf[1] = 0xF9 // [0..1] syncCode=0xFFF8 - reserved (15 bit), blockStrategy=1 - variable-blocksize (1 bit) + buf[2] = 0x70 | sr // blockSizeType=7 (4 bit), sampleRate=4 - 8000 (4 bit) + buf[3] = 0x08 // channels=1-1 (4 bit), sampleSize=4 - 16 (3 bit), reserved=0 (1 bit) + + n := 4 + utf8.EncodeRune(buf[4:], sampleNumber) // 4 bytes max + sampleNumber += int32(samples) + + // this is wrong but very simple frame block size value + binary.BigEndian.PutUint16(buf[n:], samples-1) + n += 2 + + buf[n] = crc8.Checksum(buf[:n], table8) + n += 1 + + // 2. Subframe header + buf[n] = 0x02 // padding=0 (1 bit), subframeType=1 - verbatim (6 bit), wastedFlag=0 (1 bit) + n += 1 + + // 3. Subframe + switch codec.Name { + case core.CodecPCMA: + for _, b := range packet.Payload { + s16 := PCMAtoPCM(b) + buf[n] = byte(s16 >> 8) + buf[n+1] = byte(s16) + n += 2 + } + case core.CodecPCMU: + for _, b := range packet.Payload { + s16 := PCMUtoPCM(b) + buf[n] = byte(s16 >> 8) + buf[n+1] = byte(s16) + n += 2 + } + case core.CodecPCM: + n += copy(buf[n:], packet.Payload) + } + + // 4. Frame footer + crc := crc16.Checksum(buf[:n], table16) + binary.BigEndian.PutUint16(buf[n:], crc) + n += 2 + + clone := *packet + clone.Payload = buf[:n] + + handler(&clone) + } +} diff --git a/pkg/pcm/pcma.go b/pkg/pcm/pcma.go new file mode 100644 index 00000000..3e1ef112 --- /dev/null +++ b/pkg/pcm/pcma.go @@ -0,0 +1,53 @@ +// Package pcm +// https://www.codeproject.com/Articles/14237/Using-the-G711-standard +package pcm + +const alawMax = 0x7FFF + +func PCMAtoPCM(alaw byte) int16 { + alaw ^= 0xD5 + + data := int16(((alaw & 0x0F) << 4) + 8) + exponent := (alaw & 0x70) >> 4 + + if exponent != 0 { + data |= 0x100 + } + + if exponent > 1 { + data <<= exponent - 1 + } + + // sign + if alaw&0x80 == 0 { + return data + } else { + return -data + } +} + +func PCMtoPCMA(pcm int16) byte { + var alaw byte + + if pcm < 0 { + pcm = -pcm + alaw = 0x80 + } + + if pcm > alawMax { + pcm = alawMax + } + + exponent := byte(7) + for expMask := int16(0x4000); (pcm&expMask) == 0 && exponent > 0; expMask >>= 1 { + exponent-- + } + + if exponent == 0 { + alaw |= byte(pcm>>4) & 0x0F + } else { + alaw |= (exponent << 4) | (byte(pcm>>(exponent+3)) & 0x0F) + } + + return alaw ^ 0xD5 +} diff --git a/pkg/pcm/pcmu.go b/pkg/pcm/pcmu.go new file mode 100644 index 00000000..954d8a99 --- /dev/null +++ b/pkg/pcm/pcmu.go @@ -0,0 +1,51 @@ +// Package pcm +// https://www.codeproject.com/Articles/14237/Using-the-G711-standard +package pcm + +const bias = 0x84 // 132 or 1000 0100 +const ulawMax = alawMax - bias + +func PCMUtoPCM(ulaw byte) int16 { + ulaw = ^ulaw + + exponent := (ulaw & 0x70) >> 4 + data := (int16((((ulaw&0x0F)|0x10)<<1)+1) << (exponent + 2)) - bias + + // sign + if ulaw&0x80 == 0 { + return data + } else if data == 0 { + return -1 + } else { + return -data + } +} + +func PCMtoPCMU(pcm int16) byte { + var ulaw byte + + if pcm < 0 { + pcm = -pcm + ulaw = 0x80 + } + + if pcm > ulawMax { + pcm = ulawMax + } + + pcm += bias + + exponent := byte(7) + for expMask := int16(0x4000); (pcm & expMask) == 0; expMask >>= 1 { + exponent-- + } + + // mantisa + ulaw |= byte(pcm>>(exponent+3)) & 0x0F + + if exponent > 0 { + ulaw |= exponent << 4 + } + + return ^ulaw +} diff --git a/pkg/pcm/v1/pcm.go b/pkg/pcm/v1/pcm.go new file mode 100644 index 00000000..e1652350 --- /dev/null +++ b/pkg/pcm/v1/pcm.go @@ -0,0 +1,155 @@ +// Package v1 +// http://web.archive.org/web/20110719132013/http://hazelware.luggle.com/tutorials/mulawcompression.html +package v1 + +const cBias = 0x84 +const cClip = 32635 + +var MuLawCompressTable = [256]byte{ + 0, 0, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, +} + +func LinearToMuLawSample(sample int16) byte { + sign := byte(sample>>8) & 0x80 + if sign != 0 { + sample = -sample + } + + if sample > cClip { + sample = cClip + } + sample = sample + cBias + + exponent := MuLawCompressTable[(sample>>7)&0xFF] + mantissa := byte(sample>>(exponent+3)) & 0x0F + + compressedByte := ^(sign | (exponent << 4) | mantissa) + + return compressedByte +} + +var ALawCompressTable = [128]byte{ + 1, 1, 2, 2, 3, 3, 3, 3, + 4, 4, 4, 4, 4, 4, 4, 4, + 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, + 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, + 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, +} + +func LinearToALawSample(sample int16) byte { + sign := byte((^sample)>>8) & 0x80 + if sign == 0 { + sample = -sample + } + + if sample > cClip { + sample = cClip + } + + var compressedByte byte + if sample >= 256 { + exponent := ALawCompressTable[(sample>>8)&0x7F] + mantissa := byte(sample>>(exponent+3)) & 0x0F + compressedByte = (exponent << 4) | mantissa + } else { + compressedByte = byte(sample >> 4) + } + compressedByte ^= sign ^ 0x55 + return compressedByte +} + +var MuLawDecompressTable = [256]int16{ + -32124, -31100, -30076, -29052, -28028, -27004, -25980, -24956, + -23932, -22908, -21884, -20860, -19836, -18812, -17788, -16764, + -15996, -15484, -14972, -14460, -13948, -13436, -12924, -12412, + -11900, -11388, -10876, -10364, -9852, -9340, -8828, -8316, + -7932, -7676, -7420, -7164, -6908, -6652, -6396, -6140, + -5884, -5628, -5372, -5116, -4860, -4604, -4348, -4092, + -3900, -3772, -3644, -3516, -3388, -3260, -3132, -3004, + -2876, -2748, -2620, -2492, -2364, -2236, -2108, -1980, + -1884, -1820, -1756, -1692, -1628, -1564, -1500, -1436, + -1372, -1308, -1244, -1180, -1116, -1052, -988, -924, + -876, -844, -812, -780, -748, -716, -684, -652, + -620, -588, -556, -524, -492, -460, -428, -396, + -372, -356, -340, -324, -308, -292, -276, -260, + -244, -228, -212, -196, -180, -164, -148, -132, + -120, -112, -104, -96, -88, -80, -72, -64, + -56, -48, -40, -32, -24, -16, -8, -1, + 32124, 31100, 30076, 29052, 28028, 27004, 25980, 24956, + 23932, 22908, 21884, 20860, 19836, 18812, 17788, 16764, + 15996, 15484, 14972, 14460, 13948, 13436, 12924, 12412, + 11900, 11388, 10876, 10364, 9852, 9340, 8828, 8316, + 7932, 7676, 7420, 7164, 6908, 6652, 6396, 6140, + 5884, 5628, 5372, 5116, 4860, 4604, 4348, 4092, + 3900, 3772, 3644, 3516, 3388, 3260, 3132, 3004, + 2876, 2748, 2620, 2492, 2364, 2236, 2108, 1980, + 1884, 1820, 1756, 1692, 1628, 1564, 1500, 1436, + 1372, 1308, 1244, 1180, 1116, 1052, 988, 924, + 876, 844, 812, 780, 748, 716, 684, 652, + 620, 588, 556, 524, 492, 460, 428, 396, + 372, 356, 340, 324, 308, 292, 276, 260, + 244, 228, 212, 196, 180, 164, 148, 132, + 120, 112, 104, 96, 88, 80, 72, 64, + 56, 48, 40, 32, 24, 16, 8, 0, +} + +var ALawDecompressTable = [256]int16{ + -5504, -5248, -6016, -5760, -4480, -4224, -4992, -4736, + -7552, -7296, -8064, -7808, -6528, -6272, -7040, -6784, + -2752, -2624, -3008, -2880, -2240, -2112, -2496, -2368, + -3776, -3648, -4032, -3904, -3264, -3136, -3520, -3392, + -22016, -20992, -24064, -23040, -17920, -16896, -19968, -18944, + -30208, -29184, -32256, -31232, -26112, -25088, -28160, -27136, + -11008, -10496, -12032, -11520, -8960, -8448, -9984, -9472, + -15104, -14592, -16128, -15616, -13056, -12544, -14080, -13568, + -344, -328, -376, -360, -280, -264, -312, -296, + -472, -456, -504, -488, -408, -392, -440, -424, + -88, -72, -120, -104, -24, -8, -56, -40, + -216, -200, -248, -232, -152, -136, -184, -168, + -1376, -1312, -1504, -1440, -1120, -1056, -1248, -1184, + -1888, -1824, -2016, -1952, -1632, -1568, -1760, -1696, + -688, -656, -752, -720, -560, -528, -624, -592, + -944, -912, -1008, -976, -816, -784, -880, -848, + 5504, 5248, 6016, 5760, 4480, 4224, 4992, 4736, + 7552, 7296, 8064, 7808, 6528, 6272, 7040, 6784, + 2752, 2624, 3008, 2880, 2240, 2112, 2496, 2368, + 3776, 3648, 4032, 3904, 3264, 3136, 3520, 3392, + 22016, 20992, 24064, 23040, 17920, 16896, 19968, 18944, + 30208, 29184, 32256, 31232, 26112, 25088, 28160, 27136, + 11008, 10496, 12032, 11520, 8960, 8448, 9984, 9472, + 15104, 14592, 16128, 15616, 13056, 12544, 14080, 13568, + 344, 328, 376, 360, 280, 264, 312, 296, + 472, 456, 504, 488, 408, 392, 440, 424, + 88, 72, 120, 104, 24, 8, 56, 40, + 216, 200, 248, 232, 152, 136, 184, 168, + 1376, 1312, 1504, 1440, 1120, 1056, 1248, 1184, + 1888, 1824, 2016, 1952, 1632, 1568, 1760, 1696, + 688, 656, 752, 720, 560, 528, 624, 592, + 944, 912, 1008, 976, 816, 784, 880, 848, +} diff --git a/pkg/pcm/v1/pcm_test.go b/pkg/pcm/v1/pcm_test.go new file mode 100644 index 00000000..2db5d95c --- /dev/null +++ b/pkg/pcm/v1/pcm_test.go @@ -0,0 +1,39 @@ +package v1 + +import ( + v2 "github.com/AlexxIT/go2rtc/pkg/pcm" + "github.com/stretchr/testify/require" + "testing" +) + +func TestPCMUtoPCM(t *testing.T) { + for pcmu := byte(0); pcmu < 255; pcmu++ { + pcm1 := MuLawDecompressTable[pcmu] + pcm2 := v2.PCMUtoPCM(pcmu) + require.Equal(t, pcm1, pcm2) + } +} + +func TestPCMAtoPCM(t *testing.T) { + for pcma := byte(0); pcma < 255; pcma++ { + pcm1 := ALawDecompressTable[pcma] + pcm2 := v2.PCMAtoPCM(pcma) + require.Equal(t, pcm1, pcm2) + } +} + +func TestPCMtoPCMU(t *testing.T) { + for pcm := int16(-32768); pcm < 32767; pcm++ { + pcmu1 := LinearToMuLawSample(pcm) + pcmu2 := v2.PCMtoPCMU(pcm) + require.Equal(t, pcmu1, pcmu2) + } +} + +func TestPCMtoPCMA(t *testing.T) { + for pcm := int16(-32768); pcm < 32767; pcm++ { + pcma1 := LinearToALawSample(pcm) + pcma2 := v2.PCMtoPCMA(pcm) + require.Equal(t, pcma1, pcma2) + } +} diff --git a/www/links.html b/www/links.html index 6faa4637..94f5e4df 100644 --- a/www/links.html +++ b/www/links.html @@ -67,9 +67,9 @@

H264/H265 source

  • stream.html WebRTC stream / browsers: all / codecs: H264, PCMU, PCMA, OPUS / +H265 in Safari
  • -
  • stream.html MSE stream / browsers: Chrome, Firefox, Safari Mac/iPad / codecs: H264, H265*, AAC / +OPUS in Chrome and Firefox
  • +
  • stream.html MSE stream / browsers: Chrome, Firefox, Safari Mac/iPad / codecs: H264, H265*, AAC, PCMA*, PCMU*, PCM* / +OPUS in Chrome and Firefox
  • stream.mp4 MP4 stream with AAC audio / browsers: Chrome, Firefox / codecs: H264, H265*, AAC
  • -
  • stream.mp4 MP4 stream with any audio / browsers: Chrome / codecs: H264, H265*, AAC, OPUS, MP3, PCMU, PCMA
  • +
  • stream.mp4 MP4 stream with any audio / browsers: Chrome / codecs: H264, H265*, AAC, OPUS, MP3, PCMA, PCMU, PCM
  • frame.mp4 snapshot in MP4-format / browsers: all / codecs: H264, H265*
  • stream.m3u8 HLS/TS / browsers: Safari all, Chrome Android / codecs: H264
  • stream.m3u8 HLS/fMP4 / browsers: Safari all, Chrome Android / codecs: H264, H265*, AAC
  • diff --git a/www/video-rtc.js b/www/video-rtc.js index 445aa94d..11ec30be 100644 --- a/www/video-rtc.js +++ b/www/video-rtc.js @@ -27,7 +27,8 @@ export class VideoRTC extends HTMLElement { "hvc1.1.6.L153.B0", // H.265 main 5.1 (Chromecast Ultra) "mp4a.40.2", // AAC LC "mp4a.40.5", // AAC HE - "opus", // OPUS Chrome + "flac", // FLAC (PCM compatible) + "opus", // OPUS Chrome, Firefox ]; /** From e985ad23a2a133e45d3bec821f704a324243e2db Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Thu, 20 Apr 2023 16:19:32 +0300 Subject: [PATCH 24/80] Fix HLS handler --- cmd/hls/hls.go | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/cmd/hls/hls.go b/cmd/hls/hls.go index e9658538..2cd9685d 100644 --- a/cmd/hls/hls.go +++ b/cmd/hls/hls.go @@ -10,6 +10,7 @@ import ( "github.com/AlexxIT/go2rtc/pkg/tcp" "github.com/rs/zerolog/log" "net/http" + "strings" "sync" "time" ) @@ -48,6 +49,9 @@ const keepalive = 5 * time.Second var sessions = map[string]*Session{} +// once I saw 404 on MP4 segment, so better to use mutex +var sessionsMu sync.RWMutex + func handlerStream(w http.ResponseWriter, r *http.Request) { // CORS important for Chromecast w.Header().Set("Access-Control-Allow-Origin", "*") @@ -128,11 +132,16 @@ segment.ts?id=` + sid + `&n=%d segment.ts?id=` + sid + `&n=%d` } + sessionsMu.Lock() sessions[sid] = session + sessionsMu.Unlock() + + // Apple Safari can play FLAC codec, but fail it it in m3u8 playlist + codecs := strings.Replace(cons.MimeCodecs(), mp4.MimeFlac, mp4.MimeAAC, 1) // bandwidth important for Safari, codecs useful for smooth playback data := []byte(`#EXTM3U -#EXT-X-STREAM-INF:BANDWIDTH=1000000,CODECS="` + cons.MimeCodecs() + `" +#EXT-X-STREAM-INF:BANDWIDTH=1000000,CODECS="` + codecs + `" hls/playlist.m3u8?id=` + sid) if _, err := w.Write(data); err != nil { @@ -150,7 +159,9 @@ func handlerPlaylist(w http.ResponseWriter, r *http.Request) { } sid := r.URL.Query().Get("id") + sessionsMu.RLock() session := sessions[sid] + sessionsMu.RUnlock() if session == nil { http.NotFound(w, r) return @@ -173,7 +184,9 @@ func handlerSegmentTS(w http.ResponseWriter, r *http.Request) { } sid := r.URL.Query().Get("id") + sessionsMu.RLock() session := sessions[sid] + sessionsMu.RUnlock() if session == nil { http.NotFound(w, r) return @@ -212,7 +225,9 @@ func handlerInit(w http.ResponseWriter, r *http.Request) { } sid := r.URL.Query().Get("id") + sessionsMu.RLock() session := sessions[sid] + sessionsMu.RUnlock() if session == nil { http.NotFound(w, r) return @@ -233,7 +248,9 @@ func handlerSegmentMP4(w http.ResponseWriter, r *http.Request) { } sid := r.URL.Query().Get("id") + sessionsMu.RLock() session := sessions[sid] + sessionsMu.RUnlock() if session == nil { http.NotFound(w, r) return From 5939c8acba96b9f68ba9410762cfd9a13bc82cf5 Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Thu, 20 Apr 2023 21:32:16 +0300 Subject: [PATCH 25/80] Update MP4 links query --- cmd/mp4/mp4.go | 14 ++++++++++---- pkg/mp4/helpers.go | 42 +++++++++++++++++++++++++++++++++++++++--- www/links.html | 10 ++++++---- 3 files changed, 55 insertions(+), 11 deletions(-) diff --git a/cmd/mp4/mp4.go b/cmd/mp4/mp4.go index 825cb647..e6df4910 100644 --- a/cmd/mp4/mp4.go +++ b/cmd/mp4/mp4.go @@ -9,7 +9,6 @@ import ( "github.com/AlexxIT/go2rtc/cmd/api" "github.com/AlexxIT/go2rtc/cmd/app" "github.com/AlexxIT/go2rtc/cmd/streams" - "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/mp4" "github.com/AlexxIT/go2rtc/pkg/tcp" "github.com/rs/zerolog" @@ -106,12 +105,15 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) { cons := &mp4.Consumer{ RemoteAddr: tcp.RemoteAddr(r), UserAgent: r.UserAgent(), - Medias: core.ParseQuery(r.URL.Query()), + Medias: mp4.ParseQuery(r.URL.Query()), } cons.Listen(func(msg any) { + if exit == nil { + return + } if data, ok := msg.([]byte); ok { - if _, err := w.Write(data); err != nil && exit != nil { + if _, err := w.Write(data); err != nil { select { case exit <- err: default: @@ -151,7 +153,10 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) { if i, _ := strconv.Atoi(s); i > 0 { duration = time.AfterFunc(time.Second*time.Duration(i), func() { if exit != nil { - exit <- nil + select { + case exit <- nil: + default: + } exit = nil } }) @@ -159,6 +164,7 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) { } err = <-exit + exit = nil log.Trace().Err(err).Caller().Send() diff --git a/pkg/mp4/helpers.go b/pkg/mp4/helpers.go index 909b59cb..c22f1220 100644 --- a/pkg/mp4/helpers.go +++ b/pkg/mp4/helpers.go @@ -4,9 +4,45 @@ import "github.com/AlexxIT/go2rtc/pkg/core" // ParseQuery - like usual parse, but with mp4 param handler func ParseQuery(query map[string][]string) []*core.Media { - if query["mp4"] != nil { - cons := Consumer{} - return cons.GetMedias() + if v := query["mp4"]; len(v) != 0 { + medias := []*core.Media{ + { + Kind: core.KindVideo, + Direction: core.DirectionSendonly, + Codecs: []*core.Codec{ + {Name: core.CodecH264}, + {Name: core.CodecH265}, + }, + }, + { + Kind: core.KindAudio, + Direction: core.DirectionSendonly, + Codecs: []*core.Codec{ + {Name: core.CodecAAC}, + }, + }, + } + + if v[0] == "" { + return medias // legacy + } + + medias[1].Codecs = append(medias[1].Codecs, + &core.Codec{Name: core.CodecPCMA}, + &core.Codec{Name: core.CodecPCMU}, + &core.Codec{Name: core.CodecPCM}, + ) + + if v[0] == "flac" { + return medias // modern browsers + } + + medias[1].Codecs = append(medias[1].Codecs, + &core.Codec{Name: core.CodecOpus}, + &core.Codec{Name: core.CodecMP3}, + ) + + return medias // Chrome, FFmpeg, VLC } return core.ParseQuery(query) diff --git a/www/links.html b/www/links.html index 94f5e4df..fab598a6 100644 --- a/www/links.html +++ b/www/links.html @@ -68,11 +68,13 @@

    H264/H265 source

  • stream.html WebRTC stream / browsers: all / codecs: H264, PCMU, PCMA, OPUS / +H265 in Safari
  • stream.html MSE stream / browsers: Chrome, Firefox, Safari Mac/iPad / codecs: H264, H265*, AAC, PCMA*, PCMU*, PCM* / +OPUS in Chrome and Firefox
  • -
  • stream.mp4 MP4 stream with AAC audio / browsers: Chrome, Firefox / codecs: H264, H265*, AAC
  • -
  • stream.mp4 MP4 stream with any audio / browsers: Chrome / codecs: H264, H265*, AAC, OPUS, MP3, PCMA, PCMU, PCM
  • +
  • stream.mp4 legacy MP4 stream with AAC audio / browsers: Chrome, Firefox / codecs: H264, H265*, AAC
  • +
  • stream.mp4 modern MP4 stream with common audio / browsers: Chrome, Firefox / codecs: H264, H265*, AAC, FLAC (PCMA, PCMU, PCM)
  • +
  • stream.mp4 MP4 stream with any audio / browsers: Chrome / codecs: H264, H265*, AAC, OPUS, MP3, FLAC (PCMA, PCMU, PCM)
  • frame.mp4 snapshot in MP4-format / browsers: all / codecs: H264, H265*
  • -
  • stream.m3u8 HLS/TS / browsers: Safari all, Chrome Android / codecs: H264
  • -
  • stream.m3u8 HLS/fMP4 / browsers: Safari all, Chrome Android / codecs: H264, H265*, AAC
  • +
  • stream.m3u8 legacy HLS/TS / browsers: Safari all, Chrome Android / codecs: H264
  • +
  • stream.m3u8 legacy HLS/fMP4 / browsers: Safari all, Chrome Android / codecs: H264, H265*, AAC
  • +
  • stream.m3u8 modern HLS/fMP4 / browsers: Safari all, Chrome Android / codecs: H264, H265*, AAC, FLAC (PCMA, PCMU, PCM)
  • MJPEG source

  • stream.html with MJPEG mode / browsers: all / codecs: MJPEG, JPEG
  • From db85533e74e338d32116fe8ff10608512e19d156 Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Sat, 22 Apr 2023 08:52:32 +0300 Subject: [PATCH 26/80] Add more sample rates to FLAC encoder --- pkg/core/codec.go | 1 - pkg/pcm/flac.go | 8 ++++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/pkg/core/codec.go b/pkg/core/codec.go index f3de809f..61ea74da 100644 --- a/pkg/core/codec.go +++ b/pkg/core/codec.go @@ -106,7 +106,6 @@ func UnmarshalCodec(md *sdp.MediaDescription, payloadType string) *Codec { case "11": c.Name = CodecPCM c.ClockRate = 44100 - c.Channels = 1 case "14": c.Name = CodecMP3 c.ClockRate = 44100 diff --git a/pkg/pcm/flac.go b/pkg/pcm/flac.go index cd72016d..054746d1 100644 --- a/pkg/pcm/flac.go +++ b/pkg/pcm/flac.go @@ -58,10 +58,18 @@ func FLACEncoder(codec *core.Codec, handler core.HandlerFunc) core.HandlerFunc { sr = 0b0100 case 16000: sr = 0b0101 + case 22050: + sr = 0b0110 case 24000: sr = 0b0111 + case 32000: + sr = 0b1000 + case 44100: + sr = 0b1001 case 48000: sr = 0b1010 + case 96000: + sr = 0b1011 default: return nil } From 7626a09c1c243aafa2dac2eeab96f8e719c1dba0 Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Sat, 22 Apr 2023 08:53:08 +0300 Subject: [PATCH 27/80] Fix unsupported FLAC encoder params --- pkg/mp4/consumer.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pkg/mp4/consumer.go b/pkg/mp4/consumer.go index 13542c58..069016cb 100644 --- a/pkg/mp4/consumer.go +++ b/pkg/mp4/consumer.go @@ -138,11 +138,15 @@ func (c *Consumer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiv codec.Name = core.CodecFLAC default: - println("ERROR: MP4 unsupported codec: " + track.Codec.Name) - return nil + handler.Handler = nil } } + if handler.Handler == nil { + println("ERROR: MP4 unsupported codec: " + track.Codec.String()) + return nil + } + handler.HandleRTP(track) c.senders = append(c.senders, handler) From fb1cc7dfc2c600d239b77bbe942ca7b818f6962b Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Sat, 22 Apr 2023 08:53:35 +0300 Subject: [PATCH 28/80] Update FFmpeg OPUS params --- cmd/ffmpeg/ffmpeg.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cmd/ffmpeg/ffmpeg.go b/cmd/ffmpeg/ffmpeg.go index 016603a2..d5dfe988 100644 --- a/cmd/ffmpeg/ffmpeg.go +++ b/cmd/ffmpeg/ffmpeg.go @@ -60,7 +60,8 @@ var defaults = map[string]string{ "h265": "-c:v libx265 -g 50 -profile:v high -level:v 5.1 -preset:v superfast -tune:v zerolatency", "mjpeg": "-c:v mjpeg -force_duplicated_matrix:v 1 -huffman:v 0 -pix_fmt:v yuvj420p", - "opus": "-c:a libopus -ar:a 48000 -ac:a 2", + // https://ffmpeg.org/ffmpeg-codecs.html#libopus-1 + "opus": "-c:a libopus -ar:a 48000 -ac:a 2 -application:a voip -compression_level:a 0", "pcmu": "-c:a pcm_mulaw -ar:a 8000 -ac:a 1", "pcmu/16000": "-c:a pcm_mulaw -ar:a 16000 -ac:a 1", "pcmu/48000": "-c:a pcm_mulaw -ar:a 48000 -ac:a 1", From dd98edc48ee1d15f4073c52e012fee289c1bc8e5 Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Sat, 22 Apr 2023 08:54:31 +0300 Subject: [PATCH 29/80] Add support resampling for PCM for WebRTC --- pkg/pcm/pcm.go | 116 +++++++++++++++++++++++++++++++++++++++++ pkg/webrtc/consumer.go | 19 +++++-- pkg/webrtc/helpers.go | 47 +++++++++++++++++ 3 files changed, 178 insertions(+), 4 deletions(-) create mode 100644 pkg/pcm/pcm.go diff --git a/pkg/pcm/pcm.go b/pkg/pcm/pcm.go new file mode 100644 index 00000000..717a1450 --- /dev/null +++ b/pkg/pcm/pcm.go @@ -0,0 +1,116 @@ +package pcm + +import ( + "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/pion/rtp" +) + +func Resample(codec *core.Codec, sampleRate uint32, handler core.HandlerFunc) core.HandlerFunc { + n := float32(codec.ClockRate) / float32(sampleRate) + + switch codec.Name { + case core.CodecPCMA: + return DownsampleByte(PCMAtoPCM, PCMtoPCMA, n, handler) + case core.CodecPCMU: + return DownsampleByte(PCMUtoPCM, PCMtoPCMU, n, handler) + case core.CodecPCM: + if n == 1 { + return ResamplePCM(PCMtoPCMA, handler) + } + return DownsamplePCM(PCMtoPCMA, n, handler) + } + + panic(core.Caller()) +} + +func DownsampleByte( + toPCM func(byte) int16, fromPCM func(int16) byte, n float32, handler core.HandlerFunc, +) core.HandlerFunc { + var sampleN, sampleSum float32 + var ts uint32 + + return func(packet *rtp.Packet) { + samples := len(packet.Payload) + newLen := uint32((float32(samples) + sampleN) / n) + + oldSamples := packet.Payload + newSamples := make([]byte, newLen) + + var i int + for _, sample := range oldSamples { + sampleSum += float32(toPCM(sample)) + if sampleN++; sampleN >= n { + newSamples[i] = fromPCM(int16(sampleSum / n)) + i++ + + sampleSum = 0 + sampleN -= n + } + } + + ts += newLen + + clone := *packet + clone.Payload = newSamples + clone.Timestamp = ts + handler(&clone) + } +} + +func ResamplePCM(fromPCM func(int16) byte, handler core.HandlerFunc) core.HandlerFunc { + var ts uint32 + + return func(packet *rtp.Packet) { + len1 := len(packet.Payload) + len2 := len1 / 2 + + oldSamples := packet.Payload + newSamples := make([]byte, len2) + + var i2 int + for i1 := 0; i1 < len1; i1 += 2 { + sample := int16(uint16(oldSamples[i1])<<8 | uint16(oldSamples[i1+1])) + newSamples[i2] = fromPCM(sample) + i2++ + } + + ts += uint32(len2) + + clone := *packet + clone.Payload = newSamples + clone.Timestamp = ts + handler(&clone) + } +} + +func DownsamplePCM(fromPCM func(int16) byte, n float32, handler core.HandlerFunc) core.HandlerFunc { + var sampleN, sampleSum float32 + var ts uint32 + + return func(packet *rtp.Packet) { + samples := len(packet.Payload) / 2 + newLen := uint32((float32(samples) + sampleN) / n) + + oldSamples := packet.Payload + newSamples := make([]byte, newLen) + + var i2 int + for i1 := 0; i1 < len(packet.Payload); i1 += 2 { + sampleSum += float32(int16(uint16(oldSamples[i1])<<8 | uint16(oldSamples[i1+1]))) + if sampleN++; sampleN >= n { + newSamples[i2] = fromPCM(int16(sampleSum / n)) + i2++ + + sampleSum = 0 + sampleN -= n + } + } + + ts += newLen + + clone := *packet + clone.Payload = newSamples + clone.Timestamp = ts + handler(&clone) + } +} diff --git a/pkg/webrtc/consumer.go b/pkg/webrtc/consumer.go index 0a278924..b25cb7e3 100644 --- a/pkg/webrtc/consumer.go +++ b/pkg/webrtc/consumer.go @@ -5,11 +5,12 @@ import ( "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/h264" "github.com/AlexxIT/go2rtc/pkg/h265" + "github.com/AlexxIT/go2rtc/pkg/pcm" "github.com/pion/rtp" ) func (c *Conn) GetMedias() []*core.Media { - return c.medias + return WithResampling(c.medias) } func (c *Conn) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error { @@ -31,15 +32,16 @@ func (c *Conn) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiv } localTrack := c.getTranseiver(media.ID).Sender().Track().(*Track) + payloadType := codec.PayloadType - sender := core.NewSender(media, track.Codec) + sender := core.NewSender(media, codec) sender.Handler = func(packet *rtp.Packet) { c.send += packet.MarshalSize() //important to send with remote PayloadType - _ = localTrack.WriteRTP(codec.PayloadType, packet) + _ = localTrack.WriteRTP(payloadType, packet) } - switch codec.Name { + switch track.Codec.Name { case core.CodecH264: sender.Handler = h264.RTPPay(1200, sender.Handler) if track.Codec.IsRTP() { @@ -55,6 +57,15 @@ func (c *Conn) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiv if track.Codec.IsRTP() { sender.Handler = h265.RTPDepay(track.Codec, sender.Handler) } + + case core.CodecPCMA, core.CodecPCMU, core.CodecPCM: + if codec.ClockRate == 0 { + if codec.Name == core.CodecPCM { + codec.Name = core.CodecPCMA + } + codec.ClockRate = 8000 + sender.Handler = pcm.Resample(track.Codec, 8000, sender.Handler) + } } sender.HandleRTP(track) diff --git a/pkg/webrtc/helpers.go b/pkg/webrtc/helpers.go index b6e36ee6..b92e72ee 100644 --- a/pkg/webrtc/helpers.go +++ b/pkg/webrtc/helpers.go @@ -52,6 +52,53 @@ func UnmarshalMedias(descriptions []*sdp.MediaDescription) (medias []*core.Media return } +func WithResampling(medias []*core.Media) []*core.Media { + for _, media := range medias { + if media.Kind != core.KindAudio || media.Direction != core.DirectionSendonly { + continue + } + + var pcma, pcmu, pcm *core.Codec + + for _, codec := range media.Codecs { + switch codec.Name { + case core.CodecPCMA: + if codec.ClockRate != 0 { + pcma = codec + } else { + pcma = nil + } + case core.CodecPCMU: + if codec.ClockRate != 0 { + pcmu = codec + } else { + pcmu = nil + } + case core.CodecPCM: + pcm = codec + } + } + + if pcma != nil { + pcma = pcma.Clone() + pcma.ClockRate = 0 // reset clock rate so will match any + media.Codecs = append(media.Codecs, pcma) + } + if pcmu != nil { + pcmu = pcmu.Clone() + pcmu.ClockRate = 0 + media.Codecs = append(media.Codecs, pcmu) + } + if pcma != nil && pcm == nil { + pcm = pcma.Clone() + pcm.Name = core.CodecPCM + media.Codecs = append(media.Codecs, pcm) + } + } + + return medias +} + func NewCandidate(network, address string) (string, error) { i := strings.LastIndexByte(address, ':') if i < 0 { From 5926c1deb913b72c1469b99cedcfcedc9bdf8635 Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Sat, 22 Apr 2023 18:15:20 +0300 Subject: [PATCH 30/80] Fix default sample rate for MP3 codec --- pkg/core/codec.go | 2 +- pkg/iso/codecs.go | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/core/codec.go b/pkg/core/codec.go index 61ea74da..50fc58ea 100644 --- a/pkg/core/codec.go +++ b/pkg/core/codec.go @@ -108,7 +108,7 @@ func UnmarshalCodec(md *sdp.MediaDescription, payloadType string) *Codec { c.ClockRate = 44100 case "14": c.Name = CodecMP3 - c.ClockRate = 44100 + c.ClockRate = 90000 // it's not real sample rate case "26": c.Name = CodecJPEG c.ClockRate = 90000 diff --git a/pkg/iso/codecs.go b/pkg/iso/codecs.go index 1ddd28f3..9f11428b 100644 --- a/pkg/iso/codecs.go +++ b/pkg/iso/codecs.go @@ -118,6 +118,7 @@ func (m *Movie) WriteEsdsAAC(conf []byte) { m.Skip(2) // es id m.Skip(1) // es flags + // https://learn.microsoft.com/en-us/windows/win32/medfound/mpeg-4-file-sink#aac-audio m.WriteBytes(4, 0x80, 0x80, 0x80, size4+header+size5) m.WriteBytes(0x40) // object id m.WriteBytes(0x15) // stream type @@ -151,6 +152,7 @@ func (m *Movie) WriteEsdsMP3() { m.Skip(2) // es id m.Skip(1) // es flags + // https://learn.microsoft.com/en-us/windows/win32/medfound/mpeg-4-file-sink#mp3-audio m.WriteBytes(4, 0x80, 0x80, 0x80, size4) m.WriteBytes(0x6B) // object id m.WriteBytes(0x15) // stream type From e8b22bca99e30471423c9c03d1ac0838853c6049 Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Wed, 19 Apr 2023 12:35:02 +0300 Subject: [PATCH 31/80] Fix RTSP server close (panic) without client request #364 --- pkg/rtsp/client.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/rtsp/client.go b/pkg/rtsp/client.go index 68e06d10..bf304f88 100644 --- a/pkg/rtsp/client.go +++ b/pkg/rtsp/client.go @@ -288,6 +288,8 @@ func (c *Conn) Teardown() (err error) { } func (c *Conn) Close() error { - _ = c.Teardown() + if c.mode == core.ModeActiveProducer { + _ = c.Teardown() + } return c.conn.Close() } From 5fe07aeea084c937f777eb4aa9079a4b26874427 Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Wed, 19 Apr 2023 16:33:09 +0300 Subject: [PATCH 32/80] Fix FLV to RTSP transport after v1.3 #362 --- pkg/rtsp/consumer.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pkg/rtsp/consumer.go b/pkg/rtsp/consumer.go index 78182f97..7f5f1d1a 100644 --- a/pkg/rtsp/consumer.go +++ b/pkg/rtsp/consumer.go @@ -58,21 +58,22 @@ func (c *Conn) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiv // save original codec to sender (can have Codec.Name = ANY) sender := core.NewSender(media, codec) - sender.Handler = c.packetWriter(codec, channel) + // important to send original codec for valid IsRTP check + sender.Handler = c.packetWriter(track.Codec, channel, codec.PayloadType) sender.HandleRTP(track) c.senders = append(c.senders, sender) return nil } -func (c *Conn) packetWriter(codec *core.Codec, channel uint8) core.HandlerFunc { +func (c *Conn) packetWriter(codec *core.Codec, channel, payloadType uint8) core.HandlerFunc { handlerFunc := func(packet *rtp.Packet) { if c.state == StateNone { return } clone := *packet - clone.Header.PayloadType = codec.PayloadType + clone.Header.PayloadType = payloadType size := clone.MarshalSize() From 55fdf1a6472f984452a847dd21e6468318d3eaf0 Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Wed, 19 Apr 2023 17:09:02 +0300 Subject: [PATCH 33/80] Fix RTSP server handler for some Cloud clients #347 --- pkg/core/media.go | 4 ++++ pkg/rtsp/server.go | 5 +++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/pkg/core/media.go b/pkg/core/media.go index 8c15b5b5..5d73dc6b 100644 --- a/pkg/core/media.go +++ b/pkg/core/media.go @@ -136,6 +136,10 @@ func MarshalSDP(name string, medias []*Media) ([]byte, error) { } md.WithCodec(codec.PayloadType, name, codec.ClockRate, codec.Channels, codec.FmtpLine) + if media.ID != "" { + md.WithValueAttribute("control", media.ID) + } + sd.MediaDescriptions = append(sd.MediaDescriptions, md) } diff --git a/pkg/rtsp/server.go b/pkg/rtsp/server.go index ce6658b6..74aefe37 100644 --- a/pkg/rtsp/server.go +++ b/pkg/rtsp/server.go @@ -40,8 +40,9 @@ func (c *Conn) Accept() error { if !c.auth.Validate(req) { res := &tcp.Response{ - Status: "401 Unauthorized", - Header: map[string][]string{"Www-Authenticate": {`Basic realm="go2rtc"`}}, + Status: "401 Unauthorized", + Header: map[string][]string{"Www-Authenticate": {`Basic realm="go2rtc"`}}, + Request: req, } if err = c.WriteResponse(res); err != nil { return err From 9268acf1cac4327f8b6fc2d1c9922494b11565cc Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Sun, 23 Apr 2023 08:08:16 +0300 Subject: [PATCH 34/80] Update version to 1.4.0 --- cmd/app/app.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/app/app.go b/cmd/app/app.go index 6bbab359..c7cf5c2a 100644 --- a/cmd/app/app.go +++ b/cmd/app/app.go @@ -16,7 +16,7 @@ import ( "gopkg.in/yaml.v3" ) -var Version = "1.3.2" +var Version = "1.4.0" var UserAgent = "go2rtc/" + Version var ConfigPath string From 2610f15eb610df8040659628e6351904b0fdabc6 Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Sun, 23 Apr 2023 08:08:52 +0300 Subject: [PATCH 35/80] Update readme about codecs --- README.md | 73 +++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 57 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 6af79581..ab020cb7 100644 --- a/README.md +++ b/README.md @@ -803,8 +803,8 @@ Provides several features: API examples: -- MP4 stream: `http://192.168.1.123:1984/api/stream.mp4?src=camera1` -- MP4 snapshot: `http://192.168.1.123:1984/api/frame.mp4?src=camera1` +- MP4 snapshot: `http://192.168.1.123:1984/api/frame.mp4?src=camera1` (H264, H265) +- MP4 stream: `http://192.168.1.123:1984/api/stream.mp4?src=camera1` (H264, H265, AAC) Read more about [codecs filters](#codecs-filters). @@ -895,7 +895,7 @@ But it cannot be done for [RTSP](#module-rtsp), [HTTP progressive streaming](#mo Without filters: -- RTSP will provide only the first video and only the first audio +- RTSP will provide only the first video and only the first audio (any codec) - MP4 will include only compatible codecs (H264, H265, AAC) - HLS will output in the legacy TS format (H264 without audio) @@ -906,23 +906,25 @@ Some examples: - `rtsp://192.168.1.123:8554/camera1?video=h264&audio=aac&audio=opus` - H264 video codec and two separate audio tracks - `rtsp://192.168.1.123:8554/camera1?video&audio=all` - any video codec and all audio codecs as separate tracks - `http://192.168.1.123:1984/api/stream.m3u8?src=camera1&mp4` - HLS stream with MP4 compatible codecs (HLS/fMP4) -- `http://192.168.1.123:1984/api/stream.mp4?src=camera1&video=h264,h265&audio=aac,opus,mp3,pcma,pcmu` - MP4 file with non standard audio codecs, does not work in some players +- `http://192.168.1.123:1984/api/stream.m3u8?src=camera1&mp4=flac` - HLS stream with PCMA/PCMU/PCM audio support (HLS/fMP4), won't work on old devices +- `http://192.168.1.123:1984/api/stream.mp4?src=camera1&mp4=flac` - MP4 file with PCMA/PCMU/PCM audio support, won't work on old devices (ex. iOS 12) +- `http://192.168.1.123:1984/api/stream.mp4?src=camera1&mp4=all` - MP4 file with non standard audio codecs, won't work on some players ## Codecs madness `AVC/H.264` video can be played almost anywhere. But `HEVC/H.265` has a lot of limitations in supporting with different devices and browsers. It's all about patents and money, you can't do anything about it. -| Device | WebRTC | MSE | HTTP Progressive Streaming | -|---------------------|-------------------------------|------------------------|-----------------------------------------| -| *latency* | best | medium | bad | -| Desktop Chrome 107+ | H264, OPUS, PCMU, PCMA | H264, H265*, AAC, OPUS | H264, H265*, AAC, OPUS, PCMU, PCMA, MP3 | -| Desktop Edge | H264, OPUS, PCMU, PCMA | H264, H265*, AAC, OPUS | H264, H265*, AAC, OPUS, PCMU, PCMA, MP3 | -| Desktop Safari | H264, H265*, OPUS, PCMU, PCMA | H264, H265, AAC | **no!** | -| Desktop Firefox | H264, OPUS, PCMU, PCMA | H264, AAC, OPUS | H264, AAC, OPUS | -| Android Chrome 107+ | H264, OPUS, PCMU, PCMA | H264, H265*, AAC, OPUS | H264, ?, AAC, OPUS, PCMU, PCMA, MP3 | -| iPad Safari 13+ | H264, H265*, OPUS, PCMU, PCMA | H264, H265, AAC | **no!** | -| iPhone Safari 13+ | H264, H265*, OPUS, PCMU, PCMA | **no!** | **no!** | -| masOS Hass App | no | no | no | +| Device | WebRTC | MSE | HTTP Progressive Streaming | +|---------------------|-------------------------------|-------------------------------|------------------------------------| +| *latency* | best | medium | bad | +| Desktop Chrome 107+ | H264, OPUS, PCMU, PCMA | H264, H265*, AAC, FLAC*, OPUS | H264, H265*, AAC, FLAC*, OPUS, MP3 | +| Desktop Edge | H264, OPUS, PCMU, PCMA | H264, H265*, AAC, FLAC*, OPUS | H264, H265*, AAC, FLAC*, OPUS, MP3 | +| Android Chrome 107+ | H264, OPUS, PCMU, PCMA | H264, H265*, AAC, FLAC*, OPUS | H264, H265*, AAC, FLAC*, OPUS, MP3 | +| Desktop Firefox | H264, OPUS, PCMU, PCMA | H264, AAC, FLAC*, OPUS | H264, AAC, FLAC*, OPUS | +| Desktop Safari | H264, H265*, OPUS, PCMU, PCMA | H264, H265, AAC, FLAC* | **no!** | +| iPad Safari 13+ | H264, H265*, OPUS, PCMU, PCMA | H264, H265, AAC, FLAC* | **no!** | +| iPhone Safari 13+ | H264, H265*, OPUS, PCMU, PCMA | **no!** | **no!** | +| masOS Hass App | no | no | no | - Chrome H265: [read this](https://chromestatus.com/feature/5186511939567616) and [read this](https://github.com/StaZhu/enable-chromium-hevc-hardware-decoding) - Edge H265: [read this](https://www.reddit.com/r/MicrosoftEdge/comments/v9iw8k/enable_hevc_support_in_edge/) @@ -931,9 +933,9 @@ Some examples: **Audio** +- Go2rtc support [automatic repack](#built-in-transcoding) `PCMA/PCMU/PCM` codecs to `FLAC` for MSE/MP4/HLS so they will work almost anywhere - **WebRTC** audio codecs: `PCMU/8000`, `PCMA/8000`, `OPUS/48000/2` - `OPUS` and `MP3` inside **MP4** is part of the standard, but some players do not support them anyway (especially Apple) -- `PCMU` and `PCMA` inside **MP4** isn't a standard, but some players support them, for example Chromium browsers **Apple devices** @@ -941,6 +943,45 @@ Some examples: - iPhones don't support MSE technology because it competes with the HTTP Live Streaming (HLS) technology, invented by Apple - HLS is the worst technology for **live** streaming, it still exists only because of iPhones +**Codec names** + +- H264 = H.264 = AVC (Advanced Video Coding) +- H265 = H.265 = HEVC (High Efficiency Video Coding) +- PCMU = G.711 PCM (A-law) = PCM A-law (`alaw`) +- PCMA = G.711 PCM (µ-law) = PCM mu-law (`mulaw`) +- PCM = L16 = PCM signed 16-bit big-endian (`s16be`) +- AAC = MPEG4-GENERIC +- MP3 = MPEG-1 Audio Layer III or MPEG-2 Audio Layer III + +## Built-in transcoding + +There are no plans to embed complex transcoding algorithms inside go2rtc. [FFmpeg source](#source-ffmpeg) does a great job with this. Including [hardware acceleration](https://github.com/AlexxIT/go2rtc/wiki/Hardware-acceleration) support. + +But go2rtc has some simple algorithms. They are turned on automatically, you do not need to set them up additionally. + +**PCM for MSE/MP4/HLS** + +Go2rtc can pack `PCMA`, `PCMU` and `PCM` codecs into an MP4 container so that they work in all browsers and all built-in players on modern devices. Including Apple QuickTime: + +``` +PCMA/PCMU => PCM => FLAC => MSE/MP4/HLS +``` + +**Resample PCMA/PCMU for WebRTC** + +By default WebRTC support only `PCMA/8000` and `PCMU/8000`. But go2rtc can automatically resample PCMA and PCMU codec with with a different sample rate. Also go2rtc can transcode `PCM` codec to `PCMA/8000`, so WebRTC can play it: + +``` +PCM/xxx => PCMA/8000 => WebRTC +PCMA/xxx => PCMA/8000 => WebRTC +PCMU/xxx => PCMU/8000 => WebRTC +``` + +**Important** + +- FLAC codec not supported in a RTSP stream. If you using Frigate or Hass for recording MP4 files with PCMA/PCMU/PCM audio - you should setup transcoding to AAC codec. +- PCMA and PCMU are VERY low quality codecs. Them support only 256! different sounds. Use them only when you have no other options. + ## Codecs negotiation For example, you want to watch RTSP-stream from [Dahua IPC-K42](https://www.dahuasecurity.com/fr/products/All-Products/Network-Cameras/Wireless-Series/Wi-Fi-Series/4MP/IPC-K42) camera in your Chrome browser. From 63d9c6c2b7dff2f7a4979a2c9e8df81aa94eaf54 Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Sun, 23 Apr 2023 20:42:49 +0300 Subject: [PATCH 36/80] Fix Chinese cameras with wrong Session header after v1.4.0 #382 --- pkg/rtsp/client.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pkg/rtsp/client.go b/pkg/rtsp/client.go index bf304f88..241d7f73 100644 --- a/pkg/rtsp/client.go +++ b/pkg/rtsp/client.go @@ -246,9 +246,11 @@ func (c *Conn) SetupMedia(media *core.Media) (byte, error) { if c.session == "" { // Session: 216525287999;timeout=60 if s := res.Header.Get("Session"); s != "" { - c.session, s, _ = strings.Cut(s, ";timeout=") - if s != "" { - c.keepalive, _ = strconv.Atoi(s) + if i := strings.IndexByte(s, ';'); i > 0 { + c.session = s[:i] + } + if i := strings.Index(s, "timeout="); i > 0 { + c.keepalive, _ = strconv.Atoi(s[i+8:]) } } } From 813c8b3b3de9b28c8221a4a9607517dac63b4512 Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Mon, 24 Apr 2023 06:39:07 +0300 Subject: [PATCH 37/80] Make core atoi func public --- pkg/core/codec.go | 9 ++------- pkg/core/helpers.go | 5 +++++ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pkg/core/codec.go b/pkg/core/codec.go index 50fc58ea..5f346739 100644 --- a/pkg/core/codec.go +++ b/pkg/core/codec.go @@ -68,7 +68,7 @@ func (c *Codec) Match(remote *Codec) bool { } func UnmarshalCodec(md *sdp.MediaDescription, payloadType string) *Codec { - c := &Codec{PayloadType: byte(atoi(payloadType))} + c := &Codec{PayloadType: byte(Atoi(payloadType))} for _, attr := range md.Attributes { switch { @@ -78,7 +78,7 @@ func UnmarshalCodec(md *sdp.MediaDescription, payloadType string) *Codec { c.Name = strings.ToUpper(ss[0]) // fix tailing space: `a=rtpmap:96 H264/90000 ` - c.ClockRate = uint32(atoi(strings.TrimRightFunc(ss[1], unicode.IsSpace))) + c.ClockRate = uint32(Atoi(strings.TrimRightFunc(ss[1], unicode.IsSpace))) if len(ss) == 3 && ss[2] == "2" { c.Channels = 2 @@ -120,11 +120,6 @@ func UnmarshalCodec(md *sdp.MediaDescription, payloadType string) *Codec { return c } -func atoi(s string) (i int) { - i, _ = strconv.Atoi(s) - return -} - func DecodeH264(fmtp string) string { if ps := Between(fmtp, "sprop-parameter-sets=", ","); ps != "" { if sps, _ := base64.StdEncoding.DecodeString(ps); len(sps) >= 4 { diff --git a/pkg/core/helpers.go b/pkg/core/helpers.go index 060894df..44d24953 100644 --- a/pkg/core/helpers.go +++ b/pkg/core/helpers.go @@ -41,6 +41,11 @@ func Between(s, sub1, sub2 string) string { return s } +func Atoi(s string) (i int) { + i, _ = strconv.Atoi(s) + return +} + func Assert(ok bool) { if !ok { _, file, line, _ := runtime.Caller(1) From a20de73ab2dc6c7f7fc0a149053f6a7480b0bb8e Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Mon, 24 Apr 2023 06:40:11 +0300 Subject: [PATCH 38/80] Add pkt_size option fort RTSP server --- cmd/rtsp/rtsp.go | 9 ++++++++- pkg/h264/rtp.go | 4 ++++ pkg/h265/rtp.go | 4 ++++ pkg/rtsp/conn.go | 1 + pkg/rtsp/consumer.go | 13 +++++++++++-- pkg/webrtc/api.go | 1 + 6 files changed, 29 insertions(+), 3 deletions(-) diff --git a/cmd/rtsp/rtsp.go b/cmd/rtsp/rtsp.go index 31ab985b..93306d88 100644 --- a/cmd/rtsp/rtsp.go +++ b/cmd/rtsp/rtsp.go @@ -22,6 +22,7 @@ func Init() { Username string `yaml:"username" json:"-"` Password string `yaml:"password" json:"-"` DefaultQuery string `yaml:"default_query" json:"default_query"` + PacketSize uint16 `yaml:"pkt_size"` } `yaml:"rtsp"` } @@ -67,6 +68,7 @@ func Init() { } c := rtsp.NewServer(conn) + c.PacketSize = conf.Mod.PacketSize // skip check auth for localhost if conf.Mod.Username != "" && !conn.RemoteAddr().(*net.TCPAddr).IP.IsLoopback() { c.Auth(conf.Mod.Username, conf.Mod.Password) @@ -174,13 +176,18 @@ func tcpHandler(conn *rtsp.Conn) { conn.SessionName = app.UserAgent - conn.Medias = mp4.ParseQuery(conn.URL.Query()) + query := conn.URL.Query() + conn.Medias = mp4.ParseQuery(query) if conn.Medias == nil { for _, media := range defaultMedias { conn.Medias = append(conn.Medias, media.Clone()) } } + if s := query.Get("pkt_size"); s != "" { + conn.PacketSize = uint16(core.Atoi(s)) + } + if err := stream.AddConsumer(conn); err != nil { log.Warn().Err(err).Str("stream", name).Msg("[rtsp]") return diff --git a/pkg/h264/rtp.go b/pkg/h264/rtp.go index 8cccb637..a1bc93ea 100644 --- a/pkg/h264/rtp.go +++ b/pkg/h264/rtp.go @@ -94,6 +94,10 @@ func RTPDepay(codec *core.Codec, handler core.HandlerFunc) core.HandlerFunc { } func RTPPay(mtu uint16, handler core.HandlerFunc) core.HandlerFunc { + if mtu == 0 { + mtu = 1472 + } + payloader := &Payloader{IsAVC: true} sequencer := rtp.NewRandomSequencer() mtu -= 12 // rtp.Header size diff --git a/pkg/h265/rtp.go b/pkg/h265/rtp.go index 333ca6d4..3e027b83 100644 --- a/pkg/h265/rtp.go +++ b/pkg/h265/rtp.go @@ -76,6 +76,10 @@ func RTPDepay(codec *core.Codec, handler core.HandlerFunc) core.HandlerFunc { } func RTPPay(mtu uint16, handler core.HandlerFunc) core.HandlerFunc { + if mtu == 0 { + mtu = 1472 + } + payloader := &Payloader{} sequencer := rtp.NewRandomSequencer() mtu -= 12 // rtp.Header size diff --git a/pkg/rtsp/conn.go b/pkg/rtsp/conn.go index 1a00ac8b..2bdb91bb 100644 --- a/pkg/rtsp/conn.go +++ b/pkg/rtsp/conn.go @@ -22,6 +22,7 @@ type Conn struct { // public Backchannel bool + PacketSize uint16 SessionName string Medias []*core.Media diff --git a/pkg/rtsp/consumer.go b/pkg/rtsp/consumer.go index 7f5f1d1a..38125147 100644 --- a/pkg/rtsp/consumer.go +++ b/pkg/rtsp/consumer.go @@ -104,14 +104,23 @@ func (c *Conn) packetWriter(codec *core.Codec, channel, payloadType uint8) core. if !codec.IsRTP() { switch codec.Name { case core.CodecH264: - handlerFunc = h264.RTPPay(1500, handlerFunc) + handlerFunc = h264.RTPPay(c.PacketSize, handlerFunc) case core.CodecH265: - handlerFunc = h265.RTPPay(1500, handlerFunc) + handlerFunc = h265.RTPPay(c.PacketSize, handlerFunc) case core.CodecAAC: handlerFunc = aac.RTPPay(handlerFunc) case core.CodecJPEG: handlerFunc = mjpeg.RTPPay(handlerFunc) } + } else if c.PacketSize != 0 { + switch codec.Name { + case core.CodecH264: + handlerFunc = h264.RTPPay(c.PacketSize, handlerFunc) + handlerFunc = h264.RTPDepay(codec, handlerFunc) + case core.CodecH265: + handlerFunc = h265.RTPPay(c.PacketSize, handlerFunc) + handlerFunc = h265.RTPDepay(codec, handlerFunc) + } } return handlerFunc diff --git a/pkg/webrtc/api.go b/pkg/webrtc/api.go index 8a5c7668..5086109d 100644 --- a/pkg/webrtc/api.go +++ b/pkg/webrtc/api.go @@ -9,6 +9,7 @@ import ( ) // ReceiveMTU = Ethernet MTU (1500) - IP Header (20) - UDP Header (8) +// https://ffmpeg.org/ffmpeg-all.html#Muxer const ReceiveMTU = 1472 func NewAPI(address string) (*webrtc.API, error) { From 6247746177356030e2cd3660132294f58074d648 Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Tue, 25 Apr 2023 06:21:50 +0300 Subject: [PATCH 39/80] Change localhost to 127.0.0.1 --- cmd/exec/exec.go | 2 +- cmd/ffmpeg/ffmpeg.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/exec/exec.go b/cmd/exec/exec.go index 20d2fc14..547d6a95 100644 --- a/cmd/exec/exec.go +++ b/cmd/exec/exec.go @@ -53,7 +53,7 @@ func Handle(url string) (core.Producer, error) { path := "/" + hex.EncodeToString(sum[:]) url = strings.Replace( - url, "{output}", "rtsp://localhost:"+rtsp.Port+path, 1, + url, "{output}", "rtsp://127.0.0.1:"+rtsp.Port+path, 1, ) // remove `exec:` diff --git a/cmd/ffmpeg/ffmpeg.go b/cmd/ffmpeg/ffmpeg.go index d5dfe988..89a49537 100644 --- a/cmd/ffmpeg/ffmpeg.go +++ b/cmd/ffmpeg/ffmpeg.go @@ -157,7 +157,7 @@ func parseArgs(s string) *Args { args.input = "-i " + s } } else if streams.Get(s) != nil { - s = "rtsp://localhost:" + rtsp.Port + "/" + s + s = "rtsp://127.0.0.1:" + rtsp.Port + "/" + s switch { case args.video > 0 && args.audio == 0: s += "?video" From f0893bd78b394cef8bea879d5c40dc29b66eb00d Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Thu, 27 Apr 2023 14:02:55 +0300 Subject: [PATCH 40/80] Fix bug in SDP from Annke CZ400 #384 --- pkg/mp4/muxer.go | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/pkg/mp4/muxer.go b/pkg/mp4/muxer.go index 485d8fc6..b1ec37f4 100644 --- a/pkg/mp4/muxer.go +++ b/pkg/mp4/muxer.go @@ -64,9 +64,11 @@ func (m *Muxer) GetInit(codecs []*core.Codec) ([]byte, error) { switch codec.Name { case core.CodecH264: sps, pps := h264.GetParameterSet(codec.FmtpLine) - if sps == nil { - // some dummy SPS and PPS not a problem + // some dummy SPS and PPS not a problem + if len(sps) == 0 { sps = []byte{0x67, 0x42, 0x00, 0x0a, 0xf8, 0x41, 0xa2} + } + if len(pps) == 0 { pps = []byte{0x68, 0xce, 0x38, 0x80} } @@ -83,10 +85,14 @@ func (m *Muxer) GetInit(codecs []*core.Codec) ([]byte, error) { case core.CodecH265: vps, sps, pps := h265.GetParameterSet(codec.FmtpLine) - if sps == nil { - // some dummy SPS and PPS not a problem + // some dummy SPS and PPS not a problem + if len(vps) == 0 { vps = []byte{0x40, 0x01, 0x0c, 0x01, 0xff, 0xff, 0x01, 0x40, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x99, 0xac, 0x09} + } + if len(sps) == 0 { sps = []byte{0x42, 0x01, 0x01, 0x01, 0x40, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x99, 0xa0, 0x01, 0x40, 0x20, 0x05, 0xa1, 0xfe, 0x5a, 0xee, 0x46, 0xc1, 0xae, 0x55, 0x04} + } + if len(pps) == 0 { pps = []byte{0x44, 0x01, 0xc0, 0x73, 0xc0, 0x4c, 0x90} } From 8dbaa4ba93e662f13c1844b56d07e0711577913e Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Sat, 29 Apr 2023 13:48:17 +0300 Subject: [PATCH 41/80] Fix RTSP client Session processing --- pkg/rtsp/client.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pkg/rtsp/client.go b/pkg/rtsp/client.go index 241d7f73..b5f3db6b 100644 --- a/pkg/rtsp/client.go +++ b/pkg/rtsp/client.go @@ -244,13 +244,16 @@ func (c *Conn) SetupMedia(media *core.Media) (byte, error) { } if c.session == "" { + // Session: 7116520596809429228 // Session: 216525287999;timeout=60 if s := res.Header.Get("Session"); s != "" { if i := strings.IndexByte(s, ';'); i > 0 { c.session = s[:i] - } - if i := strings.Index(s, "timeout="); i > 0 { - c.keepalive, _ = strconv.Atoi(s[i+8:]) + if i = strings.Index(s, "timeout="); i > 0 { + c.keepalive, _ = strconv.Atoi(s[i+8:]) + } + } else { + c.session = s } } } From 1e14dc9ab29a3a56dd5776a6c229ba24479656fc Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Sat, 29 Apr 2023 13:58:10 +0300 Subject: [PATCH 42/80] Add ONVIF client and server support --- cmd/hass/hass.go | 19 ++++ cmd/onvif/init.go | 173 ++++++++++++++++++++++++++++++++++ cmd/streams/init.go | 7 ++ main.go | 2 + pkg/onvif/client.go | 217 +++++++++++++++++++++++++++++++++++++++++++ pkg/onvif/helpers.go | 99 ++++++++++++++++++++ pkg/onvif/server.go | 204 ++++++++++++++++++++++++++++++++++++++++ www/add.html | 25 +++++ 8 files changed, 746 insertions(+) create mode 100644 cmd/onvif/init.go create mode 100644 pkg/onvif/client.go create mode 100644 pkg/onvif/helpers.go create mode 100644 pkg/onvif/server.go diff --git a/cmd/hass/hass.go b/cmd/hass/hass.go index ddbfe995..32d0771e 100644 --- a/cmd/hass/hass.go +++ b/cmd/hass/hass.go @@ -131,6 +131,25 @@ func importEntries(config string) map[string]string { case "roborock": _ = json.Unmarshal(entrie.Data, &roborock.Auth) + case "onvif": + var data struct { + Host string `json:"host" json:"host"` + Port uint16 `json:"port" json:"port"` + Username string `json:"username" json:"username"` + Password string `json:"password" json:"password"` + } + if err = json.Unmarshal(entrie.Data, &data); err != nil { + continue + } + + if data.Username != "" && data.Password != "" { + urls[entrie.Title] = fmt.Sprintf( + "onvif://%s:%s@%s:%d", data.Username, data.Password, data.Host, data.Port, + ) + } else { + urls[entrie.Title] = fmt.Sprintf("onvif://%s:%d", data.Host, data.Port) + } + default: continue } diff --git a/cmd/onvif/init.go b/cmd/onvif/init.go new file mode 100644 index 00000000..928b77d4 --- /dev/null +++ b/cmd/onvif/init.go @@ -0,0 +1,173 @@ +package onvif + +import ( + "github.com/AlexxIT/go2rtc/cmd/api" + "github.com/AlexxIT/go2rtc/cmd/app" + "github.com/AlexxIT/go2rtc/cmd/rtsp" + "github.com/AlexxIT/go2rtc/cmd/streams" + "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/AlexxIT/go2rtc/pkg/onvif" + "github.com/rs/zerolog" + "io" + "net" + "net/http" + "os" + "strconv" + "time" +) + +func Init() { + log = app.GetLogger("onvif") + + streams.HandleFunc("onvif", streamOnvif) + + // ONVIF server on all suburls + api.HandleFunc("/onvif/", onvifDeviceService) + + // ONVIF client autodiscovery + api.HandleFunc("api/onvif", apiOnvif) +} + +var log zerolog.Logger + +func streamOnvif(rawURL string) (core.Producer, error) { + client, err := onvif.NewClient(rawURL) + if err != nil { + return nil, err + } + + uri, err := client.GetURI() + if err != nil { + return nil, err + } + + log.Debug().Msgf("[onvif] new uri=%s", uri) + + return streams.GetProducer(uri) +} + +func onvifDeviceService(w http.ResponseWriter, r *http.Request) { + b, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + action := onvif.GetRequestAction(b) + if action == "" { + http.Error(w, "malformed request body", http.StatusBadRequest) + return + } + + log.Trace().Msgf("[onvif] %s", action) + + var res string + + switch action { + case onvif.ActionGetCapabilities: + // important for Hass: Media section + res = onvif.GetCapabilitiesResponse(r.Host) + + case onvif.ActionGetSystemDateAndTime: + // important for Hass + res = onvif.GetSystemDateAndTimeResponse() + + case onvif.ActionGetNetworkInterfaces: + // important for Hass: none + res = onvif.GetNetworkInterfacesResponse() + + case onvif.ActionGetDeviceInformation: + // important for Hass: SerialNumber (unique server ID) + res = onvif.GetDeviceInformationResponse("", "go2rtc", app.Version, r.Host) + + case onvif.ActionGetServiceCapabilities: + // important for Hass + res = onvif.GetServiceCapabilitiesResponse() + + case onvif.ActionSystemReboot: + res = onvif.SystemRebootResponse() + + time.AfterFunc(time.Second, func() { + os.Exit(0) + }) + + case onvif.ActionGetProfiles: + // important for Hass: H264 codec, width, height + res = onvif.GetProfilesResponse(streams.GetAll()) + + case onvif.ActionGetStreamUri: + host, _, err := net.SplitHostPort(r.Host) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + uri := "rtsp://" + host + ":" + rtsp.Port + "/" + onvif.FindTagValue(b, "ProfileToken") + res = onvif.GetStreamUriResponse(uri) + + default: + http.Error(w, "unsupported action", http.StatusBadRequest) + log.Debug().Msgf("[onvif] unsupported request:\n%s", b) + return + } + + w.Header().Set("Content-Type", "application/soap+xml; charset=utf-8") + if _, err = w.Write([]byte(res)); err != nil { + log.Error().Err(err).Caller().Send() + } +} + +func apiOnvif(w http.ResponseWriter, r *http.Request) { + src := r.URL.Query().Get("src") + + var items []api.Stream + + if src == "" { + hosts, err := onvif.DiscoveryStreamingHosts() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + for _, host := range hosts { + items = append(items, api.Stream{ + Name: host, + URL: "onvif://user:pass@" + host, + }) + } + } else { + client, err := onvif.NewClient(src) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + name, err := client.GetName() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + tokens, err := client.GetProfilesTokens() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + for i, token := range tokens { + items = append(items, api.Stream{ + Name: name + " stream" + strconv.Itoa(i), + URL: src + "?subtype=" + token, + }) + } + + if len(tokens) > 0 && client.HasSnapshots() { + items = append(items, api.Stream{ + Name: name + " snapshot", + URL: src + "?subtype=" + tokens[0] + "&snapshot", + }) + } + } + + api.ResponseStreams(w, items) +} diff --git a/cmd/streams/init.go b/cmd/streams/init.go index ca5ff58e..84b2f207 100644 --- a/cmd/streams/init.go +++ b/cmd/streams/init.go @@ -68,6 +68,13 @@ func GetOrNew(src string) *Stream { return New(src, src) } +func GetAll() (names []string) { + for name := range streams { + names = append(names, name) + } + return +} + func streamsHandler(w http.ResponseWriter, r *http.Request) { query := r.URL.Query() src := query.Get("src") diff --git a/main.go b/main.go index 4f91f4e6..6a50d407 100644 --- a/main.go +++ b/main.go @@ -18,6 +18,7 @@ import ( "github.com/AlexxIT/go2rtc/cmd/mp4" "github.com/AlexxIT/go2rtc/cmd/mpegts" "github.com/AlexxIT/go2rtc/cmd/ngrok" + "github.com/AlexxIT/go2rtc/cmd/onvif" "github.com/AlexxIT/go2rtc/cmd/roborock" "github.com/AlexxIT/go2rtc/cmd/rtmp" "github.com/AlexxIT/go2rtc/cmd/rtsp" @@ -36,6 +37,7 @@ func main() { app.Init() // init config and logs api.Init() // init HTTP API server streams.Init() // load streams list + onvif.Init() rtsp.Init() // add support RTSP client and RTSP server rtmp.Init() // add support RTMP client diff --git a/pkg/onvif/client.go b/pkg/onvif/client.go new file mode 100644 index 00000000..77157d0b --- /dev/null +++ b/pkg/onvif/client.go @@ -0,0 +1,217 @@ +package onvif + +import ( + "bytes" + "crypto/sha1" + "encoding/base64" + "errors" + "github.com/AlexxIT/go2rtc/pkg/core" + "html" + "io" + "net/http" + "net/url" + "regexp" + "strings" + "time" +) + +type Client struct { + url *url.URL +} + +func NewClient(rawURL string) (*Client, error) { + u, err := url.Parse(rawURL) + if err != nil { + return nil, err + } + return &Client{url: u}, nil +} + +func (c *Client) GetURI() (string, error) { + query := c.url.Query() + + token := query.Get("subtype") + + // support empty + if i := atoi(token); i >= 0 { + tokens, err := c.GetProfilesTokens() + if err != nil { + return "", err + } + if i >= len(tokens) { + return "", errors.New("wrong subtype") + } + token = tokens[i] + } + + getUri := c.GetStreamUri + if query.Has("snapshot") { + getUri = c.GetSnapshotUri + } + + b, err := getUri(token) + if err != nil { + return "", err + } + + uri := FindTagValue(b, "Uri") + uri = html.UnescapeString(uri) + + u, err := url.Parse(uri) + if err != nil { + return "", err + } + + if u.User == nil && c.url.User != nil { + u.User = c.url.User + } + + return u.String(), nil +} + +func (c *Client) GetName() (string, error) { + b, err := c.GetDeviceInformation() + if err != nil { + return "", err + } + + return FindTagValue(b, "Manufacturer") + " " + FindTagValue(b, "Model"), nil +} + +func (c *Client) GetProfilesTokens() ([]string, error) { + b, err := c.GetProfiles() + if err != nil { + return nil, err + } + + var tokens []string + + re := regexp.MustCompile(`Profiles.+?token="([^"]+)`) + for _, s := range re.FindAllStringSubmatch(string(b), 10) { + tokens = append(tokens, s[1]) + } + + return tokens, nil +} + +func (c *Client) HasSnapshots() bool { + b, err := c.GetServiceCapabilities() + if err != nil { + return false + } + return strings.Contains(string(b), `SnapshotUri="true"`) +} + +func (c *Client) GetCapabilities() ([]byte, error) { + return c.Request( + ` + All +`, + ) +} + +func (c *Client) GetNetworkInterfaces() ([]byte, error) { + return c.Request(``) +} + +func (c *Client) GetDeviceInformation() ([]byte, error) { + return c.Request(``) +} + +func (c *Client) GetProfiles() ([]byte, error) { + return c.Request(``) +} + +func (c *Client) GetStreamUri(token string) ([]byte, error) { + return c.Request( + ` + + RTP-Unicast + RTSP + + ` + token + ` +`) +} + +func (c *Client) GetSnapshotUri(token string) ([]byte, error) { + return c.Request( + ` + ` + token + ` +`) +} + +func (c *Client) GetSystemDateAndTime() ([]byte, error) { + return c.Request( + ``, + ) +} + +func (c *Client) GetServiceCapabilities() ([]byte, error) { + return c.Request( + ``, + ) +} + +func (c *Client) SystemReboot() ([]byte, error) { + return c.Request( + ``, + ) +} + +func (c *Client) GetServices() ([]byte, error) { + return c.Request(` + true +`) +} + +func (c *Client) GetScopes() ([]byte, error) { + return c.Request(``) +} + +func (c *Client) Request(body string) ([]byte, error) { + 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 + ``) + + client := &http.Client{Timeout: time.Second * 5000} + res, err := client.Post( + "http://"+c.url.Host+"/onvif/", + `application/soap+xml;charset=utf-8`, + buf, + ) + if err != nil { + return nil, err + } + + // need to close body with eny response status + b, err := io.ReadAll(res.Body) + + if err == nil && res.StatusCode != http.StatusOK { + err = errors.New(res.Status) + } + + return b, err +} diff --git a/pkg/onvif/helpers.go b/pkg/onvif/helpers.go new file mode 100644 index 00000000..dd12e6bc --- /dev/null +++ b/pkg/onvif/helpers.go @@ -0,0 +1,99 @@ +package onvif + +import ( + "github.com/AlexxIT/go2rtc/pkg/core" + "net" + "net/url" + "regexp" + "strconv" + "time" +) + +func FindTagValue(b []byte, tag string) string { + re := regexp.MustCompile(tag + `[^>]*>([^<]+)`) + m := re.FindSubmatch(b) + if len(m) != 2 { + return "" + } + return string(m[1]) +} + +// UUID - generate something like 44302cbf-0d18-4feb-79b3-33b575263da3 +func UUID() string { + s := core.RandString(32, 16) + return s[:8] + "-" + s[8:12] + "-" + s[12:16] + "-" + s[16:20] + "-" + s[20:] +} + +func DiscoveryStreamingHosts() ([]string, error) { + conn, err := net.ListenPacket("udp4", ":0") + if err != nil { + return nil, err + } + + msg := ` + + + http://schemas.xmlsoap.org/ws/2005/04/discovery/Probe + uuid:` + UUID() + ` + urn:schemas-xmlsoap-org:ws:2005:04:discovery + + + + tds:Device + onvif://www.onvif.org/Profile/Streaming + + +` + + addr := &net.UDPAddr{ + IP: net.IP{239, 255, 255, 250}, + Port: 3702, + } + + if _, err = conn.WriteTo([]byte(msg), addr); err != nil { + return nil, err + } + + if err = conn.SetReadDeadline(time.Now().Add(time.Second * 3)); err != nil { + return nil, err + } + + var hosts []string + + b := make([]byte, 8192) + for { + n, _, err := conn.ReadFrom(b) + if err != nil { + break + } + + rawURL := FindTagValue(b[:n], "XAddrs") + if rawURL == "" { + continue + } + + u, err := url.Parse(rawURL) + if err != nil { + continue + } + + if u.Scheme != "http" { + continue + } + + hosts = append(hosts, u.Host) + } + + return hosts, nil +} + +func atoi(s string) int { + if s == "" { + return 0 + } + i, err := strconv.Atoi(s) + if err != nil { + return -1 + } + return i +} diff --git a/pkg/onvif/server.go b/pkg/onvif/server.go new file mode 100644 index 00000000..3003efd7 --- /dev/null +++ b/pkg/onvif/server.go @@ -0,0 +1,204 @@ +package onvif + +import ( + "bytes" + "fmt" + "regexp" + "strconv" + "time" +) + +const ( + ActionGetCapabilities = "GetCapabilities" + ActionGetSystemDateAndTime = "GetSystemDateAndTime" + ActionGetNetworkInterfaces = "GetNetworkInterfaces" + ActionGetDeviceInformation = "GetDeviceInformation" + ActionGetServiceCapabilities = "GetServiceCapabilities" + ActionGetProfiles = "GetProfiles" + ActionGetStreamUri = "GetStreamUri" + ActionSystemReboot = "SystemReboot" + + ActionGetServices = "GetServices" + ActionGetScopes = "GetScopes" + ActionGetVideoSources = "GetVideoSources" + ActionGetAudioSources = "GetAudioSources" + ActionGetVideoSourceConfigurations = "GetVideoSourceConfigurations" + ActionGetAudioSourceConfigurations = "GetAudioSourceConfigurations" + ActionGetVideoEncoderConfigurations = "GetVideoEncoderConfigurations" + ActionGetAudioEncoderConfigurations = "GetAudioEncoderConfigurations" +) + +func GetRequestAction(b []byte) string { + // + // + re := regexp.MustCompile(`Body[^<]+<([^ />]+)`) + m := re.FindSubmatch(b) + if len(m) != 2 { + return "" + } + if i := bytes.IndexByte(m[1], ':'); i > 0 { + return string(m[1][i+1:]) + } + return string(m[1]) +} + +func GetCapabilitiesResponse(host string) string { + return ` + + + + + + http://` + host + `/onvif/device_service + + + http://` + host + `/onvif/media_service + + false + false + true + + + + + +` +} + +func GetSystemDateAndTimeResponse() string { + 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"), + utc.Hour(), utc.Minute(), utc.Second(), utc.Year(), utc.Month(), utc.Day(), + loc.Hour(), loc.Minute(), loc.Second(), loc.Year(), loc.Month(), loc.Day(), + ) +} + +func GetNetworkInterfacesResponse() string { + return ` + + + + +` +} + +func GetDeviceInformationResponse(manuf, model, firmware, serial string) string { + return ` + + + + ` + manuf + ` + ` + model + ` + ` + firmware + ` + ` + serial + ` + 1.00 + + +` +} + +func GetServiceCapabilitiesResponse() string { + return ` + + + + + + + + +` +} + +func SystemRebootResponse() string { + return ` + + + + system reboot in 1 second... + + +` +} + +func GetProfilesResponse(names []string) string { + buf := bytes.NewBuffer(nil) + buf.WriteString(` + + + `) + + for i, name := range names { + buf.WriteString(` + + ` + name + ` + + H264 + + 1920 + 1080 + + + `) + } + + buf.WriteString(` + + +`) + + return buf.String() +} + +func GetStreamUriResponse(uri string) string { + return ` + + + + + ` + uri + ` + + + +` +} diff --git a/www/add.html b/www/add.html index fc907b76..476c65c5 100644 --- a/www/add.html +++ b/www/add.html @@ -182,6 +182,31 @@ + +
    +
    + + +
    +
    +
    + + +
    From d276311fcfc4e7b3e4c1f97231b15ada15d29edc Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Sat, 29 Apr 2023 17:00:52 +0300 Subject: [PATCH 43/80] Add support insecure HTTPS client --- cmd/http/http.go | 1 + pkg/tcp/request.go | 26 +++++++++++++++++++++++--- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/cmd/http/http.go b/cmd/http/http.go index a261b0c6..167f0f13 100644 --- a/cmd/http/http.go +++ b/cmd/http/http.go @@ -16,6 +16,7 @@ import ( func Init() { streams.HandleFunc("http", handle) streams.HandleFunc("https", handle) + streams.HandleFunc("httpx", handle) } func handle(url string) (core.Producer, error) { diff --git a/pkg/tcp/request.go b/pkg/tcp/request.go index f0b76edb..5bcbc48b 100644 --- a/pkg/tcp/request.go +++ b/pkg/tcp/request.go @@ -12,7 +12,7 @@ import ( // Do - http.Client with support Digest Authorization func Do(req *http.Request) (*http.Response, error) { - if client == nil { + if secureClient == nil { transport := http.DefaultTransport.(*http.Transport).Clone() dial := transport.DialContext @@ -24,12 +24,32 @@ func Do(req *http.Request) (*http.Response, error) { return conn, err } - client = &http.Client{ + secureClient = &http.Client{ Timeout: time.Second * 5000, Transport: transport, } } + var client *http.Client + + if req.URL.Scheme == "httpx" { + req.URL.Scheme = "https" + + if insecureClient == nil { + transport := secureClient.Transport.(*http.Transport).Clone() + transport.TLSClientConfig.InsecureSkipVerify = true + + insecureClient = &http.Client{ + Timeout: secureClient.Timeout, + Transport: transport, + } + } + + client = insecureClient + } else { + client = secureClient + } + user := req.URL.User // Hikvision won't answer on Basic auth with any headers @@ -92,7 +112,7 @@ func Do(req *http.Request) (*http.Response, error) { return res, nil } -var client *http.Client +var secureClient, insecureClient *http.Client var connKey struct{} func WithConn() (context.Context, *net.Conn) { From bc770f1a8576f9583dbf1796b7bc390ce8a80eec Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Sat, 29 Apr 2023 17:04:56 +0300 Subject: [PATCH 44/80] Remove FFmpeg buffer because have problems with MJPEG --- cmd/ffmpeg/ffmpeg.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/ffmpeg/ffmpeg.go b/cmd/ffmpeg/ffmpeg.go index 89a49537..2cc2e488 100644 --- a/cmd/ffmpeg/ffmpeg.go +++ b/cmd/ffmpeg/ffmpeg.go @@ -51,7 +51,7 @@ var defaults = map[string]string{ "rtsp/udp": "-fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -i {input}", // output - "output": "-user_agent ffmpeg/go2rtc -rtsp_transport tcp -bufsize 8192k -f rtsp {output}", + "output": "-user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}", // `-preset superfast` - we can't use ultrafast because it doesn't support `-profile main -level 4.1` // `-tune zerolatency` - for minimal latency From 75f61b38aca486957e042cbc9032a85f0c5efb39 Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Mon, 1 May 2023 12:55:14 +0300 Subject: [PATCH 45/80] Move cmd module to internal --- README.md | 2 +- {cmd => internal}/README.md | 0 {cmd => internal}/api/api.go | 2 +- {cmd => internal}/api/config.go | 2 +- {cmd => internal}/api/static.go | 0 {cmd => internal}/api/ws.go | 0 {cmd => internal}/app/app.go | 0 {cmd => internal}/app/store/store.go | 0 {cmd => internal}/debug/debug.go | 4 +- {cmd => internal}/debug/stack.go | 8 +-- {cmd => internal}/dvrip/dvrip.go | 2 +- {cmd => internal}/echo/echo.go | 4 +- {cmd => internal}/exec/exec.go | 6 +-- {cmd => internal}/ffmpeg/README.md | 0 .../ffmpeg/device/device_darwin.go | 0 .../ffmpeg/device/device_linux.go | 0 .../ffmpeg/device/device_windows.go | 0 {cmd => internal}/ffmpeg/device/devices.go | 4 +- {cmd => internal}/ffmpeg/ffmpeg.go | 10 ++-- {cmd => internal}/ffmpeg/ffmpeg_test.go | 0 {cmd => internal}/ffmpeg/hardware.go | 0 {cmd => internal}/ffmpeg/hardware_darwin.go | 0 {cmd => internal}/ffmpeg/hardware_linux.go | 0 {cmd => internal}/ffmpeg/hardware_windows.go | 0 {cmd => internal}/hass/api.go | 6 +-- {cmd => internal}/hass/hass.go | 8 +-- {cmd => internal}/hls/hls.go | 4 +- {cmd => internal}/homekit/api.go | 4 +- {cmd => internal}/homekit/homekit.go | 8 +-- {cmd => internal}/http/http.go | 2 +- {cmd => internal}/isapi/init.go | 2 +- {cmd => internal}/ivideon/ivideon.go | 2 +- {cmd => internal}/mjpeg/mjpeg.go | 4 +- {cmd => internal}/mp4/mp4.go | 6 +-- {cmd => internal}/mp4/ws.go | 4 +- {cmd => internal}/mpegts/mpegts.go | 4 +- {cmd => internal}/ngrok/ngrok.go | 4 +- {cmd => internal}/onvif/init.go | 13 +++-- {cmd => internal}/roborock/roborock.go | 4 +- {cmd => internal}/rtmp/rtmp.go | 4 +- {cmd => internal}/rtsp/rtsp.go | 4 +- {cmd => internal}/srtp/srtp.go | 2 +- {cmd => internal}/streams/handlers.go | 0 {cmd => internal}/streams/init.go | 6 +-- {cmd => internal}/streams/play.go | 0 {cmd => internal}/streams/producer.go | 0 {cmd => internal}/streams/stream.go | 0 {cmd => internal}/streams/stream_test.go | 0 {cmd => internal}/tapo/tapo.go | 2 +- {cmd => internal}/tcp/init.go | 2 +- {cmd => internal}/webrtc/README.md | 0 {cmd => internal}/webrtc/candidates.go | 2 +- {cmd => internal}/webrtc/client.go | 2 +- {cmd => internal}/webrtc/init.go | 6 +-- {cmd => internal}/webrtc/server.go | 2 +- {cmd => internal}/webtorrent/init.go | 8 +-- {cmd => internal}/webtorrent/tracker.go | 0 main.go | 54 +++++++++---------- 58 files changed, 109 insertions(+), 104 deletions(-) rename {cmd => internal}/README.md (100%) rename {cmd => internal}/api/api.go (98%) rename {cmd => internal}/api/config.go (98%) rename {cmd => internal}/api/static.go (100%) rename {cmd => internal}/api/ws.go (100%) rename {cmd => internal}/app/app.go (100%) rename {cmd => internal}/app/store/store.go (100%) rename {cmd => internal}/debug/debug.go (72%) rename {cmd => internal}/debug/stack.go (79%) rename {cmd => internal}/dvrip/dvrip.go (90%) rename {cmd => internal}/echo/echo.go (84%) rename {cmd => internal}/exec/exec.go (94%) rename {cmd => internal}/ffmpeg/README.md (100%) rename {cmd => internal}/ffmpeg/device/device_darwin.go (100%) rename {cmd => internal}/ffmpeg/device/device_linux.go (100%) rename {cmd => internal}/ffmpeg/device/device_windows.go (100%) rename {cmd => internal}/ffmpeg/device/devices.go (95%) rename {cmd => internal}/ffmpeg/ffmpeg.go (97%) rename {cmd => internal}/ffmpeg/ffmpeg_test.go (100%) rename {cmd => internal}/ffmpeg/hardware.go (100%) rename {cmd => internal}/ffmpeg/hardware_darwin.go (100%) rename {cmd => internal}/ffmpeg/hardware_linux.go (100%) rename {cmd => internal}/ffmpeg/hardware_windows.go (100%) rename {cmd => internal}/hass/api.go (94%) rename {cmd => internal}/hass/hass.go (95%) rename {cmd => internal}/hls/hls.go (98%) rename {cmd => internal}/homekit/api.go (96%) rename {cmd => internal}/homekit/homekit.go (74%) rename {cmd => internal}/http/http.go (96%) rename {cmd => internal}/isapi/init.go (88%) rename {cmd => internal}/ivideon/ivideon.go (88%) rename {cmd => internal}/mjpeg/mjpeg.go (97%) rename {cmd => internal}/mp4/mp4.go (96%) rename {cmd => internal}/mp4/ws.go (97%) rename {cmd => internal}/mpegts/mpegts.go (90%) rename {cmd => internal}/ngrok/ngrok.go (94%) rename {cmd => internal}/onvif/init.go (92%) rename {cmd => internal}/roborock/roborock.go (96%) rename {cmd => internal}/rtmp/rtmp.go (93%) rename {cmd => internal}/rtsp/rtsp.go (98%) rename {cmd => internal}/srtp/srtp.go (94%) rename {cmd => internal}/streams/handlers.go (100%) rename {cmd => internal}/streams/init.go (95%) rename {cmd => internal}/streams/play.go (100%) rename {cmd => internal}/streams/producer.go (100%) rename {cmd => internal}/streams/stream.go (100%) rename {cmd => internal}/streams/stream_test.go (100%) rename {cmd => internal}/tapo/tapo.go (87%) rename {cmd => internal}/tcp/init.go (92%) rename {cmd => internal}/webrtc/README.md (100%) rename {cmd => internal}/webrtc/candidates.go (98%) rename {cmd => internal}/webrtc/client.go (98%) rename {cmd => internal}/webrtc/init.go (97%) rename {cmd => internal}/webrtc/server.go (99%) rename {cmd => internal}/webtorrent/init.go (95%) rename {cmd => internal}/webtorrent/tracker.go (100%) diff --git a/README.md b/README.md index ab020cb7..39512acd 100644 --- a/README.md +++ b/README.md @@ -273,7 +273,7 @@ streams: rotate: ffmpeg:rtsp://rtsp:12345678@192.168.1.123/av_stream/ch0#video=h264#rotate=90 ``` -All trascoding formats has [built-in templates](https://github.com/AlexxIT/go2rtc/blob/master/cmd/ffmpeg/ffmpeg.go): `h264`, `h265`, `opus`, `pcmu`, `pcmu/16000`, `pcmu/48000`, `pcma`, `pcma/16000`, `pcma/48000`, `aac`, `aac/16000`. +All trascoding formats has [built-in templates](https://github.com/AlexxIT/go2rtc/blob/master/internal/ffmpeg/ffmpeg.go): `h264`, `h265`, `opus`, `pcmu`, `pcmu/16000`, `pcmu/48000`, `pcma`, `pcma/16000`, `pcma/48000`, `aac`, `aac/16000`. But you can override them via YAML config. You can also add your own formats to config and use them with source params. diff --git a/cmd/README.md b/internal/README.md similarity index 100% rename from cmd/README.md rename to internal/README.md diff --git a/cmd/api/api.go b/internal/api/api.go similarity index 98% rename from cmd/api/api.go rename to internal/api/api.go index 288ffad9..09766ca5 100644 --- a/cmd/api/api.go +++ b/internal/api/api.go @@ -2,7 +2,7 @@ package api import ( "encoding/json" - "github.com/AlexxIT/go2rtc/cmd/app" + "github.com/AlexxIT/go2rtc/internal/app" "github.com/rs/zerolog" "net" "net/http" diff --git a/cmd/api/config.go b/internal/api/config.go similarity index 98% rename from cmd/api/config.go rename to internal/api/config.go index 1bc257cb..d817b689 100644 --- a/cmd/api/config.go +++ b/internal/api/config.go @@ -1,7 +1,7 @@ package api import ( - "github.com/AlexxIT/go2rtc/cmd/app" + "github.com/AlexxIT/go2rtc/internal/app" "gopkg.in/yaml.v3" "io" "net/http" diff --git a/cmd/api/static.go b/internal/api/static.go similarity index 100% rename from cmd/api/static.go rename to internal/api/static.go diff --git a/cmd/api/ws.go b/internal/api/ws.go similarity index 100% rename from cmd/api/ws.go rename to internal/api/ws.go diff --git a/cmd/app/app.go b/internal/app/app.go similarity index 100% rename from cmd/app/app.go rename to internal/app/app.go diff --git a/cmd/app/store/store.go b/internal/app/store/store.go similarity index 100% rename from cmd/app/store/store.go rename to internal/app/store/store.go diff --git a/cmd/debug/debug.go b/internal/debug/debug.go similarity index 72% rename from cmd/debug/debug.go rename to internal/debug/debug.go index 90861b37..3d40d1f1 100644 --- a/cmd/debug/debug.go +++ b/internal/debug/debug.go @@ -1,8 +1,8 @@ package debug import ( - "github.com/AlexxIT/go2rtc/cmd/api" - "github.com/AlexxIT/go2rtc/cmd/streams" + "github.com/AlexxIT/go2rtc/internal/api" + "github.com/AlexxIT/go2rtc/internal/streams" "github.com/AlexxIT/go2rtc/pkg/core" ) diff --git a/cmd/debug/stack.go b/internal/debug/stack.go similarity index 79% rename from cmd/debug/stack.go rename to internal/debug/stack.go index 048ed77e..5d23cb5b 100644 --- a/cmd/debug/stack.go +++ b/internal/debug/stack.go @@ -13,15 +13,15 @@ var stackSkip = [][]byte{ []byte("created by os/signal.Notify"), // api/stack.go - []byte("github.com/AlexxIT/go2rtc/cmd/api.stackHandler"), + []byte("github.com/AlexxIT/go2rtc/internal/api.stackHandler"), // api/api.go - []byte("created by github.com/AlexxIT/go2rtc/cmd/api.Init"), + []byte("created by github.com/AlexxIT/go2rtc/internal/api.Init"), []byte("created by net/http.(*connReader).startBackgroundRead"), []byte("created by net/http.(*Server).Serve"), // TODO: why two? - []byte("created by github.com/AlexxIT/go2rtc/cmd/rtsp.Init"), - []byte("created by github.com/AlexxIT/go2rtc/cmd/srtp.Init"), + []byte("created by github.com/AlexxIT/go2rtc/internal/rtsp.Init"), + []byte("created by github.com/AlexxIT/go2rtc/internal/srtp.Init"), // webrtc/api.go []byte("created by github.com/pion/ice/v2.NewTCPMuxDefault"), diff --git a/cmd/dvrip/dvrip.go b/internal/dvrip/dvrip.go similarity index 90% rename from cmd/dvrip/dvrip.go rename to internal/dvrip/dvrip.go index 0826b009..80fe760d 100644 --- a/cmd/dvrip/dvrip.go +++ b/internal/dvrip/dvrip.go @@ -1,7 +1,7 @@ package dvrip import ( - "github.com/AlexxIT/go2rtc/cmd/streams" + "github.com/AlexxIT/go2rtc/internal/streams" "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/dvrip" ) diff --git a/cmd/echo/echo.go b/internal/echo/echo.go similarity index 84% rename from cmd/echo/echo.go rename to internal/echo/echo.go index d0714fc9..6d7644f7 100644 --- a/cmd/echo/echo.go +++ b/internal/echo/echo.go @@ -2,8 +2,8 @@ package echo import ( "bytes" - "github.com/AlexxIT/go2rtc/cmd/app" - "github.com/AlexxIT/go2rtc/cmd/streams" + "github.com/AlexxIT/go2rtc/internal/app" + "github.com/AlexxIT/go2rtc/internal/streams" "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/shell" "os/exec" diff --git a/cmd/exec/exec.go b/internal/exec/exec.go similarity index 94% rename from cmd/exec/exec.go rename to internal/exec/exec.go index 547d6a95..b77eb4c4 100644 --- a/cmd/exec/exec.go +++ b/internal/exec/exec.go @@ -5,9 +5,9 @@ import ( "encoding/hex" "errors" "fmt" - "github.com/AlexxIT/go2rtc/cmd/app" - "github.com/AlexxIT/go2rtc/cmd/rtsp" - "github.com/AlexxIT/go2rtc/cmd/streams" + "github.com/AlexxIT/go2rtc/internal/app" + "github.com/AlexxIT/go2rtc/internal/rtsp" + "github.com/AlexxIT/go2rtc/internal/streams" "github.com/AlexxIT/go2rtc/pkg/core" pkg "github.com/AlexxIT/go2rtc/pkg/rtsp" "github.com/AlexxIT/go2rtc/pkg/shell" diff --git a/cmd/ffmpeg/README.md b/internal/ffmpeg/README.md similarity index 100% rename from cmd/ffmpeg/README.md rename to internal/ffmpeg/README.md diff --git a/cmd/ffmpeg/device/device_darwin.go b/internal/ffmpeg/device/device_darwin.go similarity index 100% rename from cmd/ffmpeg/device/device_darwin.go rename to internal/ffmpeg/device/device_darwin.go diff --git a/cmd/ffmpeg/device/device_linux.go b/internal/ffmpeg/device/device_linux.go similarity index 100% rename from cmd/ffmpeg/device/device_linux.go rename to internal/ffmpeg/device/device_linux.go diff --git a/cmd/ffmpeg/device/device_windows.go b/internal/ffmpeg/device/device_windows.go similarity index 100% rename from cmd/ffmpeg/device/device_windows.go rename to internal/ffmpeg/device/device_windows.go diff --git a/cmd/ffmpeg/device/devices.go b/internal/ffmpeg/device/devices.go similarity index 95% rename from cmd/ffmpeg/device/devices.go rename to internal/ffmpeg/device/devices.go index 33610c10..e6e43b5a 100644 --- a/cmd/ffmpeg/device/devices.go +++ b/internal/ffmpeg/device/devices.go @@ -1,8 +1,8 @@ package device import ( - "github.com/AlexxIT/go2rtc/cmd/api" - "github.com/AlexxIT/go2rtc/cmd/app" + "github.com/AlexxIT/go2rtc/internal/api" + "github.com/AlexxIT/go2rtc/internal/app" "github.com/AlexxIT/go2rtc/pkg/core" "github.com/rs/zerolog" "net/http" diff --git a/cmd/ffmpeg/ffmpeg.go b/internal/ffmpeg/ffmpeg.go similarity index 97% rename from cmd/ffmpeg/ffmpeg.go rename to internal/ffmpeg/ffmpeg.go index 2cc2e488..2f48cc1b 100644 --- a/cmd/ffmpeg/ffmpeg.go +++ b/internal/ffmpeg/ffmpeg.go @@ -3,11 +3,11 @@ package ffmpeg import ( "bytes" "errors" - "github.com/AlexxIT/go2rtc/cmd/app" - "github.com/AlexxIT/go2rtc/cmd/exec" - "github.com/AlexxIT/go2rtc/cmd/ffmpeg/device" - "github.com/AlexxIT/go2rtc/cmd/rtsp" - "github.com/AlexxIT/go2rtc/cmd/streams" + "github.com/AlexxIT/go2rtc/internal/app" + "github.com/AlexxIT/go2rtc/internal/exec" + "github.com/AlexxIT/go2rtc/internal/ffmpeg/device" + "github.com/AlexxIT/go2rtc/internal/rtsp" + "github.com/AlexxIT/go2rtc/internal/streams" "github.com/AlexxIT/go2rtc/pkg/core" "net/url" "strconv" diff --git a/cmd/ffmpeg/ffmpeg_test.go b/internal/ffmpeg/ffmpeg_test.go similarity index 100% rename from cmd/ffmpeg/ffmpeg_test.go rename to internal/ffmpeg/ffmpeg_test.go diff --git a/cmd/ffmpeg/hardware.go b/internal/ffmpeg/hardware.go similarity index 100% rename from cmd/ffmpeg/hardware.go rename to internal/ffmpeg/hardware.go diff --git a/cmd/ffmpeg/hardware_darwin.go b/internal/ffmpeg/hardware_darwin.go similarity index 100% rename from cmd/ffmpeg/hardware_darwin.go rename to internal/ffmpeg/hardware_darwin.go diff --git a/cmd/ffmpeg/hardware_linux.go b/internal/ffmpeg/hardware_linux.go similarity index 100% rename from cmd/ffmpeg/hardware_linux.go rename to internal/ffmpeg/hardware_linux.go diff --git a/cmd/ffmpeg/hardware_windows.go b/internal/ffmpeg/hardware_windows.go similarity index 100% rename from cmd/ffmpeg/hardware_windows.go rename to internal/ffmpeg/hardware_windows.go diff --git a/cmd/hass/api.go b/internal/hass/api.go similarity index 94% rename from cmd/hass/api.go rename to internal/hass/api.go index f5ff5572..5c8294eb 100644 --- a/cmd/hass/api.go +++ b/internal/hass/api.go @@ -3,9 +3,9 @@ package hass import ( "encoding/base64" "encoding/json" - "github.com/AlexxIT/go2rtc/cmd/api" - "github.com/AlexxIT/go2rtc/cmd/streams" - "github.com/AlexxIT/go2rtc/cmd/webrtc" + "github.com/AlexxIT/go2rtc/internal/api" + "github.com/AlexxIT/go2rtc/internal/streams" + "github.com/AlexxIT/go2rtc/internal/webrtc" "net" "net/http" "strings" diff --git a/cmd/hass/hass.go b/internal/hass/hass.go similarity index 95% rename from cmd/hass/hass.go rename to internal/hass/hass.go index 32d0771e..e9a9f9b4 100644 --- a/cmd/hass/hass.go +++ b/internal/hass/hass.go @@ -4,10 +4,10 @@ import ( "bytes" "encoding/json" "fmt" - "github.com/AlexxIT/go2rtc/cmd/api" - "github.com/AlexxIT/go2rtc/cmd/app" - "github.com/AlexxIT/go2rtc/cmd/roborock" - "github.com/AlexxIT/go2rtc/cmd/streams" + "github.com/AlexxIT/go2rtc/internal/api" + "github.com/AlexxIT/go2rtc/internal/app" + "github.com/AlexxIT/go2rtc/internal/roborock" + "github.com/AlexxIT/go2rtc/internal/streams" "github.com/AlexxIT/go2rtc/pkg/core" "github.com/rs/zerolog" "net/http" diff --git a/cmd/hls/hls.go b/internal/hls/hls.go similarity index 98% rename from cmd/hls/hls.go rename to internal/hls/hls.go index 2cd9685d..06e841cd 100644 --- a/cmd/hls/hls.go +++ b/internal/hls/hls.go @@ -2,8 +2,8 @@ package hls import ( "fmt" - "github.com/AlexxIT/go2rtc/cmd/api" - "github.com/AlexxIT/go2rtc/cmd/streams" + "github.com/AlexxIT/go2rtc/internal/api" + "github.com/AlexxIT/go2rtc/internal/streams" "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/mp4" "github.com/AlexxIT/go2rtc/pkg/mpegts" diff --git a/cmd/homekit/api.go b/internal/homekit/api.go similarity index 96% rename from cmd/homekit/api.go rename to internal/homekit/api.go index a055871a..39fdaa43 100644 --- a/cmd/homekit/api.go +++ b/internal/homekit/api.go @@ -3,8 +3,8 @@ package homekit import ( "encoding/json" "fmt" - "github.com/AlexxIT/go2rtc/cmd/app/store" - "github.com/AlexxIT/go2rtc/cmd/streams" + "github.com/AlexxIT/go2rtc/internal/app/store" + "github.com/AlexxIT/go2rtc/internal/streams" "github.com/AlexxIT/go2rtc/pkg/hap" "github.com/AlexxIT/go2rtc/pkg/hap/mdns" "net/http" diff --git a/cmd/homekit/homekit.go b/internal/homekit/homekit.go similarity index 74% rename from cmd/homekit/homekit.go rename to internal/homekit/homekit.go index 1e7d0756..8376fd2e 100644 --- a/cmd/homekit/homekit.go +++ b/internal/homekit/homekit.go @@ -1,10 +1,10 @@ package homekit import ( - "github.com/AlexxIT/go2rtc/cmd/api" - "github.com/AlexxIT/go2rtc/cmd/app" - "github.com/AlexxIT/go2rtc/cmd/srtp" - "github.com/AlexxIT/go2rtc/cmd/streams" + "github.com/AlexxIT/go2rtc/internal/api" + "github.com/AlexxIT/go2rtc/internal/app" + "github.com/AlexxIT/go2rtc/internal/srtp" + "github.com/AlexxIT/go2rtc/internal/streams" "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/homekit" "github.com/rs/zerolog" diff --git a/cmd/http/http.go b/internal/http/http.go similarity index 96% rename from cmd/http/http.go rename to internal/http/http.go index 167f0f13..1bb86a26 100644 --- a/cmd/http/http.go +++ b/internal/http/http.go @@ -3,7 +3,7 @@ package http import ( "errors" "fmt" - "github.com/AlexxIT/go2rtc/cmd/streams" + "github.com/AlexxIT/go2rtc/internal/streams" "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/mjpeg" "github.com/AlexxIT/go2rtc/pkg/mpegts" diff --git a/cmd/isapi/init.go b/internal/isapi/init.go similarity index 88% rename from cmd/isapi/init.go rename to internal/isapi/init.go index 33bb85a7..a37afa23 100644 --- a/cmd/isapi/init.go +++ b/internal/isapi/init.go @@ -1,7 +1,7 @@ package isapi import ( - "github.com/AlexxIT/go2rtc/cmd/streams" + "github.com/AlexxIT/go2rtc/internal/streams" "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/isapi" ) diff --git a/cmd/ivideon/ivideon.go b/internal/ivideon/ivideon.go similarity index 88% rename from cmd/ivideon/ivideon.go rename to internal/ivideon/ivideon.go index d63edfd5..0ae5dc9f 100644 --- a/cmd/ivideon/ivideon.go +++ b/internal/ivideon/ivideon.go @@ -1,7 +1,7 @@ package ivideon import ( - "github.com/AlexxIT/go2rtc/cmd/streams" + "github.com/AlexxIT/go2rtc/internal/streams" "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/ivideon" "strings" diff --git a/cmd/mjpeg/mjpeg.go b/internal/mjpeg/mjpeg.go similarity index 97% rename from cmd/mjpeg/mjpeg.go rename to internal/mjpeg/mjpeg.go index 06d89023..30dd97d0 100644 --- a/cmd/mjpeg/mjpeg.go +++ b/internal/mjpeg/mjpeg.go @@ -2,8 +2,8 @@ package mjpeg import ( "errors" - "github.com/AlexxIT/go2rtc/cmd/api" - "github.com/AlexxIT/go2rtc/cmd/streams" + "github.com/AlexxIT/go2rtc/internal/api" + "github.com/AlexxIT/go2rtc/internal/streams" "github.com/AlexxIT/go2rtc/pkg/mjpeg" "github.com/AlexxIT/go2rtc/pkg/tcp" "github.com/rs/zerolog/log" diff --git a/cmd/mp4/mp4.go b/internal/mp4/mp4.go similarity index 96% rename from cmd/mp4/mp4.go rename to internal/mp4/mp4.go index e6df4910..e6791ee7 100644 --- a/cmd/mp4/mp4.go +++ b/internal/mp4/mp4.go @@ -6,9 +6,9 @@ import ( "strings" "time" - "github.com/AlexxIT/go2rtc/cmd/api" - "github.com/AlexxIT/go2rtc/cmd/app" - "github.com/AlexxIT/go2rtc/cmd/streams" + "github.com/AlexxIT/go2rtc/internal/api" + "github.com/AlexxIT/go2rtc/internal/app" + "github.com/AlexxIT/go2rtc/internal/streams" "github.com/AlexxIT/go2rtc/pkg/mp4" "github.com/AlexxIT/go2rtc/pkg/tcp" "github.com/rs/zerolog" diff --git a/cmd/mp4/ws.go b/internal/mp4/ws.go similarity index 97% rename from cmd/mp4/ws.go rename to internal/mp4/ws.go index 8366916c..0cd7e4fe 100644 --- a/cmd/mp4/ws.go +++ b/internal/mp4/ws.go @@ -2,8 +2,8 @@ package mp4 import ( "errors" - "github.com/AlexxIT/go2rtc/cmd/api" - "github.com/AlexxIT/go2rtc/cmd/streams" + "github.com/AlexxIT/go2rtc/internal/api" + "github.com/AlexxIT/go2rtc/internal/streams" "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/mp4" "github.com/AlexxIT/go2rtc/pkg/tcp" diff --git a/cmd/mpegts/mpegts.go b/internal/mpegts/mpegts.go similarity index 90% rename from cmd/mpegts/mpegts.go rename to internal/mpegts/mpegts.go index 7f76da92..fad0f11e 100644 --- a/cmd/mpegts/mpegts.go +++ b/internal/mpegts/mpegts.go @@ -1,8 +1,8 @@ package mpegts import ( - "github.com/AlexxIT/go2rtc/cmd/api" - "github.com/AlexxIT/go2rtc/cmd/streams" + "github.com/AlexxIT/go2rtc/internal/api" + "github.com/AlexxIT/go2rtc/internal/streams" "github.com/AlexxIT/go2rtc/pkg/mpegts" "net/http" ) diff --git a/cmd/ngrok/ngrok.go b/internal/ngrok/ngrok.go similarity index 94% rename from cmd/ngrok/ngrok.go rename to internal/ngrok/ngrok.go index 25266b08..67668829 100644 --- a/cmd/ngrok/ngrok.go +++ b/internal/ngrok/ngrok.go @@ -2,8 +2,8 @@ package ngrok import ( "fmt" - "github.com/AlexxIT/go2rtc/cmd/app" - "github.com/AlexxIT/go2rtc/cmd/webrtc" + "github.com/AlexxIT/go2rtc/internal/app" + "github.com/AlexxIT/go2rtc/internal/webrtc" "github.com/AlexxIT/go2rtc/pkg/ngrok" "github.com/rs/zerolog" "net" diff --git a/cmd/onvif/init.go b/internal/onvif/init.go similarity index 92% rename from cmd/onvif/init.go rename to internal/onvif/init.go index 928b77d4..a4f5ad22 100644 --- a/cmd/onvif/init.go +++ b/internal/onvif/init.go @@ -1,10 +1,10 @@ package onvif import ( - "github.com/AlexxIT/go2rtc/cmd/api" - "github.com/AlexxIT/go2rtc/cmd/app" - "github.com/AlexxIT/go2rtc/cmd/rtsp" - "github.com/AlexxIT/go2rtc/cmd/streams" + "github.com/AlexxIT/go2rtc/internal/api" + "github.com/AlexxIT/go2rtc/internal/app" + "github.com/AlexxIT/go2rtc/internal/rtsp" + "github.com/AlexxIT/go2rtc/internal/streams" "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/onvif" "github.com/rs/zerolog" @@ -142,6 +142,11 @@ func apiOnvif(w http.ResponseWriter, r *http.Request) { return } + if l := log.Trace(); l.Enabled() { + b, _ := client.GetProfiles() + l.Msgf("[onvif] src=%s profiles:\n%s", src, b) + } + name, err := client.GetName() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) diff --git a/cmd/roborock/roborock.go b/internal/roborock/roborock.go similarity index 96% rename from cmd/roborock/roborock.go rename to internal/roborock/roborock.go index 6e3457b7..e99586d9 100644 --- a/cmd/roborock/roborock.go +++ b/internal/roborock/roborock.go @@ -2,8 +2,8 @@ package roborock import ( "fmt" - "github.com/AlexxIT/go2rtc/cmd/api" - "github.com/AlexxIT/go2rtc/cmd/streams" + "github.com/AlexxIT/go2rtc/internal/api" + "github.com/AlexxIT/go2rtc/internal/streams" "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/roborock" "net/http" diff --git a/cmd/rtmp/rtmp.go b/internal/rtmp/rtmp.go similarity index 93% rename from cmd/rtmp/rtmp.go rename to internal/rtmp/rtmp.go index e3042981..f84c16be 100644 --- a/cmd/rtmp/rtmp.go +++ b/internal/rtmp/rtmp.go @@ -1,8 +1,8 @@ package rtmp import ( - "github.com/AlexxIT/go2rtc/cmd/api" - "github.com/AlexxIT/go2rtc/cmd/streams" + "github.com/AlexxIT/go2rtc/internal/api" + "github.com/AlexxIT/go2rtc/internal/streams" "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/rtmp" "github.com/rs/zerolog/log" diff --git a/cmd/rtsp/rtsp.go b/internal/rtsp/rtsp.go similarity index 98% rename from cmd/rtsp/rtsp.go rename to internal/rtsp/rtsp.go index 93306d88..a2c90e51 100644 --- a/cmd/rtsp/rtsp.go +++ b/internal/rtsp/rtsp.go @@ -6,8 +6,8 @@ import ( "net/url" "strings" - "github.com/AlexxIT/go2rtc/cmd/app" - "github.com/AlexxIT/go2rtc/cmd/streams" + "github.com/AlexxIT/go2rtc/internal/app" + "github.com/AlexxIT/go2rtc/internal/streams" "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/mp4" "github.com/AlexxIT/go2rtc/pkg/rtsp" diff --git a/cmd/srtp/srtp.go b/internal/srtp/srtp.go similarity index 94% rename from cmd/srtp/srtp.go rename to internal/srtp/srtp.go index a8e2c8b6..6cb2b546 100644 --- a/cmd/srtp/srtp.go +++ b/internal/srtp/srtp.go @@ -1,7 +1,7 @@ package srtp import ( - "github.com/AlexxIT/go2rtc/cmd/app" + "github.com/AlexxIT/go2rtc/internal/app" "github.com/AlexxIT/go2rtc/pkg/srtp" "net" ) diff --git a/cmd/streams/handlers.go b/internal/streams/handlers.go similarity index 100% rename from cmd/streams/handlers.go rename to internal/streams/handlers.go diff --git a/cmd/streams/init.go b/internal/streams/init.go similarity index 95% rename from cmd/streams/init.go rename to internal/streams/init.go index 84b2f207..697ce9d2 100644 --- a/cmd/streams/init.go +++ b/internal/streams/init.go @@ -2,9 +2,9 @@ package streams import ( "encoding/json" - "github.com/AlexxIT/go2rtc/cmd/api" - "github.com/AlexxIT/go2rtc/cmd/app" - "github.com/AlexxIT/go2rtc/cmd/app/store" + "github.com/AlexxIT/go2rtc/internal/api" + "github.com/AlexxIT/go2rtc/internal/app" + "github.com/AlexxIT/go2rtc/internal/app/store" "github.com/rs/zerolog" "net/http" "net/url" diff --git a/cmd/streams/play.go b/internal/streams/play.go similarity index 100% rename from cmd/streams/play.go rename to internal/streams/play.go diff --git a/cmd/streams/producer.go b/internal/streams/producer.go similarity index 100% rename from cmd/streams/producer.go rename to internal/streams/producer.go diff --git a/cmd/streams/stream.go b/internal/streams/stream.go similarity index 100% rename from cmd/streams/stream.go rename to internal/streams/stream.go diff --git a/cmd/streams/stream_test.go b/internal/streams/stream_test.go similarity index 100% rename from cmd/streams/stream_test.go rename to internal/streams/stream_test.go diff --git a/cmd/tapo/tapo.go b/internal/tapo/tapo.go similarity index 87% rename from cmd/tapo/tapo.go rename to internal/tapo/tapo.go index 07c278a5..971928c7 100644 --- a/cmd/tapo/tapo.go +++ b/internal/tapo/tapo.go @@ -1,7 +1,7 @@ package tapo import ( - "github.com/AlexxIT/go2rtc/cmd/streams" + "github.com/AlexxIT/go2rtc/internal/streams" "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/tapo" ) diff --git a/cmd/tcp/init.go b/internal/tcp/init.go similarity index 92% rename from cmd/tcp/init.go rename to internal/tcp/init.go index 353d28e6..7f712415 100644 --- a/cmd/tcp/init.go +++ b/internal/tcp/init.go @@ -1,7 +1,7 @@ package tcp import ( - "github.com/AlexxIT/go2rtc/cmd/streams" + "github.com/AlexxIT/go2rtc/internal/streams" "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/mpegts" "net" diff --git a/cmd/webrtc/README.md b/internal/webrtc/README.md similarity index 100% rename from cmd/webrtc/README.md rename to internal/webrtc/README.md diff --git a/cmd/webrtc/candidates.go b/internal/webrtc/candidates.go similarity index 98% rename from cmd/webrtc/candidates.go rename to internal/webrtc/candidates.go index 7825a5c5..51f97040 100644 --- a/cmd/webrtc/candidates.go +++ b/internal/webrtc/candidates.go @@ -1,7 +1,7 @@ package webrtc import ( - "github.com/AlexxIT/go2rtc/cmd/api" + "github.com/AlexxIT/go2rtc/internal/api" "github.com/AlexxIT/go2rtc/pkg/webrtc" "github.com/pion/sdp/v3" "strconv" diff --git a/cmd/webrtc/client.go b/internal/webrtc/client.go similarity index 98% rename from cmd/webrtc/client.go rename to internal/webrtc/client.go index bad44da8..9db347e9 100644 --- a/cmd/webrtc/client.go +++ b/internal/webrtc/client.go @@ -2,7 +2,7 @@ package webrtc import ( "errors" - "github.com/AlexxIT/go2rtc/cmd/api" + "github.com/AlexxIT/go2rtc/internal/api" "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/webrtc" "github.com/gorilla/websocket" diff --git a/cmd/webrtc/init.go b/internal/webrtc/init.go similarity index 97% rename from cmd/webrtc/init.go rename to internal/webrtc/init.go index d760d688..6855e1bd 100644 --- a/cmd/webrtc/init.go +++ b/internal/webrtc/init.go @@ -2,9 +2,9 @@ package webrtc import ( "errors" - "github.com/AlexxIT/go2rtc/cmd/api" - "github.com/AlexxIT/go2rtc/cmd/app" - "github.com/AlexxIT/go2rtc/cmd/streams" + "github.com/AlexxIT/go2rtc/internal/api" + "github.com/AlexxIT/go2rtc/internal/app" + "github.com/AlexxIT/go2rtc/internal/streams" "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/webrtc" pion "github.com/pion/webrtc/v3" diff --git a/cmd/webrtc/server.go b/internal/webrtc/server.go similarity index 99% rename from cmd/webrtc/server.go rename to internal/webrtc/server.go index f66cab00..0bdd8341 100644 --- a/cmd/webrtc/server.go +++ b/internal/webrtc/server.go @@ -2,7 +2,7 @@ package webrtc import ( "encoding/json" - "github.com/AlexxIT/go2rtc/cmd/streams" + "github.com/AlexxIT/go2rtc/internal/streams" "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/webrtc" pion "github.com/pion/webrtc/v3" diff --git a/cmd/webtorrent/init.go b/internal/webtorrent/init.go similarity index 95% rename from cmd/webtorrent/init.go rename to internal/webtorrent/init.go index e5de0da5..ede46e0e 100644 --- a/cmd/webtorrent/init.go +++ b/internal/webtorrent/init.go @@ -3,10 +3,10 @@ package webtorrent import ( "errors" "fmt" - "github.com/AlexxIT/go2rtc/cmd/api" - "github.com/AlexxIT/go2rtc/cmd/app" - "github.com/AlexxIT/go2rtc/cmd/streams" - "github.com/AlexxIT/go2rtc/cmd/webrtc" + "github.com/AlexxIT/go2rtc/internal/api" + "github.com/AlexxIT/go2rtc/internal/app" + "github.com/AlexxIT/go2rtc/internal/streams" + "github.com/AlexxIT/go2rtc/internal/webrtc" "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/webtorrent" "github.com/rs/zerolog" diff --git a/cmd/webtorrent/tracker.go b/internal/webtorrent/tracker.go similarity index 100% rename from cmd/webtorrent/tracker.go rename to internal/webtorrent/tracker.go diff --git a/main.go b/main.go index 6a50d407..c637fdc8 100644 --- a/main.go +++ b/main.go @@ -1,33 +1,33 @@ package main import ( - "github.com/AlexxIT/go2rtc/cmd/api" - "github.com/AlexxIT/go2rtc/cmd/app" - "github.com/AlexxIT/go2rtc/cmd/debug" - "github.com/AlexxIT/go2rtc/cmd/dvrip" - "github.com/AlexxIT/go2rtc/cmd/echo" - "github.com/AlexxIT/go2rtc/cmd/exec" - "github.com/AlexxIT/go2rtc/cmd/ffmpeg" - "github.com/AlexxIT/go2rtc/cmd/hass" - "github.com/AlexxIT/go2rtc/cmd/hls" - "github.com/AlexxIT/go2rtc/cmd/homekit" - "github.com/AlexxIT/go2rtc/cmd/http" - "github.com/AlexxIT/go2rtc/cmd/isapi" - "github.com/AlexxIT/go2rtc/cmd/ivideon" - "github.com/AlexxIT/go2rtc/cmd/mjpeg" - "github.com/AlexxIT/go2rtc/cmd/mp4" - "github.com/AlexxIT/go2rtc/cmd/mpegts" - "github.com/AlexxIT/go2rtc/cmd/ngrok" - "github.com/AlexxIT/go2rtc/cmd/onvif" - "github.com/AlexxIT/go2rtc/cmd/roborock" - "github.com/AlexxIT/go2rtc/cmd/rtmp" - "github.com/AlexxIT/go2rtc/cmd/rtsp" - "github.com/AlexxIT/go2rtc/cmd/srtp" - "github.com/AlexxIT/go2rtc/cmd/streams" - "github.com/AlexxIT/go2rtc/cmd/tapo" - "github.com/AlexxIT/go2rtc/cmd/tcp" - "github.com/AlexxIT/go2rtc/cmd/webrtc" - "github.com/AlexxIT/go2rtc/cmd/webtorrent" + "github.com/AlexxIT/go2rtc/internal/api" + "github.com/AlexxIT/go2rtc/internal/app" + "github.com/AlexxIT/go2rtc/internal/debug" + "github.com/AlexxIT/go2rtc/internal/dvrip" + "github.com/AlexxIT/go2rtc/internal/echo" + "github.com/AlexxIT/go2rtc/internal/exec" + "github.com/AlexxIT/go2rtc/internal/ffmpeg" + "github.com/AlexxIT/go2rtc/internal/hass" + "github.com/AlexxIT/go2rtc/internal/hls" + "github.com/AlexxIT/go2rtc/internal/homekit" + "github.com/AlexxIT/go2rtc/internal/http" + "github.com/AlexxIT/go2rtc/internal/isapi" + "github.com/AlexxIT/go2rtc/internal/ivideon" + "github.com/AlexxIT/go2rtc/internal/mjpeg" + "github.com/AlexxIT/go2rtc/internal/mp4" + "github.com/AlexxIT/go2rtc/internal/mpegts" + "github.com/AlexxIT/go2rtc/internal/ngrok" + "github.com/AlexxIT/go2rtc/internal/onvif" + "github.com/AlexxIT/go2rtc/internal/roborock" + "github.com/AlexxIT/go2rtc/internal/rtmp" + "github.com/AlexxIT/go2rtc/internal/rtsp" + "github.com/AlexxIT/go2rtc/internal/srtp" + "github.com/AlexxIT/go2rtc/internal/streams" + "github.com/AlexxIT/go2rtc/internal/tapo" + "github.com/AlexxIT/go2rtc/internal/tcp" + "github.com/AlexxIT/go2rtc/internal/webrtc" + "github.com/AlexxIT/go2rtc/internal/webtorrent" "os" "os/signal" "syscall" From 378f071e2ce1b43d1b762e3b2a30c975312f6259 Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Mon, 1 May 2023 12:55:32 +0300 Subject: [PATCH 46/80] Add go2rtc_rtsp app --- cmd/go2rtc_rtsp/main.go | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 cmd/go2rtc_rtsp/main.go diff --git a/cmd/go2rtc_rtsp/main.go b/cmd/go2rtc_rtsp/main.go new file mode 100644 index 00000000..2babffab --- /dev/null +++ b/cmd/go2rtc_rtsp/main.go @@ -0,0 +1,23 @@ +package main + +import ( + "github.com/AlexxIT/go2rtc/internal/app" + "github.com/AlexxIT/go2rtc/internal/rtsp" + "github.com/AlexxIT/go2rtc/internal/streams" + "os" + "os/signal" + "syscall" +) + +func main() { + app.Init() + streams.Init() + + rtsp.Init() + + sigs := make(chan os.Signal, 1) + signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) + <-sigs + + println("exit OK") +} From c7d228daff3442f9b7f6ae4145a9cb6fa4453069 Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Mon, 1 May 2023 14:33:03 +0300 Subject: [PATCH 47/80] Remove mp4 pkg dependency from rtsp pkg --- internal/rtsp/rtsp.go | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/internal/rtsp/rtsp.go b/internal/rtsp/rtsp.go index a2c90e51..f50337f4 100644 --- a/internal/rtsp/rtsp.go +++ b/internal/rtsp/rtsp.go @@ -9,7 +9,6 @@ import ( "github.com/AlexxIT/go2rtc/internal/app" "github.com/AlexxIT/go2rtc/internal/streams" "github.com/AlexxIT/go2rtc/pkg/core" - "github.com/AlexxIT/go2rtc/pkg/mp4" "github.com/AlexxIT/go2rtc/pkg/rtsp" "github.com/AlexxIT/go2rtc/pkg/tcp" "github.com/rs/zerolog" @@ -57,7 +56,7 @@ func Init() { log.Info().Str("addr", address).Msg("[rtsp] listen") if query, err := url.ParseQuery(conf.Mod.DefaultQuery); err == nil { - defaultMedias = mp4.ParseQuery(query) + defaultMedias = ParseQuery(query) } go func() { @@ -177,7 +176,7 @@ func tcpHandler(conn *rtsp.Conn) { conn.SessionName = app.UserAgent query := conn.URL.Query() - conn.Medias = mp4.ParseQuery(query) + conn.Medias = ParseQuery(query) if conn.Medias == nil { for _, media := range defaultMedias { conn.Medias = append(conn.Medias, media.Clone()) @@ -249,3 +248,27 @@ func tcpHandler(conn *rtsp.Conn) { _ = conn.Close() } + +func ParseQuery(query map[string][]string) []*core.Media { + if v := query["mp4"]; v != nil { + return []*core.Media{ + { + Kind: core.KindVideo, + Direction: core.DirectionSendonly, + Codecs: []*core.Codec{ + {Name: core.CodecH264}, + {Name: core.CodecH265}, + }, + }, + { + Kind: core.KindAudio, + Direction: core.DirectionSendonly, + Codecs: []*core.Codec{ + {Name: core.CodecAAC}, + }, + }, + } + } + + return core.ParseQuery(query) +} From 31f870e95000e6278c39980c302c152f8a9bc9ed Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Mon, 1 May 2023 15:09:58 +0300 Subject: [PATCH 48/80] Update internal readme --- internal/README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/internal/README.md b/internal/README.md index d3d1388c..e200c163 100644 --- a/internal/README.md +++ b/internal/README.md @@ -1,4 +1,11 @@ -**Project layout** +## Go + +``` +go mod why github.com/pion/rtcp +go list -deps .\cmd\go2rtc_rtsp\ +``` + +## Useful links - https://github.com/golang-standards/project-layout - https://github.com/micro/micro From 53967fc72a5012c8928687cd97ca1bef804570e1 Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Tue, 2 May 2023 09:36:38 +0300 Subject: [PATCH 49/80] Update ONVIF discovery request #397 --- pkg/onvif/helpers.go | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/pkg/onvif/helpers.go b/pkg/onvif/helpers.go index dd12e6bc..c772ee02 100644 --- a/pkg/onvif/helpers.go +++ b/pkg/onvif/helpers.go @@ -6,6 +6,7 @@ import ( "net/url" "regexp" "strconv" + "strings" "time" ) @@ -30,17 +31,19 @@ func DiscoveryStreamingHosts() ([]string, error) { return nil, err } + // https://www.onvif.org/wp-content/uploads/2016/12/ONVIF_Feature_Discovery_Specification_16.07.pdf + // 5.3 Discovery Procedure: msg := ` http://schemas.xmlsoap.org/ws/2005/04/discovery/Probe - uuid:` + UUID() + ` + urn:uuid:` + UUID() + ` urn:schemas-xmlsoap-org:ws:2005:04:discovery - tds:Device - onvif://www.onvif.org/Profile/Streaming + + ` @@ -67,6 +70,13 @@ func DiscoveryStreamingHosts() ([]string, error) { break } + // ignore printers, etc + if !strings.Contains(string(b[:n]), "onvif") { + continue + } + + //log.Printf("[onvif] discovery response:\n%s", b[:n]) + rawURL := FindTagValue(b[:n], "XAddrs") if rawURL == "" { continue From 9c8a1d8b1974fb57cc626e4e96f6226cb0f7b925 Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Tue, 2 May 2023 11:07:12 +0300 Subject: [PATCH 50/80] Add path to ONVIF requests --- pkg/onvif/client.go | 46 +++++++++++++++++++++++++++++--------------- pkg/onvif/helpers.go | 5 +++++ 2 files changed, 36 insertions(+), 15 deletions(-) diff --git a/pkg/onvif/client.go b/pkg/onvif/client.go index 77157d0b..a4b6413f 100644 --- a/pkg/onvif/client.go +++ b/pkg/onvif/client.go @@ -104,6 +104,7 @@ func (c *Client) HasSnapshots() bool { func (c *Client) GetCapabilities() ([]byte, error) { return c.Request( + PathDevice, ` All `, @@ -111,64 +112,79 @@ func (c *Client) GetCapabilities() ([]byte, error) { } func (c *Client) GetNetworkInterfaces() ([]byte, error) { - return c.Request(``) + return c.Request( + PathDevice, ``, + ) } func (c *Client) GetDeviceInformation() ([]byte, error) { - return c.Request(``) + return c.Request( + PathDevice, ``, + ) } func (c *Client) GetProfiles() ([]byte, error) { - return c.Request(``) + return c.Request( + PathMedia, ``, + ) } func (c *Client) GetStreamUri(token string) ([]byte, error) { return c.Request( + PathMedia, ` RTP-Unicast RTSP - ` + token + ` -`) + `+token+` +`, + ) } func (c *Client) GetSnapshotUri(token string) ([]byte, error) { return c.Request( + PathMedia, ` - ` + token + ` -`) + `+token+` +`, + ) } func (c *Client) GetSystemDateAndTime() ([]byte, error) { return c.Request( - ``, + PathDevice, ``, ) } func (c *Client) GetServiceCapabilities() ([]byte, error) { + // some cameras answer GetServiceCapabilities for media only for path = "/onvif/media" return c.Request( - ``, + PathMedia, ``, ) } func (c *Client) SystemReboot() ([]byte, error) { return c.Request( - ``, + PathDevice, ``, ) } func (c *Client) GetServices() ([]byte, error) { - return c.Request(` + return c.Request( + PathDevice, ` true -`) +`, + ) } func (c *Client) GetScopes() ([]byte, error) { - return c.Request(``) + return c.Request( + PathDevice, ``, + ) } -func (c *Client) Request(body string) ([]byte, error) { +func (c *Client) Request(path, body string) ([]byte, error) { buf := bytes.NewBuffer(nil) buf.WriteString( ``, @@ -198,7 +214,7 @@ func (c *Client) Request(body string) ([]byte, error) { client := &http.Client{Timeout: time.Second * 5000} res, err := client.Post( - "http://"+c.url.Host+"/onvif/", + "http://"+c.url.Host+path, `application/soap+xml;charset=utf-8`, buf, ) diff --git a/pkg/onvif/helpers.go b/pkg/onvif/helpers.go index c772ee02..ba9e61bf 100644 --- a/pkg/onvif/helpers.go +++ b/pkg/onvif/helpers.go @@ -10,6 +10,11 @@ import ( "time" ) +const ( + PathDevice = "/onvif/device_service" + PathMedia = "/onvif/media_service" +) + func FindTagValue(b []byte, tag string) string { re := regexp.MustCompile(tag + `[^>]*>([^<]+)`) m := re.FindSubmatch(b) From 4bbd3a1cd2e7da960cfb0ea709d8df17d60ac499 Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Tue, 2 May 2023 11:25:00 +0300 Subject: [PATCH 51/80] Fix ONVIF discovery for buggy camera --- pkg/onvif/helpers.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/pkg/onvif/helpers.go b/pkg/onvif/helpers.go index ba9e61bf..714bc0fc 100644 --- a/pkg/onvif/helpers.go +++ b/pkg/onvif/helpers.go @@ -31,7 +31,7 @@ func UUID() string { } func DiscoveryStreamingHosts() ([]string, error) { - conn, err := net.ListenPacket("udp4", ":0") + conn, err := net.ListenUDP("udp4", nil) if err != nil { return nil, err } @@ -70,7 +70,7 @@ func DiscoveryStreamingHosts() ([]string, error) { b := make([]byte, 8192) for { - n, _, err := conn.ReadFrom(b) + n, addr, err := conn.ReadFromUDP(b) if err != nil { break } @@ -96,6 +96,12 @@ func DiscoveryStreamingHosts() ([]string, error) { continue } + // fix some buggy cameras + // http://0.0.0.0:8080/onvif/device_service + if strings.HasPrefix(u.Host, "0.0.0.0") { + u.Host = addr.IP.String() + u.Host[7:] + } + hosts = append(hosts, u.Host) } From 95ca5f5fe12ddf70016e633957886a1d50b2bb86 Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Tue, 2 May 2023 14:20:55 +0300 Subject: [PATCH 52/80] Remove unnecessary run.sh file --- build/docker/run.sh | 19 ------------------- 1 file changed, 19 deletions(-) delete mode 100644 build/docker/run.sh diff --git a/build/docker/run.sh b/build/docker/run.sh deleted file mode 100644 index ca544747..00000000 --- a/build/docker/run.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/bash - -set -euo pipefail - -echo "Starting go2rtc..." >&2 - -readonly config_path="/config" - -if [[ -x "${config_path}/go2rtc" ]]; then - readonly binary_path="${config_path}/go2rtc" - echo "Using go2rtc binary from '${binary_path}' instead of the embedded one" >&2 -else - readonly binary_path="/usr/local/bin/go2rtc" -fi - -# set cwd for go2rtc (for config file, Hass integration, etc) -cd "${config_path}" || echo "Could not change working directory to '${config_path}'" >&2 - -exec "${binary_path}" From c1923627c0a585bd058627a35a76dfd0938d382e Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Wed, 3 May 2023 07:57:00 +0300 Subject: [PATCH 53/80] Fix panic on Producer GetMedias --- internal/streams/producer.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internal/streams/producer.go b/internal/streams/producer.go index 6eed311b..6306e0af 100644 --- a/internal/streams/producer.go +++ b/internal/streams/producer.go @@ -56,6 +56,10 @@ func (p *Producer) GetMedias() []*core.Media { p.mu.Lock() defer p.mu.Unlock() + if p.conn == nil { + return nil + } + return p.conn.GetMedias() } From 6d9d89bbe3ae0e90c8838412226d5aacbef922dc Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Wed, 3 May 2023 08:01:33 +0300 Subject: [PATCH 54/80] Fix support 2 way audio for Reolink Doorbell #331 --- pkg/pcm/helpers.go | 35 +++++++++++++++++++++++++++++++++++ pkg/rtsp/consumer.go | 7 +++++++ 2 files changed, 42 insertions(+) create mode 100644 pkg/pcm/helpers.go diff --git a/pkg/pcm/helpers.go b/pkg/pcm/helpers.go new file mode 100644 index 00000000..d873f7e3 --- /dev/null +++ b/pkg/pcm/helpers.go @@ -0,0 +1,35 @@ +package pcm + +import ( + "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/pion/rtp" +) + +func RepackBackchannel(handler core.HandlerFunc) core.HandlerFunc { + var buf []byte + var seq uint16 + + return func(packet *rtp.Packet) { + buf = append(buf, packet.Payload...) + if len(buf) < 1024 { + return + } + + pkt := &rtp.Packet{ + Header: rtp.Header{ + Version: 2, + Marker: true, // should be true + PayloadType: packet.PayloadType, // will be owerwriten + SequenceNumber: seq, + Timestamp: 0, // should be always zero + SSRC: packet.SSRC, + }, + Payload: buf[:1024], + } + + handler(pkt) + + buf = buf[1024:] + seq++ + } +} diff --git a/pkg/rtsp/consumer.go b/pkg/rtsp/consumer.go index 38125147..663c8522 100644 --- a/pkg/rtsp/consumer.go +++ b/pkg/rtsp/consumer.go @@ -6,6 +6,7 @@ import ( "github.com/AlexxIT/go2rtc/pkg/h264" "github.com/AlexxIT/go2rtc/pkg/h265" "github.com/AlexxIT/go2rtc/pkg/mjpeg" + "github.com/AlexxIT/go2rtc/pkg/pcm" "github.com/pion/rtp" "time" ) @@ -60,6 +61,12 @@ func (c *Conn) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiv sender := core.NewSender(media, codec) // important to send original codec for valid IsRTP check sender.Handler = c.packetWriter(track.Codec, channel, codec.PayloadType) + + // https://github.com/AlexxIT/go2rtc/issues/331 + if c.mode == core.ModeActiveProducer && track.Codec.Name == core.CodecPCMA { + sender.Handler = pcm.RepackBackchannel(sender.Handler) + } + sender.HandleRTP(track) c.senders = append(c.senders, sender) From 4d53889519d233a5f875e0e877d18cbc12d30a35 Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Wed, 3 May 2023 08:02:56 +0300 Subject: [PATCH 55/80] Improve support ONVIF client --- internal/onvif/init.go | 28 ++++++++++++++---- pkg/onvif/client.go | 64 ++++++++++++++++++++++++++++-------------- pkg/onvif/helpers.go | 33 ++++++++-------------- www/add.html | 1 + 4 files changed, 77 insertions(+), 49 deletions(-) diff --git a/internal/onvif/init.go b/internal/onvif/init.go index a4f5ad22..01b33702 100644 --- a/internal/onvif/init.go +++ b/internal/onvif/init.go @@ -11,6 +11,7 @@ import ( "io" "net" "net/http" + "net/url" "os" "strconv" "time" @@ -123,17 +124,32 @@ func apiOnvif(w http.ResponseWriter, r *http.Request) { var items []api.Stream if src == "" { - hosts, err := onvif.DiscoveryStreamingHosts() + urls, err := onvif.DiscoveryStreamingURLs() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } - for _, host := range hosts { - items = append(items, api.Stream{ - Name: host, - URL: "onvif://user:pass@" + host, - }) + for _, rawURL := range urls { + u, err := url.Parse(rawURL) + if err != nil { + log.Warn().Str("url", rawURL).Msg("[onvif] broken") + continue + } + + if u.Scheme != "http" { + log.Warn().Str("url", rawURL).Msg("[onvif] unsupported") + continue + } + + u.Scheme = "onvif" + u.User = url.UserPassword("user", "pass") + + if u.Path == onvif.PathDevice { + u.Path = "" + } + + items = append(items, api.Stream{Name: u.Host, URL: u.String()}) } } else { client, err := onvif.NewClient(src) diff --git a/pkg/onvif/client.go b/pkg/onvif/client.go index a4b6413f..d090baf3 100644 --- a/pkg/onvif/client.go +++ b/pkg/onvif/client.go @@ -17,6 +17,10 @@ import ( type Client struct { url *url.URL + + deviceURL string + mediaURL string + imaginURL string } func NewClient(rawURL string) (*Client, error) { @@ -24,7 +28,25 @@ func NewClient(rawURL string) (*Client, error) { if err != nil { return nil, err } - return &Client{url: u}, nil + + baseURL := "http://" + u.Host + + client := &Client{url: u} + if u.Path == "" { + client.deviceURL = baseURL + PathDevice + } else { + client.deviceURL = baseURL + u.Path + } + + b, err := client.GetCapabilities() + if err != nil { + return nil, err + } + + client.mediaURL = FindTagValue(b, "Media.+?XAddr") + client.imaginURL = FindTagValue(b, "Imaging.+?XAddr") + + return client, nil } func (c *Client) GetURI() (string, error) { @@ -39,7 +61,7 @@ func (c *Client) GetURI() (string, error) { return "", err } if i >= len(tokens) { - return "", errors.New("wrong subtype") + return "", errors.New("onvif: wrong subtype") } token = tokens[i] } @@ -104,7 +126,7 @@ func (c *Client) HasSnapshots() bool { func (c *Client) GetCapabilities() ([]byte, error) { return c.Request( - PathDevice, + c.deviceURL, ` All `, @@ -113,25 +135,25 @@ func (c *Client) GetCapabilities() ([]byte, error) { func (c *Client) GetNetworkInterfaces() ([]byte, error) { return c.Request( - PathDevice, ``, + c.deviceURL, ``, ) } func (c *Client) GetDeviceInformation() ([]byte, error) { return c.Request( - PathDevice, ``, + c.deviceURL, ``, ) } func (c *Client) GetProfiles() ([]byte, error) { return c.Request( - PathMedia, ``, + c.mediaURL, ``, ) } func (c *Client) GetStreamUri(token string) ([]byte, error) { return c.Request( - PathMedia, + c.mediaURL, ` RTP-Unicast @@ -144,8 +166,8 @@ func (c *Client) GetStreamUri(token string) ([]byte, error) { func (c *Client) GetSnapshotUri(token string) ([]byte, error) { return c.Request( - PathMedia, - ` + c.imaginURL, + ` `+token+` `, ) @@ -153,26 +175,26 @@ func (c *Client) GetSnapshotUri(token string) ([]byte, error) { func (c *Client) GetSystemDateAndTime() ([]byte, error) { return c.Request( - PathDevice, ``, + c.deviceURL, ``, ) } func (c *Client) GetServiceCapabilities() ([]byte, error) { // some cameras answer GetServiceCapabilities for media only for path = "/onvif/media" return c.Request( - PathMedia, ``, + c.mediaURL, ``, ) } func (c *Client) SystemReboot() ([]byte, error) { return c.Request( - PathDevice, ``, + c.deviceURL, ``, ) } func (c *Client) GetServices() ([]byte, error) { return c.Request( - PathDevice, ` + c.deviceURL, ` true `, ) @@ -180,11 +202,15 @@ func (c *Client) GetServices() ([]byte, error) { func (c *Client) GetScopes() ([]byte, error) { return c.Request( - PathDevice, ``, + c.deviceURL, ``, ) } -func (c *Client) Request(path, body string) ([]byte, error) { +func (c *Client) Request(url, body string) ([]byte, error) { + if url == "" { + return nil, errors.New("onvif: unsupported service") + } + buf := bytes.NewBuffer(nil) buf.WriteString( ``, @@ -213,11 +239,7 @@ func (c *Client) Request(path, body string) ([]byte, error) { buf.WriteString(`` + body + ``) client := &http.Client{Timeout: time.Second * 5000} - res, err := client.Post( - "http://"+c.url.Host+path, - `application/soap+xml;charset=utf-8`, - buf, - ) + res, err := client.Post(url, `application/soap+xml;charset=utf-8`, buf) if err != nil { return nil, err } @@ -226,7 +248,7 @@ func (c *Client) Request(path, body string) ([]byte, error) { b, err := io.ReadAll(res.Body) if err == nil && res.StatusCode != http.StatusOK { - err = errors.New(res.Status) + err = errors.New("onvif: " + res.Status + " for " + url) } return b, err diff --git a/pkg/onvif/helpers.go b/pkg/onvif/helpers.go index 714bc0fc..c5451c42 100644 --- a/pkg/onvif/helpers.go +++ b/pkg/onvif/helpers.go @@ -3,7 +3,6 @@ package onvif import ( "github.com/AlexxIT/go2rtc/pkg/core" "net" - "net/url" "regexp" "strconv" "strings" @@ -12,11 +11,10 @@ import ( const ( PathDevice = "/onvif/device_service" - PathMedia = "/onvif/media_service" ) func FindTagValue(b []byte, tag string) string { - re := regexp.MustCompile(tag + `[^>]*>([^<]+)`) + re := regexp.MustCompile(`<[^/>]*` + tag + `[^>]*>([^<]+)`) m := re.FindSubmatch(b) if len(m) != 2 { return "" @@ -30,7 +28,7 @@ func UUID() string { return s[:8] + "-" + s[8:12] + "-" + s[12:16] + "-" + s[16:20] + "-" + s[20:] } -func DiscoveryStreamingHosts() ([]string, error) { +func DiscoveryStreamingURLs() ([]string, error) { conn, err := net.ListenUDP("udp4", nil) if err != nil { return nil, err @@ -66,7 +64,7 @@ func DiscoveryStreamingHosts() ([]string, error) { return nil, err } - var hosts []string + var urls []string b := make([]byte, 8192) for { @@ -75,37 +73,28 @@ func DiscoveryStreamingHosts() ([]string, error) { break } + //log.Printf("[onvif] discovery response addr=%s:\n%s", addr, b[:n]) + // ignore printers, etc if !strings.Contains(string(b[:n]), "onvif") { continue } - //log.Printf("[onvif] discovery response:\n%s", b[:n]) - - rawURL := FindTagValue(b[:n], "XAddrs") - if rawURL == "" { - continue - } - - u, err := url.Parse(rawURL) - if err != nil { - continue - } - - if u.Scheme != "http" { + url := FindTagValue(b[:n], "XAddrs") + if url == "" { continue } // fix some buggy cameras // http://0.0.0.0:8080/onvif/device_service - if strings.HasPrefix(u.Host, "0.0.0.0") { - u.Host = addr.IP.String() + u.Host[7:] + if strings.HasPrefix(url, "http://0.0.0.0") { + url = "http://" + addr.IP.String() + url[14:] } - hosts = append(hosts, u.Host) + urls = append(urls, url) } - return hosts, nil + return urls, nil } func atoi(s string) int { diff --git a/www/add.html b/www/add.html index 476c65c5..fc8a7c46 100644 --- a/www/add.html +++ b/www/add.html @@ -60,6 +60,7 @@ + +
    + +
    +
    + + +
    @@ -232,19 +245,6 @@ - -
    - -
    -
    - - -
    From 2e8be342ef226eab316980556f3e54507841862c Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Thu, 4 May 2023 01:24:37 +0300 Subject: [PATCH 58/80] Rework FFmpeg hardware support --- internal/ffmpeg/device/devices.go | 4 +- internal/ffmpeg/ffmpeg.go | 133 +++++-------------- internal/ffmpeg/{ => hardware}/hardware.go | 67 ++++++---- internal/ffmpeg/hardware/hardware_darwin.go | 37 ++++++ internal/ffmpeg/hardware/hardware_linux.go | 94 +++++++++++++ internal/ffmpeg/hardware/hardware_windows.go | 61 +++++++++ internal/ffmpeg/hardware_darwin.go | 21 --- internal/ffmpeg/hardware_linux.go | 67 ---------- internal/ffmpeg/hardware_windows.go | 40 ------ pkg/ffmpeg/ffmpeg.go | 80 +++++++++++ www/add.html | 13 ++ 11 files changed, 360 insertions(+), 257 deletions(-) rename internal/ffmpeg/{ => hardware}/hardware.go (53%) create mode 100644 internal/ffmpeg/hardware/hardware_darwin.go create mode 100644 internal/ffmpeg/hardware/hardware_linux.go create mode 100644 internal/ffmpeg/hardware/hardware_windows.go delete mode 100644 internal/ffmpeg/hardware_darwin.go delete mode 100644 internal/ffmpeg/hardware_linux.go delete mode 100644 internal/ffmpeg/hardware_windows.go create mode 100644 pkg/ffmpeg/ffmpeg.go diff --git a/internal/ffmpeg/device/devices.go b/internal/ffmpeg/device/devices.go index e951aa76..a02cb896 100644 --- a/internal/ffmpeg/device/devices.go +++ b/internal/ffmpeg/device/devices.go @@ -9,7 +9,9 @@ import ( "sync" ) -func Init() { +func Init(bin string) { + Bin = bin + api.HandleFunc("api/ffmpeg/devices", apiDevices) } diff --git a/internal/ffmpeg/ffmpeg.go b/internal/ffmpeg/ffmpeg.go index e33afba0..c69ad822 100644 --- a/internal/ffmpeg/ffmpeg.go +++ b/internal/ffmpeg/ffmpeg.go @@ -1,16 +1,16 @@ package ffmpeg import ( - "bytes" "errors" "github.com/AlexxIT/go2rtc/internal/app" "github.com/AlexxIT/go2rtc/internal/exec" "github.com/AlexxIT/go2rtc/internal/ffmpeg/device" + "github.com/AlexxIT/go2rtc/internal/ffmpeg/hardware" "github.com/AlexxIT/go2rtc/internal/rtsp" "github.com/AlexxIT/go2rtc/internal/streams" "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/AlexxIT/go2rtc/pkg/ffmpeg" "net/url" - "strconv" "strings" ) @@ -35,8 +35,8 @@ func Init() { return exec.Handle("exec:" + args.String()) }) - device.Bin = defaults["bin"] - device.Init() + device.Init(defaults["bin"]) + hardware.Init(defaults["bin"]) } var defaults = map[string]string{ @@ -116,19 +116,19 @@ func inputTemplate(name, s string, query url.Values) string { return strings.Replace(template, "{input}", s, 1) } -func parseArgs(s string) *Args { +func parseArgs(s string) *ffmpeg.Args { // init FFmpeg arguments - args := &Args{ - bin: defaults["bin"], - global: defaults["global"], - output: defaults["output"], + args := &ffmpeg.Args{ + Bin: defaults["bin"], + Global: defaults["global"], + Output: defaults["output"], } var query url.Values if i := strings.IndexByte(s, '#'); i > 0 { query = parseQuery(s[i+1:]) - args.video = len(query["video"]) - args.audio = len(query["audio"]) + args.Video = len(query["video"]) + args.Audio = len(query["audio"]) s = s[:i] } @@ -139,46 +139,46 @@ func parseArgs(s string) *Args { if i := strings.Index(s, "://"); i > 0 { switch s[:i] { case "http", "https", "rtmp": - args.input = inputTemplate("http", s, query) + args.Input = inputTemplate("http", s, query) case "rtsp", "rtsps": // https://ffmpeg.org/ffmpeg-protocols.html#rtsp // skip unnecessary input tracks switch { - case (args.video > 0 && args.audio > 0) || (args.video == 0 && args.audio == 0): - args.input = "-allowed_media_types video+audio " - case args.video > 0: - args.input = "-allowed_media_types video " - case args.audio > 0: - args.input = "-allowed_media_types audio " + case (args.Video > 0 && args.Audio > 0) || (args.Video == 0 && args.Audio == 0): + args.Input = "-allowed_media_types video+audio " + case args.Video > 0: + args.Input = "-allowed_media_types video " + case args.Audio > 0: + args.Input = "-allowed_media_types audio " } - args.input += inputTemplate("rtsp", s, query) + args.Input += inputTemplate("rtsp", s, query) default: - args.input = "-i " + s + args.Input = "-i " + s } } else if streams.Get(s) != nil { s = "rtsp://127.0.0.1:" + rtsp.Port + "/" + s switch { - case args.video > 0 && args.audio == 0: + case args.Video > 0 && args.Audio == 0: s += "?video" - case args.audio > 0 && args.video == 0: + case args.Audio > 0 && args.Video == 0: s += "?audio" default: s += "?video&audio" } - args.input = inputTemplate("rtsp", s, query) + args.Input = inputTemplate("rtsp", s, query) } else if strings.HasPrefix(s, "device?") { var err error - args.input, err = device.GetInput(s) + args.Input, err = device.GetInput(s) if err != nil { return nil } } else { - args.input = inputTemplate("file", s, query) + args.Input = inputTemplate("file", s, query) } if query["async"] != nil { - args.input = "-use_wallclock_as_timestamps 1 -async 1 " + args.input + args.Input = "-use_wallclock_as_timestamps 1 -async 1 " + args.Input } // Parse query params: @@ -226,7 +226,7 @@ func parseArgs(s string) *Args { } // 3. Process video codecs - if args.video > 0 { + if args.Video > 0 { for _, video := range query["video"] { if video != "copy" { if codec := defaults[video]; codec != "" { @@ -243,7 +243,7 @@ func parseArgs(s string) *Args { } // 4. Process audio codecs - if args.audio > 0 { + if args.Audio > 0 { for _, audio := range query["audio"] { if audio != "copy" { if codec := defaults[audio]; codec != "" { @@ -260,11 +260,11 @@ func parseArgs(s string) *Args { } if query["hardware"] != nil { - MakeHardware(args, query["hardware"][0]) + hardware.MakeHardware(args, query["hardware"][0], defaults) } } - if args.codecs == nil { + if args.Codecs == nil { args.AddCodec("-c copy") } @@ -283,76 +283,3 @@ func parseQuery(s string) map[string][]string { } return query } - -type Args struct { - bin string // ffmpeg - global string // -hide_banner -v error - input string // -re -stream_loop -1 -i /media/bunny.mp4 - codecs []string // -c:v libx264 -g:v 30 -preset:v ultrafast -tune:v zerolatency - filters []string // scale=1920:1080 - output string // -f rtsp {output} - - video, audio int // count of video and audio params -} - -func (a *Args) AddCodec(codec string) { - a.codecs = append(a.codecs, codec) -} - -func (a *Args) AddFilter(filter string) { - a.filters = append(a.filters, filter) -} - -func (a *Args) InsertFilter(filter string) { - a.filters = append([]string{filter}, a.filters...) -} - -func (a *Args) String() string { - b := bytes.NewBuffer(make([]byte, 0, 512)) - - b.WriteString(a.bin) - - if a.global != "" { - b.WriteByte(' ') - b.WriteString(a.global) - } - - b.WriteByte(' ') - b.WriteString(a.input) - - multimode := a.video > 1 || a.audio > 1 - var iv, ia int - - for _, codec := range a.codecs { - // support multiple video and/or audio codecs - if multimode && len(codec) >= 5 { - switch codec[:5] { - case "-c:v ": - codec = "-map 0:v:0? " + strings.ReplaceAll(codec, ":v ", ":v:"+strconv.Itoa(iv)+" ") - iv++ - case "-c:a ": - codec = "-map 0:a:0? " + strings.ReplaceAll(codec, ":a ", ":a:"+strconv.Itoa(ia)+" ") - ia++ - } - } - - b.WriteByte(' ') - b.WriteString(codec) - } - - if a.filters != nil { - for i, filter := range a.filters { - if i == 0 { - b.WriteString(" -vf ") - } else { - b.WriteByte(',') - } - b.WriteString(filter) - } - } - - b.WriteByte(' ') - b.WriteString(a.output) - - return b.String() -} diff --git a/internal/ffmpeg/hardware.go b/internal/ffmpeg/hardware/hardware.go similarity index 53% rename from internal/ffmpeg/hardware.go rename to internal/ffmpeg/hardware/hardware.go index 45001271..e560ac54 100644 --- a/internal/ffmpeg/hardware.go +++ b/internal/ffmpeg/hardware/hardware.go @@ -1,6 +1,9 @@ -package ffmpeg +package hardware import ( + "github.com/AlexxIT/go2rtc/internal/api" + "github.com/AlexxIT/go2rtc/pkg/ffmpeg" + "net/http" "os/exec" "strings" @@ -16,12 +19,16 @@ const ( EngineVideoToolbox = "videotoolbox" // macOS ) -var cache = map[string]string{} +func Init(bin string) { + api.HandleFunc("api/ffmpeg/hardware", func(w http.ResponseWriter, r *http.Request) { + api.ResponseStreams(w, ProbeAll(bin)) + }) +} // MakeHardware converts software FFmpeg args to hardware args // empty engine for autoselect -func MakeHardware(args *Args, engine string) { - for i, codec := range args.codecs { +func MakeHardware(args *ffmpeg.Args, engine string, defaults map[string]string) { + for i, codec := range args.Codecs { if len(codec) < 12 { continue // skip short line (-c:v libx264...) } @@ -41,25 +48,25 @@ func MakeHardware(args *Args, engine string) { // temporary disable probe for H265 and MJPEG if engine == "" && name == "h264" { if engine = cache[name]; engine == "" { - engine = ProbeHardware(name) + engine = ProbeHardware(args.Bin, name) cache[name] = engine } } switch engine { case EngineVAAPI: - args.input = "-hwaccel vaapi -hwaccel_output_format vaapi " + args.input - args.codecs[i] = defaults[name+"/"+engine] + args.Input = "-hwaccel vaapi -hwaccel_output_format vaapi " + args.Input + args.Codecs[i] = defaults[name+"/"+engine] - for i, filter := range args.filters { + for i, filter := range args.Filters { if strings.HasPrefix(filter, "scale=") { - args.filters[i] = "scale_vaapi=" + filter[6:] + args.Filters[i] = "scale_vaapi=" + filter[6:] } if strings.HasPrefix(filter, "transpose=") { if filter == "transpose=1,transpose=1" { // 180 degrees half-turn - args.filters[i] = "transpose_vaapi=4" // reversal + args.Filters[i] = "transpose_vaapi=4" // reversal } else { - args.filters[i] = "transpose_vaapi=" + filter[10:] + args.Filters[i] = "transpose_vaapi=" + filter[10:] } } } @@ -68,43 +75,53 @@ func MakeHardware(args *Args, engine string) { args.InsertFilter("format=vaapi|nv12,hwupload") case EngineCUDA: - args.input = "-hwaccel cuda -hwaccel_output_format cuda -extra_hw_frames 2 " + args.input - args.codecs[i] = defaults[name+"/"+engine] + args.Input = "-hwaccel cuda -hwaccel_output_format cuda -extra_hw_frames 2 " + args.Input + args.Codecs[i] = defaults[name+"/"+engine] - for i, filter := range args.filters { + for i, filter := range args.Filters { if strings.HasPrefix(filter, "scale=") { - args.filters[i] = "scale_cuda=" + filter[6:] + args.Filters[i] = "scale_cuda=" + filter[6:] } } case EngineDXVA2: - args.input = "-hwaccel dxva2 -hwaccel_output_format dxva2_vld " + args.input - args.codecs[i] = defaults[name+"/"+engine] + args.Input = "-hwaccel dxva2 -hwaccel_output_format dxva2_vld " + args.Input + args.Codecs[i] = defaults[name+"/"+engine] - for i, filter := range args.filters { + for i, filter := range args.Filters { if strings.HasPrefix(filter, "scale=") { - args.filters[i] = "scale_qsv=" + filter[6:] + args.Filters[i] = "scale_qsv=" + filter[6:] } } args.InsertFilter("hwmap=derive_device=qsv,format=qsv") case EngineVideoToolbox: - args.input = "-hwaccel videotoolbox -hwaccel_output_format videotoolbox_vld " + args.input - args.codecs[i] = defaults[name+"/"+engine] + args.Input = "-hwaccel videotoolbox -hwaccel_output_format videotoolbox_vld " + args.Input + args.Codecs[i] = defaults[name+"/"+engine] case EngineV4L2M2M: - args.codecs[i] = defaults[name+"/"+engine] + args.Codecs[i] = defaults[name+"/"+engine] } } } -func run(arg ...string) bool { - err := exec.Command(defaults["bin"], arg...).Run() - log.Printf("%v %v", arg, err) +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 } +func runToString(bin string, args string) string { + if run(bin, args) { + return "OK" + } else { + return "ERROR" + } +} + func cut(s string, sep byte, pos int) string { for n := 0; n < pos; n++ { if i := strings.IndexByte(s, sep); i > 0 { diff --git a/internal/ffmpeg/hardware/hardware_darwin.go b/internal/ffmpeg/hardware/hardware_darwin.go new file mode 100644 index 00000000..923f4159 --- /dev/null +++ b/internal/ffmpeg/hardware/hardware_darwin.go @@ -0,0 +1,37 @@ +package hardware + +import ( + "github.com/AlexxIT/go2rtc/internal/api" +) + +const ProbeVideoToolboxH264 = "-f lavfi -i testsrc2 -t 1 -c h264_videotoolbox -f null -" +const ProbeVideoToolboxH265 = "-f lavfi -i testsrc2 -t 1 -c hevc_videotoolbox -f null -" + +func ProbeAll(bin string) []api.Stream { + return []api.Stream{ + { + Name: runToString(bin, ProbeVideoToolboxH264), + URL: "ffmpeg:...#video=h264#hardware=" + EngineVideoToolbox, + }, + { + Name: runToString(bin, ProbeVideoToolboxH265), + URL: "ffmpeg:...#video=h265#hardware=" + EngineVideoToolbox, + }, + } +} + +func ProbeHardware(bin, name string) string { + switch name { + case "h264": + if run(bin, ProbeVideoToolboxH264) { + return EngineVideoToolbox + } + + case "h265": + if run(bin, ProbeVideoToolboxH265) { + return EngineVideoToolbox + } + } + + return EngineSoftware +} diff --git a/internal/ffmpeg/hardware/hardware_linux.go b/internal/ffmpeg/hardware/hardware_linux.go new file mode 100644 index 00000000..cacfb10e --- /dev/null +++ b/internal/ffmpeg/hardware/hardware_linux.go @@ -0,0 +1,94 @@ +package hardware + +import ( + "github.com/AlexxIT/go2rtc/internal/api" + "runtime" +) + +const ProbeV4L2M2MH264 = "-f lavfi -i testsrc2 -t 1 -c h264_v4l2m2m -f null -" +const ProbeV4L2M2MH265 = "-f lavfi -i testsrc2 -t 1 -c hevc_v4l2m2m -f null -" +const ProbeVAAPIH264 = "-init_hw_device vaapi -f lavfi -i testsrc2 -t 1 -vf format=nv12,hwupload -c h264_vaapi -f null -" +const ProbeVAAPIH265 = "-init_hw_device vaapi -f lavfi -i testsrc2 -t 1 -vf format=nv12,hwupload -c hevc_vaapi -f null -" +const ProbeVAAPIJPEG = "-init_hw_device vaapi -f lavfi -i testsrc2 -t 1 -vf format=nv12,hwupload -c mjpeg_vaapi -f null -" +const ProbeCUDAH264 = "-init_hw_device cuda -f lavfi -i testsrc2 -t 1 -c h264_nvenc -f null -" +const ProbeCUDAH265 = "-init_hw_device cuda -f lavfi -i testsrc2 -t 1 -c hevc_nvenc -f null -" + +func ProbeAll(bin string) []api.Stream { + if runtime.GOARCH == "arm64" || runtime.GOARCH == "arm" { + return []api.Stream{ + { + Name: runToString(bin, ProbeV4L2M2MH264), + URL: "ffmpeg:...#video=h264#hardware=" + EngineV4L2M2M, + }, + { + Name: runToString(bin, ProbeV4L2M2MH265), + URL: "ffmpeg:...#video=h265#hardware=" + EngineV4L2M2M, + }, + } + } + + return []api.Stream{ + { + Name: runToString(bin, ProbeVAAPIH264), + URL: "ffmpeg:...#video=h264#hardware=" + EngineVAAPI, + }, + { + Name: runToString(bin, ProbeVAAPIH265), + URL: "ffmpeg:...#video=h265#hardware=" + EngineVAAPI, + }, + { + Name: runToString(bin, ProbeVAAPIJPEG), + URL: "ffmpeg:...#video=mjpeg#hardware=" + EngineVAAPI, + }, + { + Name: runToString(bin, ProbeCUDAH264), + URL: "ffmpeg:...#video=h264#hardware=" + EngineCUDA, + }, + { + Name: runToString(bin, ProbeCUDAH265), + URL: "ffmpeg:...#video=h265#hardware=" + EngineCUDA, + }, + } +} + +func ProbeHardware(bin, name string) string { + if runtime.GOARCH == "arm64" || runtime.GOARCH == "arm" { + switch name { + case "h264": + if run(bin, ProbeV4L2M2MH264) { + return EngineV4L2M2M + } + case "h265": + if run(bin, ProbeV4L2M2MH265) { + return EngineV4L2M2M + } + } + + return EngineSoftware + } + + switch name { + case "h264": + if run(bin, ProbeCUDAH264) { + return EngineCUDA + } + if run(bin, ProbeVAAPIH264) { + return EngineVAAPI + } + + case "h265": + if run(bin, ProbeCUDAH265) { + return EngineCUDA + } + if run(bin, ProbeVAAPIH265) { + return EngineVAAPI + } + + case "mjpeg": + if run(bin, ProbeVAAPIJPEG) { + return EngineVAAPI + } + } + + return EngineSoftware +} diff --git a/internal/ffmpeg/hardware/hardware_windows.go b/internal/ffmpeg/hardware/hardware_windows.go new file mode 100644 index 00000000..6a8898f2 --- /dev/null +++ b/internal/ffmpeg/hardware/hardware_windows.go @@ -0,0 +1,61 @@ +package hardware + +import "github.com/AlexxIT/go2rtc/internal/api" + +const ProbeDXVA2H264 = "-init_hw_device dxva2 -f lavfi -i testsrc2 -t 1 -c h264_qsv -f null -" +const ProbeDXVA2H265 = "-init_hw_device dxva2 -f lavfi -i testsrc2 -t 1 -c hevc_qsv -f null -" +const ProbeDXVA2JPEG = "-init_hw_device dxva2 -f lavfi -i testsrc2 -t 1 -c mjpeg_qsv -f null -" +const ProbeCUDAH264 = "-init_hw_device cuda -f lavfi -i testsrc2 -t 1 -c h264_nvenc -f null -" +const ProbeCUDAH265 = "-init_hw_device cuda -f lavfi -i testsrc2 -t 1 -c hevc_nvenc -f null -" + +func ProbeAll(bin string) []api.Stream { + return []api.Stream{ + { + Name: runToString(bin, ProbeDXVA2H264), + URL: "ffmpeg:...#video=h264#hardware=" + EngineDXVA2, + }, + { + Name: runToString(bin, ProbeDXVA2H265), + URL: "ffmpeg:...#video=h265#hardware=" + EngineDXVA2, + }, + { + Name: runToString(bin, ProbeDXVA2JPEG), + URL: "ffmpeg:...#video=mjpeg#hardware=" + EngineDXVA2, + }, + { + Name: runToString(bin, ProbeCUDAH264), + URL: "ffmpeg:...#video=h264#hardware=" + EngineCUDA, + }, + { + Name: runToString(bin, ProbeCUDAH265), + URL: "ffmpeg:...#video=h265#hardware=" + EngineCUDA, + }, + } +} + +func ProbeHardware(bin, name string) string { + switch name { + case "h264": + if run(bin, ProbeCUDAH264) { + return EngineCUDA + } + if run(bin, ProbeDXVA2H264) { + return EngineDXVA2 + } + + case "h265": + if run(bin, ProbeCUDAH265) { + return EngineCUDA + } + if run(bin, ProbeDXVA2H265) { + return EngineDXVA2 + } + + case "mjpeg": + if run(bin, ProbeDXVA2JPEG) { + return EngineDXVA2 + } + } + + return EngineSoftware +} diff --git a/internal/ffmpeg/hardware_darwin.go b/internal/ffmpeg/hardware_darwin.go deleted file mode 100644 index fb4a7170..00000000 --- a/internal/ffmpeg/hardware_darwin.go +++ /dev/null @@ -1,21 +0,0 @@ -package ffmpeg - -func ProbeHardware(name string) string { - switch name { - case "h264": - if run( - "-f", "lavfi", "-i", "testsrc2", "-t", "1", - "-c", "h264_videotoolbox", "-f", "null", "-") { - return EngineVideoToolbox - } - - case "h265": - if run( - "-f", "lavfi", "-i", "testsrc2", "-t", "1", - "-c", "hevc_videotoolbox", "-f", "null", "-") { - return EngineVideoToolbox - } - } - - return EngineSoftware -} diff --git a/internal/ffmpeg/hardware_linux.go b/internal/ffmpeg/hardware_linux.go deleted file mode 100644 index 00839bd1..00000000 --- a/internal/ffmpeg/hardware_linux.go +++ /dev/null @@ -1,67 +0,0 @@ -package ffmpeg - -import ( - "runtime" -) - -func ProbeHardware(name string) string { - if runtime.GOARCH == "arm64" || runtime.GOARCH == "arm" { - switch name { - case "h264": - if run( - "-f", "lavfi", "-i", "testsrc2", "-t", "1", - "-c", "h264_v4l2m2m", "-f", "null", "-") { - return EngineV4L2M2M - } - - case "h265": - if run( - "-f", "lavfi", "-i", "testsrc2", "-t", "1", - "-c", "hevc_v4l2m2m", "-f", "null", "-") { - return EngineV4L2M2M - } - } - - return EngineSoftware - } - - switch name { - case "h264": - if run("-init_hw_device", "cuda", - "-f", "lavfi", "-i", "testsrc2", "-t", "1", - "-c", "h264_nvenc", "-f", "null", "-") { - return EngineCUDA - } - - if run("-init_hw_device", "vaapi", - "-f", "lavfi", "-i", "testsrc2", "-t", "1", - "-vf", "format=nv12,hwupload", - "-c", "h264_vaapi", "-f", "null", "-") { - return EngineVAAPI - } - - case "h265": - if run("-init_hw_device", "cuda", - "-f", "lavfi", "-i", "testsrc2", "-t", "1", - "-c", "hevc_nvenc", "-f", "null", "-") { - return EngineCUDA - } - - if run("-init_hw_device", "vaapi", - "-f", "lavfi", "-i", "testsrc2", "-t", "1", - "-vf", "format=nv12,hwupload", - "-c", "hevc_vaapi", "-f", "null", "-") { - return EngineVAAPI - } - - case "mjpeg": - if run("-init_hw_device", "vaapi", - "-f", "lavfi", "-i", "testsrc2", "-t", "1", - "-vf", "format=nv12,hwupload", - "-c", "mjpeg_vaapi", "-f", "null", "-") { - return EngineVAAPI - } - } - - return EngineSoftware -} diff --git a/internal/ffmpeg/hardware_windows.go b/internal/ffmpeg/hardware_windows.go deleted file mode 100644 index 4a259fe6..00000000 --- a/internal/ffmpeg/hardware_windows.go +++ /dev/null @@ -1,40 +0,0 @@ -package ffmpeg - -func ProbeHardware(name string) string { - switch name { - case "h264": - if run("-init_hw_device", "cuda", - "-f", "lavfi", "-i", "testsrc2", "-t", "1", - "-c", "h264_nvenc", "-f", "null", "-") { - return EngineCUDA - } - - if run("-init_hw_device", "dxva2", - "-f", "lavfi", "-i", "testsrc2", "-t", "1", - "-c", "h264_qsv", "-f", "null", "-") { - return EngineDXVA2 - } - - case "h265": - if run("-init_hw_device", "cuda", - "-f", "lavfi", "-i", "testsrc2", "-t", "1", - "-c", "hevc_nvenc", "-f", "null", "-") { - return EngineCUDA - } - - if run("-init_hw_device", "dxva2", - "-f", "lavfi", "-i", "testsrc2", "-t", "1", - "-c", "hevc_qsv", "-f", "null", "-") { - return EngineDXVA2 - } - - case "mjpeg": - if run("-init_hw_device", "dxva2", - "-f", "lavfi", "-i", "testsrc2", "-t", "1", - "-c", "mjpeg_qsv", "-f", "null", "-") { - return EngineDXVA2 - } - } - - return EngineSoftware -} diff --git a/pkg/ffmpeg/ffmpeg.go b/pkg/ffmpeg/ffmpeg.go new file mode 100644 index 00000000..10fb5bed --- /dev/null +++ b/pkg/ffmpeg/ffmpeg.go @@ -0,0 +1,80 @@ +package ffmpeg + +import ( + "bytes" + "strconv" + "strings" +) + +type Args struct { + Bin string // ffmpeg + Global string // -hide_banner -v error + Input string // -re -stream_loop -1 -i /media/bunny.mp4 + Codecs []string // -c:v libx264 -g:v 30 -preset:v ultrafast -tune:v zerolatency + Filters []string // scale=1920:1080 + Output string // -f rtsp {output} + + Video, Audio int // count of Video and Audio params +} + +func (a *Args) AddCodec(codec string) { + a.Codecs = append(a.Codecs, codec) +} + +func (a *Args) AddFilter(filter string) { + a.Filters = append(a.Filters, filter) +} + +func (a *Args) InsertFilter(filter string) { + a.Filters = append([]string{filter}, a.Filters...) +} + +func (a *Args) String() string { + b := bytes.NewBuffer(make([]byte, 0, 512)) + + b.WriteString(a.Bin) + + if a.Global != "" { + b.WriteByte(' ') + b.WriteString(a.Global) + } + + b.WriteByte(' ') + b.WriteString(a.Input) + + multimode := a.Video > 1 || a.Audio > 1 + var iv, ia int + + for _, codec := range a.Codecs { + // support multiple video and/or audio codecs + if multimode && len(codec) >= 5 { + switch codec[:5] { + case "-c:v ": + codec = "-map 0:v:0? " + strings.ReplaceAll(codec, ":v ", ":v:"+strconv.Itoa(iv)+" ") + iv++ + case "-c:a ": + codec = "-map 0:a:0? " + strings.ReplaceAll(codec, ":a ", ":a:"+strconv.Itoa(ia)+" ") + ia++ + } + } + + b.WriteByte(' ') + b.WriteString(codec) + } + + if a.Filters != nil { + for i, filter := range a.Filters { + if i == 0 { + b.WriteString(" -vf ") + } else { + b.WriteByte(',') + } + b.WriteString(filter) + } + } + + b.WriteByte(' ') + b.WriteString(a.Output) + + return b.String() +} diff --git a/www/add.html b/www/add.html index 7e3ce3b6..06daa33d 100644 --- a/www/add.html +++ b/www/add.html @@ -184,6 +184,19 @@ + +
    + +
    +
    + + +
    From e78f9fa69de54b40c8fa46ebf5a28a8dc2d71636 Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Wed, 3 May 2023 07:49:10 +0300 Subject: [PATCH 59/80] Add support pipe to exec source --- internal/exec/exec.go | 38 +++++---- pkg/core/helpers.go | 7 ++ pkg/mpegts/helpers.go | 2 +- pkg/mpegts/reader.go | 8 ++ pkg/pipe/client.go | 188 ++++++++++++++++++++++++++++++++++++++++++ pkg/pipe/producer.go | 51 ++++++++++++ 6 files changed, 277 insertions(+), 17 deletions(-) create mode 100644 pkg/pipe/client.go create mode 100644 pkg/pipe/producer.go diff --git a/internal/exec/exec.go b/internal/exec/exec.go index b77eb4c4..d2003a55 100644 --- a/internal/exec/exec.go +++ b/internal/exec/exec.go @@ -9,22 +9,17 @@ import ( "github.com/AlexxIT/go2rtc/internal/rtsp" "github.com/AlexxIT/go2rtc/internal/streams" "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/AlexxIT/go2rtc/pkg/pipe" pkg "github.com/AlexxIT/go2rtc/pkg/rtsp" "github.com/AlexxIT/go2rtc/pkg/shell" "github.com/rs/zerolog" "os" "os/exec" - "strings" "sync" "time" ) func Init() { - // depends on RTSP server - if rtsp.Port == "" { - return - } - rtsp.HandleFunc(func(conn *pkg.Conn) bool { waitersMu.Lock() waiter := waiters[conn.URL.Path] @@ -49,23 +44,34 @@ func Init() { } func Handle(url string) (core.Producer, error) { - sum := md5.Sum([]byte(url)) - path := "/" + hex.EncodeToString(sum[:]) + var path string - url = strings.Replace( - url, "{output}", "rtsp://127.0.0.1:"+rtsp.Port+path, 1, - ) + args := shell.QuoteSplit(url[5:]) // remove `exec:` + for i, arg := range args { + if arg == "{output}" { + if rtsp.Port == "" { + return nil, errors.New("rtsp module disabled") + } + + sum := md5.Sum([]byte(url)) + path = "/" + hex.EncodeToString(sum[:]) + args[i] = "rtsp://127.0.0.1:" + rtsp.Port + path + break + } + } - // remove `exec:` - args := shell.QuoteSplit(url[5:]) cmd := exec.Command(args[0], args[1:]...) + if log.Debug().Enabled() { + cmd.Stderr = os.Stderr + } + + if path == "" { + return pipe.NewClient(cmd) + } if log.Trace().Enabled() { cmd.Stdout = os.Stdout } - if log.Debug().Enabled() { - cmd.Stderr = os.Stderr - } ch := make(chan core.Producer) diff --git a/pkg/core/helpers.go b/pkg/core/helpers.go index 44d24953..0cb78aba 100644 --- a/pkg/core/helpers.go +++ b/pkg/core/helpers.go @@ -6,8 +6,15 @@ import ( "runtime" "strconv" "strings" + "time" ) +// Now90000 - timestamp for Video (clock rate = 90000 samples per second) +// same as: uint32(time.Duration(time.Now().UnixNano()) * 90000 / time.Second) +func Now90000() uint32 { + return uint32(time.Duration(time.Now().UnixMilli()) * 90) +} + const symbols = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_" // RandString base10 - numbers, base16 - hex, base36 - digits+letters, base64 - URL safe symbols diff --git a/pkg/mpegts/helpers.go b/pkg/mpegts/helpers.go index 037078a7..673deeeb 100644 --- a/pkg/mpegts/helpers.go +++ b/pkg/mpegts/helpers.go @@ -137,7 +137,7 @@ func (p *PES) GetPacket() (pkt *rtp.Packet) { pkt = &rtp.Packet{ Header: rtp.Header{ PayloadType: p.StreamType, - Timestamp: uint32(time.Duration(time.Now().UnixNano()) * 90000 / time.Second), + Timestamp: core.Now90000(), }, Payload: payload, } diff --git a/pkg/mpegts/reader.go b/pkg/mpegts/reader.go index 66607992..c38b35dd 100644 --- a/pkg/mpegts/reader.go +++ b/pkg/mpegts/reader.go @@ -129,6 +129,14 @@ func (r *Reader) GetPacket() *rtp.Packet { return nil } +func (r *Reader) GetStreamTypes() []byte { + types := make([]byte, 0, len(r.pes)) + for _, pes := range r.pes { + types = append(types, pes.StreamType) + } + return types +} + // Sync - search sync byte func (r *Reader) Sync() bool { // drop previous readed packet diff --git a/pkg/pipe/client.go b/pkg/pipe/client.go new file mode 100644 index 00000000..91265010 --- /dev/null +++ b/pkg/pipe/client.go @@ -0,0 +1,188 @@ +package pipe + +import ( + "bytes" + "encoding/hex" + "errors" + "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/AlexxIT/go2rtc/pkg/h264" + "github.com/AlexxIT/go2rtc/pkg/mpegts" + "github.com/pion/rtp" + "io" + "os/exec" +) + +type Client struct { + cmd *exec.Cmd + stdout io.ReadCloser + sniff []byte + handle func() error + + medias []*core.Media + receiver *core.Receiver + + recv int +} + +func NewClient(cmd *exec.Cmd) (prod *Client, err error) { + prod = &Client{cmd: cmd} + + prod.stdout, err = cmd.StdoutPipe() + if err != nil { + return nil, err + } + + if err = cmd.Start(); err != nil { + return nil, err + } + + prod.sniff = make([]byte, mpegts.PacketSize*3) // MPEG-TS: SDT+PAT+PMT + prod.recv, err = io.ReadFull(prod.stdout, prod.sniff) + if err != nil { + _ = prod.Stop() + return nil, err + } + + var codec *core.Codec + + if bytes.HasPrefix(prod.sniff, []byte{0, 0, 0, 1}) { + switch { + case h264.NALUType(prod.sniff) == h264.NALUTypeSPS: + codec = &core.Codec{ + Name: core.CodecH264, + ClockRate: 90000, + PayloadType: core.PayloadTypeRAW, + } + prod.handle = prod.ReadBitstreams + } + } else if bytes.HasPrefix(prod.sniff, []byte{0xFF, 0xD8}) { + codec = &core.Codec{ + Name: core.CodecJPEG, + ClockRate: 90000, + PayloadType: core.PayloadTypeRAW, + } + prod.handle = prod.ReadMJPEG + } else if prod.sniff[0] == mpegts.SyncByte { + ts := mpegts.NewReader() + ts.AppendBuffer(prod.sniff) + _ = ts.GetPacket() + for _, streamType := range ts.GetStreamTypes() { + switch streamType { + case mpegts.StreamTypeH264: + codec = &core.Codec{ + Name: core.CodecH264, + ClockRate: 90000, + PayloadType: core.PayloadTypeRAW, + } + prod.handle = prod.ReadMPEGTS + } + } + } + + if codec == nil { + _ = prod.Stop() + return nil, errors.New("unknown format: " + hex.EncodeToString(prod.sniff)) + } + + prod.medias = append(prod.medias, &core.Media{ + Kind: core.KindVideo, + Direction: core.DirectionRecvonly, + Codecs: []*core.Codec{codec}, + }) + + return +} + +func (c *Client) ReadBitstreams() error { + buf := c.sniff // total bufer + b := make([]byte, 1024*1024) // reading buffer + + for { + payload, n := h264.DecodeStream(buf) + if payload == nil { + n, err := c.stdout.Read(b) + if err != nil { + return err + } + + buf = append(buf, b[:n]...) + c.recv += n + continue + } + + buf = buf[n:] + + //log.Printf("[AVC] %v, len: %d", h264.Types(payload), len(payload)) + + pkt := &rtp.Packet{ + Header: rtp.Header{Timestamp: core.Now90000()}, + Payload: payload, + } + c.receiver.WriteRTP(pkt) + } +} + +func (c *Client) ReadMJPEG() error { + buf := c.sniff // total bufer + b := make([]byte, 1024*1024) // reading buffer + + for { + // one JPEG end and next start + i := bytes.Index(buf, []byte{0xFF, 0xD9, 0xFF, 0xD8}) + if i < 0 { + n, err := c.stdout.Read(b) + if err != nil { + return err + } + + buf = append(buf, b[:n]...) + c.recv += n + + // if we receive frame + if n >= 2 && b[n-2] == 0xFF && b[n-1] == 0xD9 { + i = len(buf) + } else { + continue + } + } else { + i += 2 + } + + pkt := &rtp.Packet{ + Header: rtp.Header{Timestamp: core.Now90000()}, + Payload: buf[:i], + } + c.receiver.WriteRTP(pkt) + + buf = buf[i:] + } +} + +func (c *Client) ReadMPEGTS() error { + b := make([]byte, 1024*1024) // reading buffer + + ts := mpegts.NewReader() + ts.AppendBuffer(c.sniff) + + for { + packet := ts.GetPacket() + if packet == nil { + n, err := c.stdout.Read(b) + if err != nil { + return err + } + + ts.AppendBuffer(b[:n]) + c.recv += n + continue + } + + //log.Printf("[AVC] %v, len: %d, ts: %10d", h264.Types(packet.Payload), len(packet.Payload), packet.Timestamp) + + if packet.PayloadType != mpegts.StreamTypeH264 { + continue + } + + c.receiver.WriteRTP(packet) + } +} diff --git a/pkg/pipe/producer.go b/pkg/pipe/producer.go new file mode 100644 index 00000000..c2d5afdd --- /dev/null +++ b/pkg/pipe/producer.go @@ -0,0 +1,51 @@ +package pipe + +import ( + "encoding/json" + "github.com/AlexxIT/go2rtc/pkg/core" + "strings" +) + +func (c *Client) GetMedias() []*core.Media { + 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 { + return c.handle() +} + +func (c *Client) Stop() (err error) { + if c.receiver != nil { + c.receiver.Close() + } + if err1 := c.stdout.Close(); err != nil { + err = err1 + } + if err1 := c.cmd.Process.Kill(); err != nil { + err = err1 + } + if err1 := c.cmd.Wait(); err != nil { + err = err1 + } + return +} + +func (c *Client) MarshalJSON() ([]byte, error) { + info := &core.Info{ + Type: "PIPE active producer", + URL: c.cmd.Path + " " + strings.Join(c.cmd.Args, " "), + Medias: c.medias, + Recv: c.recv, + } + if c.receiver != nil { + info.Receivers = append(info.Receivers, c.receiver) + } + return json.Marshal(info) +} From 465608698581bb2d3c4e240a80877ce4ff632499 Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Wed, 3 May 2023 07:49:48 +0300 Subject: [PATCH 60/80] Add auto transcoding to JPEG snapshot --- internal/ffmpeg/helpers.go | 12 ++++ internal/mjpeg/{mjpeg.go => init.go} | 25 ++++++-- pkg/h264/avc.go | 13 ++++ pkg/pipe/keyframe.go | 91 ++++++++++++++++++++++++++++ 4 files changed, 137 insertions(+), 4 deletions(-) create mode 100644 internal/ffmpeg/helpers.go rename internal/mjpeg/{mjpeg.go => init.go} (87%) create mode 100644 pkg/pipe/keyframe.go diff --git a/internal/ffmpeg/helpers.go b/internal/ffmpeg/helpers.go new file mode 100644 index 00000000..30c3520d --- /dev/null +++ b/internal/ffmpeg/helpers.go @@ -0,0 +1,12 @@ +package ffmpeg + +import ( + "bytes" + "os/exec" +) + +func TranscodeToJPEG(b []byte) ([]byte, error) { + cmd := exec.Command("ffmpeg", "-hide_banner", "-i", "-", "-f", "mjpeg", "-") + cmd.Stdin = bytes.NewBuffer(b) + return cmd.Output() +} diff --git a/internal/mjpeg/mjpeg.go b/internal/mjpeg/init.go similarity index 87% rename from internal/mjpeg/mjpeg.go rename to internal/mjpeg/init.go index 30dd97d0..c3945674 100644 --- a/internal/mjpeg/mjpeg.go +++ b/internal/mjpeg/init.go @@ -3,13 +3,17 @@ package mjpeg import ( "errors" "github.com/AlexxIT/go2rtc/internal/api" + "github.com/AlexxIT/go2rtc/internal/ffmpeg" "github.com/AlexxIT/go2rtc/internal/streams" + "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/mjpeg" + "github.com/AlexxIT/go2rtc/pkg/pipe" "github.com/AlexxIT/go2rtc/pkg/tcp" "github.com/rs/zerolog/log" "io" "net/http" "strconv" + "time" ) func Init() { @@ -29,14 +33,16 @@ func handlerKeyframe(w http.ResponseWriter, r *http.Request) { exit := make(chan []byte) - cons := &mjpeg.Consumer{ + cons := &pipe.Keyframe{ RemoteAddr: tcp.RemoteAddr(r), UserAgent: r.UserAgent(), } cons.Listen(func(msg any) { - switch msg := msg.(type) { - case []byte: - exit <- msg + if b, ok := msg.([]byte); ok { + select { + case exit <- b: + default: + } } }) @@ -49,6 +55,17 @@ func handlerKeyframe(w http.ResponseWriter, r *http.Request) { stream.RemoveConsumer(cons) + switch cons.CodecName() { + case core.CodecH264, core.CodecH265: + ts := time.Now() + var err error + if data, err = ffmpeg.TranscodeToJPEG(data); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + log.Debug().Msgf("[mjpeg] transcoding time=%s", time.Since(ts)) + } + h := w.Header() h.Set("Content-Type", "image/jpeg") h.Set("Content-Length", strconv.Itoa(len(data))) diff --git a/pkg/h264/avc.go b/pkg/h264/avc.go index 99fd4598..c21b2f11 100644 --- a/pkg/h264/avc.go +++ b/pkg/h264/avc.go @@ -26,6 +26,19 @@ func AnnexB2AVC(b []byte) []byte { return b } +func AVCtoAnnexB(b []byte) []byte { + b = bytes.Clone(b) + for i := 0; i < len(b); { + size := int(binary.BigEndian.Uint32(b[i:])) + b[i] = 0 + b[i+1] = 0 + b[i+2] = 0 + b[i+3] = 1 + i += 4 + size + } + return b +} + const forbiddenZeroBit = 0x80 const nalUnitType = 0x1F diff --git a/pkg/pipe/keyframe.go b/pkg/pipe/keyframe.go new file mode 100644 index 00000000..0126024e --- /dev/null +++ b/pkg/pipe/keyframe.go @@ -0,0 +1,91 @@ +package pipe + +import ( + "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/AlexxIT/go2rtc/pkg/h264" + "github.com/AlexxIT/go2rtc/pkg/h265" + "github.com/AlexxIT/go2rtc/pkg/mjpeg" + "github.com/pion/rtp" +) + +type Keyframe struct { + core.Listener + + UserAgent string + RemoteAddr string + + medias []*core.Media + sender *core.Sender +} + +func (k *Keyframe) GetMedias() []*core.Media { + if k.medias == nil { + k.medias = append(k.medias, &core.Media{ + Kind: core.KindVideo, + Direction: core.DirectionSendonly, + Codecs: []*core.Codec{ + {Name: core.CodecH264}, + {Name: core.CodecH265}, + {Name: core.CodecJPEG}, + }, + }) + } + return k.medias +} + +func (k *Keyframe) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiver) error { + var handler core.HandlerFunc + + switch track.Codec.Name { + case core.CodecH264: + handler = func(packet *rtp.Packet) { + if !h264.IsKeyframe(packet.Payload) { + return + } + b := h264.AVCtoAnnexB(packet.Payload) + k.Fire(b) + } + + if track.Codec.IsRTP() { + handler = h264.RTPDepay(track.Codec, handler) + } + case core.CodecH265: + handler = func(packet *rtp.Packet) { + if !h265.IsKeyframe(packet.Payload) { + return + } + k.Fire(packet.Payload) + } + + if track.Codec.IsRTP() { + handler = h265.RTPDepay(track.Codec, handler) + } + case core.CodecJPEG: + handler = func(packet *rtp.Packet) { + k.Fire(packet.Payload) + } + + if track.Codec.IsRTP() { + handler = mjpeg.RTPDepay(handler) + } + } + + k.sender = core.NewSender(media, track.Codec) + k.sender.Handler = handler + k.sender.HandleRTP(track) + return nil +} + +func (k *Keyframe) CodecName() string { + if k.sender != nil { + return k.sender.Codec.Name + } + return "" +} + +func (k *Keyframe) Stop() error { + if k.sender != nil { + k.sender.Close() + } + return nil +} From 3c371e70465933c8a96cb9646db4680fb861d313 Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Thu, 4 May 2023 07:38:49 +0300 Subject: [PATCH 61/80] Change FFmpeg output for MJPEG to pipe --- internal/ffmpeg/device/devices.go | 4 ++-- internal/ffmpeg/ffmpeg.go | 13 +++++++++++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/internal/ffmpeg/device/devices.go b/internal/ffmpeg/device/devices.go index a02cb896..3e657906 100644 --- a/internal/ffmpeg/device/devices.go +++ b/internal/ffmpeg/device/devices.go @@ -33,10 +33,10 @@ func GetInput(src string) (string, error) { video = value[0] case "audio": audio = value[0] - case "framerate": - input += " -framerate " + value[0] case "resolution": input += " -video_size " + value[0] + default: // "input_format", "framerate", "video_size" + input += " -" + key + " " + value[0] } } } diff --git a/internal/ffmpeg/ffmpeg.go b/internal/ffmpeg/ffmpeg.go index c69ad822..51d6c74a 100644 --- a/internal/ffmpeg/ffmpeg.go +++ b/internal/ffmpeg/ffmpeg.go @@ -51,14 +51,16 @@ var defaults = map[string]string{ "rtsp/udp": "-fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -i {input}", // output - "output": "-user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}", + "output": "-user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}", + "output/mjpeg": "-f mjpeg -", // `-preset superfast` - we can't use ultrafast because it doesn't support `-profile main -level 4.1` // `-tune zerolatency` - for minimal latency // `-profile high -level 4.1` - most used streaming profile "h264": "-c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuvj420p", "h265": "-c:v libx265 -g 50 -profile:v high -level:v 5.1 -preset:v superfast -tune:v zerolatency", - "mjpeg": "-c:v mjpeg -force_duplicated_matrix:v 1 -huffman:v 0 -pix_fmt:v yuvj420p", + "mjpeg": "-c:v mjpeg", + //"mjpeg": "-c:v mjpeg -force_duplicated_matrix:v 1 -huffman:v 0 -pix_fmt:v yuvj420p", // https://ffmpeg.org/ffmpeg-codecs.html#libopus-1 "opus": "-c:a libopus -ar:a 48000 -ac:a 2 -application:a voip -compression_level:a 0", @@ -268,6 +270,13 @@ func parseArgs(s string) *ffmpeg.Args { args.AddCodec("-c copy") } + // transcoding to only mjpeg + if (args.Video == 1 && args.Audio == 0 && query.Get("video") == "mjpeg") || + // no transcoding from mjpeg input + (args.Video == 0 && args.Audio == 0 && strings.Contains(args.Input, " mjpeg ")) { + args.Output = defaults["output/mjpeg"] + } + return args } From 03968d2f2ee295088c23704c8c71808a719e30dd Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Thu, 4 May 2023 07:39:15 +0300 Subject: [PATCH 62/80] Restore hadrware transcoding for MJPEG --- internal/ffmpeg/ffmpeg_test.go | 11 ++++++++++- internal/ffmpeg/hardware/hardware.go | 8 ++++---- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/internal/ffmpeg/ffmpeg_test.go b/internal/ffmpeg/ffmpeg_test.go index 6fdd9a91..c778babf 100644 --- a/internal/ffmpeg/ffmpeg_test.go +++ b/internal/ffmpeg/ffmpeg_test.go @@ -7,8 +7,17 @@ import ( func TestParseArgs(t *testing.T) { args := parseArgs("rtsp://example.com#video=h264#rotate=180") - assert.Equal(t, "ffmpeg -hide_banner -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_transport tcp -i rtsp://example.com -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -an -vf transpose=1,transpose=1 -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}", args.String()) + assert.Equal(t, "ffmpeg -hide_banner -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_transport tcp -i rtsp://example.com -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuvj420p -an -vf transpose=1,transpose=1 -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}", args.String()) args = parseArgs("rtsp://example.com#video=h264#rotate=180#hardware=vaapi") assert.Equal(t, "ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_transport tcp -i rtsp://example.com -c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0 -an -vf format=vaapi|nv12,hwupload,transpose_vaapi=4 -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}", args.String()) + + args = parseArgs("/media/bbb.mp4#video=mjpeg") + assert.Equal(t, "ffmpeg -hide_banner -re -i /media/bbb.mp4 -c:v mjpeg -an -f mjpeg -", args.String()) + + args = parseArgs("/media/bbb.mp4#video=mjpeg#hardware=vaapi") + assert.Equal(t, "ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -re -i /media/bbb.mp4 -c:v mjpeg_vaapi -an -vf format=vaapi|nv12,hwupload -f mjpeg -", args.String()) + + args = parseArgs("device?video=0&input_format=mjpeg&video_size=1920x1080") + assert.Equal(t, `ffmpeg -hide_banner -f dshow -input_format mjpeg -video_size 1920x1080 -i video="0" -c copy -f mjpeg -`, args.String()) } diff --git a/internal/ffmpeg/hardware/hardware.go b/internal/ffmpeg/hardware/hardware.go index e560ac54..bb8f7174 100644 --- a/internal/ffmpeg/hardware/hardware.go +++ b/internal/ffmpeg/hardware/hardware.go @@ -29,8 +29,8 @@ func Init(bin string) { // empty engine for autoselect func MakeHardware(args *ffmpeg.Args, engine string, defaults map[string]string) { for i, codec := range args.Codecs { - if len(codec) < 12 { - continue // skip short line (-c:v libx264...) + if len(codec) < 10 { + continue // skip short line (-c:v mjpeg...) } // get current codec name @@ -45,8 +45,8 @@ func MakeHardware(args *ffmpeg.Args, engine string, defaults map[string]string) continue // skip unsupported codec } - // temporary disable probe for H265 and MJPEG - if engine == "" && name == "h264" { + // temporary disable probe for H265 + if engine == "" && name != "h265" { if engine = cache[name]; engine == "" { engine = ProbeHardware(args.Bin, name) cache[name] = engine From d44efb84a00746404960aec09a168d254104a017 Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Thu, 4 May 2023 11:49:38 +0300 Subject: [PATCH 63/80] Fix buffer size for mpegts --- pkg/mpegts/client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/mpegts/client.go b/pkg/mpegts/client.go index 4ef41813..16bc5420 100644 --- a/pkg/mpegts/client.go +++ b/pkg/mpegts/client.go @@ -23,7 +23,7 @@ func NewClient(res *http.Response) *Client { func (c *Client) Handle() error { reader := NewReader() - b := make([]byte, 1024*1024*256) // 256K + b := make([]byte, 1024*256) // 256K probe := core.NewProbe(c.medias == nil) for probe == nil || probe.Active() { From b5f4c7f75b17f13b7a99d62286125b41d5ef31d0 Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Thu, 4 May 2023 11:52:36 +0300 Subject: [PATCH 64/80] Rewrite exec pipe, TCP and HTTP sources --- internal/exec/exec.go | 33 ++++++++++-- internal/exec/pipe.go | 26 ++++++++++ internal/ffmpeg/ffmpeg.go | 3 +- internal/http/http.go | 55 +++++++++++++++----- internal/mjpeg/init.go | 4 +- internal/tcp/init.go | 35 ------------- main.go | 2 - pkg/core/helpers.go | 9 ++++ pkg/h265/avc.go | 54 +++++++++++++++++++ pkg/{pipe => magic}/client.go | 91 ++++++++++++++++++++------------- pkg/{pipe => magic}/keyframe.go | 2 +- pkg/{pipe => magic}/producer.go | 20 ++------ 12 files changed, 224 insertions(+), 110 deletions(-) create mode 100644 internal/exec/pipe.go delete mode 100644 internal/tcp/init.go create mode 100644 pkg/h265/avc.go rename pkg/{pipe => magic}/client.go (64%) rename pkg/{pipe => magic}/keyframe.go (99%) rename pkg/{pipe => magic}/producer.go (67%) diff --git a/internal/exec/exec.go b/internal/exec/exec.go index d2003a55..1b7842dd 100644 --- a/internal/exec/exec.go +++ b/internal/exec/exec.go @@ -9,7 +9,7 @@ import ( "github.com/AlexxIT/go2rtc/internal/rtsp" "github.com/AlexxIT/go2rtc/internal/streams" "github.com/AlexxIT/go2rtc/pkg/core" - "github.com/AlexxIT/go2rtc/pkg/pipe" + "github.com/AlexxIT/go2rtc/pkg/magic" pkg "github.com/AlexxIT/go2rtc/pkg/rtsp" "github.com/AlexxIT/go2rtc/pkg/shell" "github.com/rs/zerolog" @@ -38,12 +38,12 @@ func Init() { } }) - streams.HandleFunc("exec", Handle) + streams.HandleFunc("exec", execHandle) log = app.GetLogger("exec") } -func Handle(url string) (core.Producer, error) { +func execHandle(url string) (core.Producer, error) { var path string args := shell.QuoteSplit(url[5:]) // remove `exec:` @@ -66,9 +66,34 @@ func Handle(url string) (core.Producer, error) { } if path == "" { - return pipe.NewClient(cmd) + return handlePipe(url, cmd) } + return handleRTSP(url, path, cmd) +} + +func handlePipe(url string, cmd *exec.Cmd) (core.Producer, error) { + r, err := PipeCloser(cmd) + if err != nil { + return nil, err + } + + if err = cmd.Start(); err != nil { + return nil, err + } + + client := magic.NewClient(r) + if err = client.Probe(); err != nil { + return nil, err + } + + client.Desc = "exec active producer" + client.URL = url + + return client, nil +} + +func handleRTSP(url, path string, cmd *exec.Cmd) (core.Producer, error) { if log.Trace().Enabled() { cmd.Stdout = os.Stdout } diff --git a/internal/exec/pipe.go b/internal/exec/pipe.go new file mode 100644 index 00000000..de101e04 --- /dev/null +++ b/internal/exec/pipe.go @@ -0,0 +1,26 @@ +package exec + +import ( + "github.com/AlexxIT/go2rtc/pkg/core" + "io" + "os/exec" +) + +// PipeCloser - return StdoutPipe that Kill cmd on Close call +func PipeCloser(cmd *exec.Cmd) (io.ReadCloser, error) { + stdout, err := cmd.StdoutPipe() + if err != nil { + return nil, err + } + + return pipeCloser{stdout, cmd}, nil +} + +type pipeCloser struct { + io.ReadCloser + cmd *exec.Cmd +} + +func (p pipeCloser) Close() error { + return core.Any(p.ReadCloser.Close(), p.cmd.Process.Kill(), p.cmd.Wait()) +} diff --git a/internal/ffmpeg/ffmpeg.go b/internal/ffmpeg/ffmpeg.go index 51d6c74a..d24fcdb0 100644 --- a/internal/ffmpeg/ffmpeg.go +++ b/internal/ffmpeg/ffmpeg.go @@ -3,7 +3,6 @@ package ffmpeg import ( "errors" "github.com/AlexxIT/go2rtc/internal/app" - "github.com/AlexxIT/go2rtc/internal/exec" "github.com/AlexxIT/go2rtc/internal/ffmpeg/device" "github.com/AlexxIT/go2rtc/internal/ffmpeg/hardware" "github.com/AlexxIT/go2rtc/internal/rtsp" @@ -32,7 +31,7 @@ func Init() { if args == nil { return nil, errors.New("can't generate ffmpeg command") } - return exec.Handle("exec:" + args.String()) + return streams.GetProducer("exec:" + args.String()) }) device.Init(defaults["bin"]) diff --git a/internal/http/http.go b/internal/http/http.go index 1bb86a26..6c158196 100644 --- a/internal/http/http.go +++ b/internal/http/http.go @@ -2,24 +2,28 @@ package http import ( "errors" - "fmt" "github.com/AlexxIT/go2rtc/internal/streams" "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/AlexxIT/go2rtc/pkg/magic" "github.com/AlexxIT/go2rtc/pkg/mjpeg" - "github.com/AlexxIT/go2rtc/pkg/mpegts" "github.com/AlexxIT/go2rtc/pkg/rtmp" "github.com/AlexxIT/go2rtc/pkg/tcp" + "net" "net/http" + "net/url" "strings" + "time" ) func Init() { - streams.HandleFunc("http", handle) - streams.HandleFunc("https", handle) - streams.HandleFunc("httpx", handle) + streams.HandleFunc("http", handleHTTP) + streams.HandleFunc("https", handleHTTP) + streams.HandleFunc("httpx", handleHTTP) + + streams.HandleFunc("tcp", handleTCP) } -func handle(url string) (core.Producer, error) { +func handleHTTP(url string) (core.Producer, error) { // first we get the Content-Type to define supported producer req, err := http.NewRequest("GET", url, nil) if err != nil { @@ -54,13 +58,38 @@ func handle(url string) (core.Producer, error) { } return conn, nil - case "video/mpeg": - client := mpegts.NewClient(res) - if err = client.Handle(); err != nil { - return nil, err - } - return client, nil + default: // "video/mpeg": } - return nil, fmt.Errorf("unsupported Content-Type: %s", ct) + client := magic.NewClient(res.Body) + if err = client.Probe(); err != nil { + return nil, err + } + + client.Desc = "HTTP active producer" + client.URL = url + + return client, nil +} + +func handleTCP(rawURL string) (core.Producer, error) { + u, err := url.Parse(rawURL) + if err != nil { + return nil, err + } + + conn, err := net.DialTimeout("tcp", u.Host, time.Second*3) + if err != nil { + return nil, err + } + + client := magic.NewClient(conn) + if err = client.Probe(); err != nil { + return nil, err + } + + client.Desc = "TCP active producer" + client.URL = rawURL + + return client, nil } diff --git a/internal/mjpeg/init.go b/internal/mjpeg/init.go index c3945674..598aae62 100644 --- a/internal/mjpeg/init.go +++ b/internal/mjpeg/init.go @@ -6,8 +6,8 @@ import ( "github.com/AlexxIT/go2rtc/internal/ffmpeg" "github.com/AlexxIT/go2rtc/internal/streams" "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/AlexxIT/go2rtc/pkg/magic" "github.com/AlexxIT/go2rtc/pkg/mjpeg" - "github.com/AlexxIT/go2rtc/pkg/pipe" "github.com/AlexxIT/go2rtc/pkg/tcp" "github.com/rs/zerolog/log" "io" @@ -33,7 +33,7 @@ func handlerKeyframe(w http.ResponseWriter, r *http.Request) { exit := make(chan []byte) - cons := &pipe.Keyframe{ + cons := &magic.Keyframe{ RemoteAddr: tcp.RemoteAddr(r), UserAgent: r.UserAgent(), } diff --git a/internal/tcp/init.go b/internal/tcp/init.go deleted file mode 100644 index 7f712415..00000000 --- a/internal/tcp/init.go +++ /dev/null @@ -1,35 +0,0 @@ -package tcp - -import ( - "github.com/AlexxIT/go2rtc/internal/streams" - "github.com/AlexxIT/go2rtc/pkg/core" - "github.com/AlexxIT/go2rtc/pkg/mpegts" - "net" - "net/http" - "net/url" - "time" -) - -func Init() { - streams.HandleFunc("tcp", handle) -} - -func handle(rawURL string) (core.Producer, error) { - u, err := url.Parse(rawURL) - if err != nil { - return nil, err - } - - conn, err := net.DialTimeout("tcp", u.Host, time.Second*3) - if err != nil { - return nil, err - } - - req := &http.Request{URL: u} - res := &http.Response{Body: conn, Request: req} - client := mpegts.NewClient(res) - if err := client.Handle(); err != nil { - return nil, err - } - return client, nil -} diff --git a/main.go b/main.go index c637fdc8..973537cd 100644 --- a/main.go +++ b/main.go @@ -25,7 +25,6 @@ import ( "github.com/AlexxIT/go2rtc/internal/srtp" "github.com/AlexxIT/go2rtc/internal/streams" "github.com/AlexxIT/go2rtc/internal/tapo" - "github.com/AlexxIT/go2rtc/internal/tcp" "github.com/AlexxIT/go2rtc/internal/webrtc" "github.com/AlexxIT/go2rtc/internal/webtorrent" "os" @@ -52,7 +51,6 @@ func main() { isapi.Init() mpegts.Init() roborock.Init() - tcp.Init() srtp.Init() homekit.Init() diff --git a/pkg/core/helpers.go b/pkg/core/helpers.go index 0cb78aba..c895fbbf 100644 --- a/pkg/core/helpers.go +++ b/pkg/core/helpers.go @@ -29,6 +29,15 @@ func RandString(size, base byte) string { return string(b) } +func Any(errs ...error) error { + for _, err := range errs { + if err != nil { + return err + } + } + return nil +} + func Between(s, sub1, sub2 string) string { i := strings.Index(s, sub1) if i < 0 { diff --git a/pkg/h265/avc.go b/pkg/h265/avc.go new file mode 100644 index 00000000..f6d68559 --- /dev/null +++ b/pkg/h265/avc.go @@ -0,0 +1,54 @@ +package h265 + +import "github.com/AlexxIT/go2rtc/pkg/h264" + +const forbiddenZeroBit = 0x80 +const nalUnitType = 0x3F + +// DecodeStream - find and return first AU in AVC format +// useful for processing live streams with unknown separator size +func DecodeStream(annexb []byte) ([]byte, int) { + startPos := -1 + + i := 0 + for { + // search next separator + if i = h264.IndexFrom(annexb, []byte{0, 0, 1}, i); i < 0 { + break + } + + // move i to next AU + if i += 3; i >= len(annexb) { + break + } + + // check if AU type valid + octet := annexb[i] + if octet&forbiddenZeroBit != 0 { + continue + } + + nalType := (octet >> 1) & nalUnitType + if startPos >= 0 { + switch nalType { + case NALUTypeVPS, NALUTypePFrame: + if annexb[i-4] == 0 { + return h264.DecodeAnnexB(annexb[startPos : i-4]), i - 4 + } else { + return h264.DecodeAnnexB(annexb[startPos : i-3]), i - 3 + } + } + } else { + switch nalType { + case NALUTypeVPS, NALUTypePFrame: + if i >= 4 && annexb[i-4] == 0 { + startPos = i - 4 + } else { + startPos = i - 3 + } + } + } + } + + return nil, 0 +} diff --git a/pkg/pipe/client.go b/pkg/magic/client.go similarity index 64% rename from pkg/pipe/client.go rename to pkg/magic/client.go index 91265010..cc9ac3bb 100644 --- a/pkg/pipe/client.go +++ b/pkg/magic/client.go @@ -1,4 +1,4 @@ -package pipe +package magic import ( "bytes" @@ -6,17 +6,21 @@ import ( "errors" "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/h264" + "github.com/AlexxIT/go2rtc/pkg/h265" "github.com/AlexxIT/go2rtc/pkg/mpegts" "github.com/pion/rtp" "io" - "os/exec" ) +// Client - can read unknown bytestream and autodetect format type Client struct { - cmd *exec.Cmd - stdout io.ReadCloser - sniff []byte - handle func() error + Desc string + URL string + + Handle func() error + + r io.ReadCloser + sniff []byte medias []*core.Media receiver *core.Receiver @@ -24,47 +28,50 @@ type Client struct { recv int } -func NewClient(cmd *exec.Cmd) (prod *Client, err error) { - prod = &Client{cmd: cmd} +func NewClient(r io.ReadCloser) *Client { + return &Client{r: r} +} - prod.stdout, err = cmd.StdoutPipe() +func (c *Client) Probe() (err error) { + c.sniff = make([]byte, mpegts.PacketSize*3) // MPEG-TS: SDT+PAT+PMT + c.recv, err = io.ReadFull(c.r, c.sniff) if err != nil { - return nil, err - } - - if err = cmd.Start(); err != nil { - return nil, err - } - - prod.sniff = make([]byte, mpegts.PacketSize*3) // MPEG-TS: SDT+PAT+PMT - prod.recv, err = io.ReadFull(prod.stdout, prod.sniff) - if err != nil { - _ = prod.Stop() - return nil, err + _ = c.Close() + return } var codec *core.Codec - if bytes.HasPrefix(prod.sniff, []byte{0, 0, 0, 1}) { + if bytes.HasPrefix(c.sniff, []byte{0, 0, 0, 1}) { switch { - case h264.NALUType(prod.sniff) == h264.NALUTypeSPS: + case h264.NALUType(c.sniff) == h264.NALUTypeSPS: codec = &core.Codec{ Name: core.CodecH264, ClockRate: 90000, PayloadType: core.PayloadTypeRAW, } - prod.handle = prod.ReadBitstreams + c.Handle = c.ReadBitstreams + + case h265.NALUType(c.sniff) == h265.NALUTypeVPS: + codec = &core.Codec{ + Name: core.CodecH265, + ClockRate: 90000, + PayloadType: core.PayloadTypeRAW, + } + c.Handle = c.ReadBitstreams } - } else if bytes.HasPrefix(prod.sniff, []byte{0xFF, 0xD8}) { + + } else if bytes.HasPrefix(c.sniff, []byte{0xFF, 0xD8}) { codec = &core.Codec{ Name: core.CodecJPEG, ClockRate: 90000, PayloadType: core.PayloadTypeRAW, } - prod.handle = prod.ReadMJPEG - } else if prod.sniff[0] == mpegts.SyncByte { + c.Handle = c.ReadMJPEG + + } else if c.sniff[0] == mpegts.SyncByte { ts := mpegts.NewReader() - ts.AppendBuffer(prod.sniff) + ts.AppendBuffer(c.sniff) _ = ts.GetPacket() for _, streamType := range ts.GetStreamTypes() { switch streamType { @@ -74,17 +81,17 @@ func NewClient(cmd *exec.Cmd) (prod *Client, err error) { ClockRate: 90000, PayloadType: core.PayloadTypeRAW, } - prod.handle = prod.ReadMPEGTS + c.Handle = c.ReadMPEGTS } } } if codec == nil { - _ = prod.Stop() - return nil, errors.New("unknown format: " + hex.EncodeToString(prod.sniff)) + _ = c.Close() + return errors.New("unknown format: " + hex.EncodeToString(c.sniff[:8])) } - prod.medias = append(prod.medias, &core.Media{ + c.medias = append(c.medias, &core.Media{ Kind: core.KindVideo, Direction: core.DirectionRecvonly, Codecs: []*core.Codec{codec}, @@ -97,10 +104,18 @@ func (c *Client) ReadBitstreams() error { buf := c.sniff // total bufer b := make([]byte, 1024*1024) // reading buffer + var decodeStream func([]byte) ([]byte, int) + switch c.receiver.Codec.Name { + case core.CodecH264: + decodeStream = h264.DecodeStream + case core.CodecH265: + decodeStream = h265.DecodeStream + } + for { - payload, n := h264.DecodeStream(buf) + payload, n := decodeStream(buf) if payload == nil { - n, err := c.stdout.Read(b) + n, err := c.r.Read(b) if err != nil { return err } @@ -130,7 +145,7 @@ func (c *Client) ReadMJPEG() error { // one JPEG end and next start i := bytes.Index(buf, []byte{0xFF, 0xD9, 0xFF, 0xD8}) if i < 0 { - n, err := c.stdout.Read(b) + n, err := c.r.Read(b) if err != nil { return err } @@ -167,7 +182,7 @@ func (c *Client) ReadMPEGTS() error { for { packet := ts.GetPacket() if packet == nil { - n, err := c.stdout.Read(b) + n, err := c.r.Read(b) if err != nil { return err } @@ -186,3 +201,7 @@ func (c *Client) ReadMPEGTS() error { c.receiver.WriteRTP(packet) } } + +func (c *Client) Close() error { + return c.r.Close() +} diff --git a/pkg/pipe/keyframe.go b/pkg/magic/keyframe.go similarity index 99% rename from pkg/pipe/keyframe.go rename to pkg/magic/keyframe.go index 0126024e..abe83e59 100644 --- a/pkg/pipe/keyframe.go +++ b/pkg/magic/keyframe.go @@ -1,4 +1,4 @@ -package pipe +package magic import ( "github.com/AlexxIT/go2rtc/pkg/core" diff --git a/pkg/pipe/producer.go b/pkg/magic/producer.go similarity index 67% rename from pkg/pipe/producer.go rename to pkg/magic/producer.go index c2d5afdd..716a1eec 100644 --- a/pkg/pipe/producer.go +++ b/pkg/magic/producer.go @@ -1,9 +1,8 @@ -package pipe +package magic import ( "encoding/json" "github.com/AlexxIT/go2rtc/pkg/core" - "strings" ) func (c *Client) GetMedias() []*core.Media { @@ -18,29 +17,20 @@ func (c *Client) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, } func (c *Client) Start() error { - return c.handle() + return c.Handle() } func (c *Client) Stop() (err error) { if c.receiver != nil { c.receiver.Close() } - if err1 := c.stdout.Close(); err != nil { - err = err1 - } - if err1 := c.cmd.Process.Kill(); err != nil { - err = err1 - } - if err1 := c.cmd.Wait(); err != nil { - err = err1 - } - return + return c.Close() } func (c *Client) MarshalJSON() ([]byte, error) { info := &core.Info{ - Type: "PIPE active producer", - URL: c.cmd.Path + " " + strings.Join(c.cmd.Args, " "), + Type: c.Desc, + URL: c.URL, Medias: c.medias, Recv: c.recv, } From f617c148cd1bf3d678d819b35c865e6e15a3bdf8 Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Thu, 4 May 2023 11:57:32 +0300 Subject: [PATCH 65/80] Fix FFmpeg template for H265 --- internal/ffmpeg/ffmpeg.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/ffmpeg/ffmpeg.go b/internal/ffmpeg/ffmpeg.go index d24fcdb0..5311aba0 100644 --- a/internal/ffmpeg/ffmpeg.go +++ b/internal/ffmpeg/ffmpeg.go @@ -57,7 +57,7 @@ var defaults = map[string]string{ // `-tune zerolatency` - for minimal latency // `-profile high -level 4.1` - most used streaming profile "h264": "-c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuvj420p", - "h265": "-c:v libx265 -g 50 -profile:v high -level:v 5.1 -preset:v superfast -tune:v zerolatency", + "h265": "-c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency", "mjpeg": "-c:v mjpeg", //"mjpeg": "-c:v mjpeg -force_duplicated_matrix:v 1 -huffman:v 0 -pix_fmt:v yuvj420p", From 23dd5b450c1f98a5e2008c84ec7adffef015cd38 Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Thu, 4 May 2023 12:26:56 +0300 Subject: [PATCH 66/80] Add support H265 codec for MPEG-TS --- pkg/magic/client.go | 15 +++++++++++---- pkg/mpegts/helpers.go | 17 +++++++++++++---- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/pkg/magic/client.go b/pkg/magic/client.go index cc9ac3bb..640f794c 100644 --- a/pkg/magic/client.go +++ b/pkg/magic/client.go @@ -82,6 +82,14 @@ func (c *Client) Probe() (err error) { PayloadType: core.PayloadTypeRAW, } c.Handle = c.ReadMPEGTS + + case mpegts.StreamTypeH265: + codec = &core.Codec{ + Name: core.CodecH265, + ClockRate: 90000, + PayloadType: core.PayloadTypeRAW, + } + c.Handle = c.ReadMPEGTS } } } @@ -194,11 +202,10 @@ func (c *Client) ReadMPEGTS() error { //log.Printf("[AVC] %v, len: %d, ts: %10d", h264.Types(packet.Payload), len(packet.Payload), packet.Timestamp) - if packet.PayloadType != mpegts.StreamTypeH264 { - continue + switch packet.PayloadType { + case mpegts.StreamTypeH264, mpegts.StreamTypeH265: + c.receiver.WriteRTP(packet) } - - c.receiver.WriteRTP(packet) } } diff --git a/pkg/mpegts/helpers.go b/pkg/mpegts/helpers.go index 673deeeb..5407f70b 100644 --- a/pkg/mpegts/helpers.go +++ b/pkg/mpegts/helpers.go @@ -3,6 +3,7 @@ package mpegts import ( "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/h264" + "github.com/AlexxIT/go2rtc/pkg/h265" "github.com/pion/rtp" "time" ) @@ -16,6 +17,7 @@ const ( StreamTypePrivate = 0x06 // PCMU or PCMA or FLAC from FFmpeg StreamTypeAAC = 0x0F StreamTypeH264 = 0x1B + StreamTypeH265 = 0x24 StreamTypePCMATapo = 0x90 ) @@ -36,6 +38,8 @@ type PES struct { Sequence uint16 Timestamp uint32 + + decodeStream func([]byte) ([]byte, int) } const ( @@ -52,9 +56,14 @@ func (p *PES) SetBuffer(size uint16, b []byte) { optSize := b[2] // optional fields b = b[minHeaderSize+optSize:] - if p.StreamType == StreamTypeH264 { + switch p.StreamType { + case StreamTypeH264: p.Mode = ModeStream - } else { + p.decodeStream = h264.DecodeStream + case StreamTypeH265: + p.Mode = ModeStream + p.decodeStream = h265.DecodeStream + default: println("WARNING: mpegts: unknown zero-size stream") } } else { @@ -91,7 +100,7 @@ func (p *PES) GetPacket() (pkt *rtp.Packet) { payload := p.Payload[minHeaderSize+optSize:] switch p.StreamType { - case StreamTypeH264: + case StreamTypeH264, StreamTypeH265: var ts uint32 const hasPTS = 0b1000_0000 @@ -125,7 +134,7 @@ func (p *PES) GetPacket() (pkt *rtp.Packet) { p.Payload = nil case ModeStream: - payload, i := h264.DecodeStream(p.Payload) + payload, i := p.decodeStream(p.Payload) if payload == nil { return } From 2a91c4625a1e33e544d8ce2a49a0c4e205a1488c Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Thu, 4 May 2023 13:40:14 +0300 Subject: [PATCH 67/80] Add ALSA support inside docker --- Dockerfile | 3 ++- hardware.Dockerfile | 6 +++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 6ead9a1b..09e6adad 100644 --- a/Dockerfile +++ b/Dockerfile @@ -40,7 +40,8 @@ FROM base # Install ffmpeg, tini (for signal handling), # and other common tools for the echo source. -RUN apk add --no-cache tini ffmpeg bash curl jq +# alsa-plugins-pulse for ALSA support (+0MB) +RUN apk add --no-cache tini ffmpeg bash curl jq alsa-plugins-pulse # Hardware Acceleration for Intel CPU (+50MB) ARG TARGETARCH diff --git a/hardware.Dockerfile b/hardware.Dockerfile index d23c1691..c6424d29 100644 --- a/hardware.Dockerfile +++ b/hardware.Dockerfile @@ -38,9 +38,13 @@ RUN rm -f /etc/apt/apt.conf.d/docker-clean \ # Install ffmpeg, bash (for run.sh), tini (for signal handling), # and other common tools for the echo source. # non-free for Intel QSV support (not used by go2rtc, just for tests) +# libasound2-plugins for ALSA support RUN --mount=type=cache,target=/var/cache/apt,sharing=locked --mount=type=cache,target=/var/lib/apt,sharing=locked \ echo 'deb http://deb.debian.org/debian bookworm non-free' > /etc/apt/sources.list.d/debian-non-free.list && \ - apt-get -y update && apt-get -y install tini ffmpeg python3 curl jq intel-media-va-driver-non-free + apt-get -y update && apt-get -y install tini ffmpeg \ + python3 curl jq \ + intel-media-va-driver-non-free \ + libasound2-plugins COPY --link --from=rootfs / / From 035b82464552fadacf6d5c687e821b57bc74054a Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Thu, 4 May 2023 15:03:03 +0300 Subject: [PATCH 68/80] Update readme --- README.md | 49 ++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 42 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 39512acd..b1bf8d2f 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg * [Source: RTSP](#source-rtsp) * [Source: RTMP](#source-rtmp) * [Source: HTTP](#source-http) + * [Source: ONVIF](#source-onvif) * [Source: FFmpeg](#source-ffmpeg) * [Source: FFmpeg Device](#source-ffmpeg-device) * [Source: Exec](#source-exec) @@ -156,10 +157,11 @@ Available source types: - [rtsp](#source-rtsp) - `RTSP` and `RTSPS` cameras with [two way audio](#two-way-audio) support - [rtmp](#source-rtmp) - `RTMP` streams -- [http](#source-http) - `HTTP-FLV`, `MPEG-TS`, `JPEG` (snapshots), `MJPEG` streams +- [http](#source-http) - `HTTP-FLV`, `MPEG-TS`, `JPEG` (snapshots), `MJPEG` streams +- [onvif](#source-onvif) - get camera `RTSP` link and snapshot link using `ONVIF` protocol - [ffmpeg](#source-ffmpeg) - FFmpeg integration (`HLS`, `files` and many others) - [ffmpeg:device](#source-ffmpeg-device) - local USB Camera or Webcam -- [exec](#source-exec) - advanced FFmpeg and GStreamer integration +- [exec](#source-exec) - get media from external app output - [echo](#source-echo) - get stream link from bash or python - [homekit](#source-homekit) - streaming from HomeKit Camera - [dvrip](#source-dvrip) - streaming from DVR-IP NVR @@ -229,6 +231,8 @@ Support Content-Type: - **HTTP-MJPEG** (`multipart/x`) - simple MJPEG stream over HTTP - **MPEG-TS** (`video/mpeg`) - legacy [streaming format](https://en.wikipedia.org/wiki/MPEG_transport_stream) +Source also support HTTP and TCP streams with autodetection for different formats: **MJPEG**, **H.264/H.265 bitstream**, **MPEG-TS**. + ```yaml streams: # [HTTP-FLV] stream in video/x-flv format @@ -239,10 +243,26 @@ streams: # [MJPEG] stream will be proxied without modification http_mjpeg: https://mjpeg.sanford.io/count.mjpeg + + # [MJPEG or H.264/H.265 bitstream or MPEG-TS] + tcp_magic: tcp://192.168.1.123:12345 ``` **PS.** Dahua camera has bug: if you select MJPEG codec for RTSP second stream - snapshot won't work. +#### Source: ONVIF + +The source is not very useful if you already know RTSP and snapshot links for your camera. But it can be useful if you don't. + +**WebUI > Add** webpage support ONVIF autodiscovery. Your server must be on the same subnet as the camera. If you use docker, you must use "network host". + +```yaml +streams: + dahua1: onvif://admin:password@192.168.1.123 + reolink1: onvif://admin:password@192.168.1.123:8000 + tapo1: onvif://admin:password@192.168.1.123:2020 +``` + #### Source: FFmpeg You can get any stream or file or device via FFmpeg and push it to go2rtc. The app will automatically start FFmpeg with the proper arguments when someone starts watching the stream. @@ -301,25 +321,40 @@ Read more about encoding [hardware acceleration](https://github.com/AlexxIT/go2r You can get video from any USB-camera or Webcam as RTSP or WebRTC stream. This is part of FFmpeg integration. - check available devices in Web interface -- `resolution` and `framerate` must be supported by your camera! +- `video_size` and `framerate` must be supported by your camera! - for Linux supported only video for now - for macOS you can stream Facetime camera or whole Desktop! - for macOS important to set right framerate +Format: `ffmpeg:device?{input-params}#{param1}#{param2}#{param3}` + ```yaml streams: - linux_usbcam: ffmpeg:device?video=0&resolution=1280x720#video=h264 + linux_usbcam: ffmpeg:device?video=0&video_size=1280x720#video=h264 windows_webcam: ffmpeg:device?video=0#video=h264 - macos_facetime: ffmpeg:device?video=0&audio=1&resolution=1280x720&framerate=30#video=h264#audio=pcma + macos_facetime: ffmpeg:device?video=0&audio=1&video_size=1280x720&framerate=30#video=h264#audio=pcma ``` #### Source: Exec -FFmpeg source just a shortcut to exec source. You can get any stream or file or device via FFmpeg or GStreamer and push it to go2rtc via RTSP protocol: +Exec source can run any external application and expect data from it. Two transports are supported - **pipe** and **RTSP**. + +If you want to use **RTSP** transport - the command must contain the `{output}` argument in any place. On launch, it will be replaced by the local address of the RTSP server. + +**pipe** reads data from app stdout in different formats: **MJPEG**, **H.264/H.265 bitstream**, **MPEG-TS**. + +The source can be used with: + +- [FFmpeg](https://ffmpeg.org/) - go2rtc ffmpeg source just a shortcut to exec source +- [GStreamer](https://gstreamer.freedesktop.org/) +- [Raspberry Pi Cameras](https://www.raspberrypi.com/documentation/computers/camera_software.html) +- any your own software ```yaml streams: - stream1: exec:ffmpeg -hide_banner -re -stream_loop -1 -i /media/BigBuckBunny.mp4 -c copy -rtsp_transport tcp -f rtsp {output} + stream: exec:ffmpeg -re -i /media/BigBuckBunny.mp4 -c copy -rtsp_transport tcp -f rtsp {output} + picam_h264: exec:libcamera-vid -t 0 --inline -o - + picam_mjpeg: exec:libcamera-vid -t 0 --codec mjpeg -o - ``` #### Source: Echo From da92256910e8cbd1afbe77579d2ee1505d4e0ee9 Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Thu, 4 May 2023 15:04:06 +0300 Subject: [PATCH 69/80] Update version to 1.5.0 --- 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 c7cf5c2a..8aeeee0f 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -16,7 +16,7 @@ import ( "gopkg.in/yaml.v3" ) -var Version = "1.4.0" +var Version = "1.5.0" var UserAgent = "go2rtc/" + Version var ConfigPath string From 04f263aa157efc2baf3caa4ef683b4caff1f9512 Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Thu, 4 May 2023 17:40:54 +0300 Subject: [PATCH 70/80] Add binary for old Raspberry 1 and Zero --- README.md | 2 ++ scripts/build.cmd | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/README.md b/README.md index b1bf8d2f..e5c47223 100644 --- a/README.md +++ b/README.md @@ -98,10 +98,12 @@ Download binary for your OS from [latest release](https://github.com/AlexxIT/go2 - `go2rtc_win64.zip` - Windows 64-bit - `go2rtc_win32.zip` - Windows 32-bit +- `go2rtc_win_arm64.zip` - Windows ARM 64-bit - `go2rtc_linux_amd64` - Linux 64-bit - `go2rtc_linux_i386` - Linux 32-bit - `go2rtc_linux_arm64` - Linux ARM 64-bit (ex. Raspberry 64-bit OS) - `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)) - `go2rtc_mac_amd64.zip` - Mac Intel 64-bit - `go2rtc_mac_arm64.zip` - Mac ARM 64-bit diff --git a/scripts/build.cmd b/scripts/build.cmd index ef14aa30..54565b2d 100644 --- a/scripts/build.cmd +++ b/scripts/build.cmd @@ -36,6 +36,12 @@ go build -ldflags "-s -w" -trimpath -o %FILENAME% && upx %FILENAME% @SET FILENAME=go2rtc_linux_arm go build -ldflags "-s -w" -trimpath -o %FILENAME% && upx %FILENAME% +@SET GOOS=linux +@SET GOARCH=arm +@SET GOARM=6 +@SET FILENAME=go2rtc_linux_armv6 +go build -ldflags "-s -w" -trimpath -o %FILENAME% && upx %FILENAME% + @SET GOOS=linux @SET GOARCH=mipsle @SET FILENAME=go2rtc_linux_mipsel From e89c5cb429a07c91caf109e4eb1880ad293bff57 Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Thu, 4 May 2023 17:45:17 +0300 Subject: [PATCH 71/80] Add bages to readme --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index e5c47223..32e0ecfd 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ # go2rtc +[![](https://img.shields.io/github/stars/AlexxIT/go2rtc?style=flat-square&logo=github)](https://github.com/AlexxIT/go2rtc/stargazers) +[![](https://img.shields.io/docker/pulls/alexxit/go2rtc?style=flat-square&logo=docker&logoColor=white&label=pulls)](https://hub.docker.com/r/alexxit/go2rtc) +[![](https://img.shields.io/github/downloads/AlexxIT/go2rtc/total?color=blue&style=flat-square&logo=github)](https://github.com/AlexxIT/go2rtc/releases) + Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg, RTMP, etc. ![](assets/go2rtc.png) From bcb9756acad4f34780e0ad961f9035915698673c Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Fri, 5 May 2023 09:08:13 +0300 Subject: [PATCH 72/80] Update readme about ONVIF --- README.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 32e0ecfd..96806992 100644 --- a/README.md +++ b/README.md @@ -453,8 +453,10 @@ streams: Support import camera links from [Home Assistant](https://www.home-assistant.io/) config files: -- support [Generic Camera](https://www.home-assistant.io/integrations/generic/), setup via GUI -- support [HomeKit Camera](https://www.home-assistant.io/integrations/homekit_controller/) +- [Generic Camera](https://www.home-assistant.io/integrations/generic/), setup via GUI +- [HomeKit Camera](https://www.home-assistant.io/integrations/homekit_controller/) +- [ONVIF](https://www.home-assistant.io/integrations/onvif/) +- [Roborock](https://github.com/humbertogontijo/homeassistant-roborock) vacuums with camera ```yaml hass: @@ -465,7 +467,7 @@ streams: aqara_g3: hass:Camera-Hub-G3-AB12 ``` -More cameras, like [Tuya](https://www.home-assistant.io/integrations/tuya/), [ONVIF](https://www.home-assistant.io/integrations/onvif/), and possibly others can also be imported by using [this method](https://github.com/felipecrs/hass-expose-camera-stream-source#importing-home-assistant-cameras-to-go2rtc-andor-frigate). +More cameras, like [Tuya](https://www.home-assistant.io/integrations/tuya/), and possibly others can also be imported by using [this method](https://github.com/felipecrs/hass-expose-camera-stream-source#importing-home-assistant-cameras-to-go2rtc-andor-frigate). #### Source: ISAPI @@ -811,6 +813,7 @@ You have several options on how to add a camera to Home Assistant: 2. Camera [any source](#module-streams) => [go2rtc config](#configuration) => [Generic Camera](https://www.home-assistant.io/integrations/generic/) - Install any [go2rtc](#fast-start) - Add your stream to [go2rtc config](#configuration) + - Hass > Settings > Integrations > Add Integration > [ONVIF](https://my.home-assistant.io/redirect/config_flow_start/?domain=onvif) > Host: `127.0.0.1`, Port: `1984` - Hass > Settings > Integrations > Add Integration > [Generic Camera](https://my.home-assistant.io/redirect/config_flow_start/?domain=generic) > `rtsp://127.0.0.1:8554/camera1` (change to your stream name) You have several options on how to watch the stream from the cameras in Home Assistant: From 083ec127fd8bc356815a5224618c80116d18c858 Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Fri, 5 May 2023 09:45:55 +0300 Subject: [PATCH 73/80] Fix video timestamp accuracy --- pkg/core/helpers.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pkg/core/helpers.go b/pkg/core/helpers.go index c895fbbf..b3871a6d 100644 --- a/pkg/core/helpers.go +++ b/pkg/core/helpers.go @@ -10,9 +10,8 @@ import ( ) // Now90000 - timestamp for Video (clock rate = 90000 samples per second) -// same as: uint32(time.Duration(time.Now().UnixNano()) * 90000 / time.Second) func Now90000() uint32 { - return uint32(time.Duration(time.Now().UnixMilli()) * 90) + return uint32(time.Duration(time.Now().UnixNano()) * 90000 / time.Second) } const symbols = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_" From 4fe078c7c0f0f704b4b41b59d894a20c47f3bd61 Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Fri, 5 May 2023 10:01:59 +0300 Subject: [PATCH 74/80] ONVIF code refactoring --- pkg/onvif/client.go | 2 ++ pkg/onvif/helpers.go | 6 +----- pkg/onvif/server.go | 18 +++++++++--------- 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/pkg/onvif/client.go b/pkg/onvif/client.go index d090baf3..090e9ef2 100644 --- a/pkg/onvif/client.go +++ b/pkg/onvif/client.go @@ -15,6 +15,8 @@ import ( "time" ) +const PathDevice = "/onvif/device_service" + type Client struct { url *url.URL diff --git a/pkg/onvif/helpers.go b/pkg/onvif/helpers.go index c5451c42..7925e1ec 100644 --- a/pkg/onvif/helpers.go +++ b/pkg/onvif/helpers.go @@ -9,10 +9,6 @@ import ( "time" ) -const ( - PathDevice = "/onvif/device_service" -) - func FindTagValue(b []byte, tag string) string { re := regexp.MustCompile(`<[^/>]*` + tag + `[^>]*>([^<]+)`) m := re.FindSubmatch(b) @@ -45,7 +41,7 @@ func DiscoveryStreamingURLs() ([]string, error) { - + diff --git a/pkg/onvif/server.go b/pkg/onvif/server.go index 3003efd7..f8f2883c 100644 --- a/pkg/onvif/server.go +++ b/pkg/onvif/server.go @@ -45,12 +45,12 @@ func GetRequestAction(b []byte) string { func GetCapabilitiesResponse(host string) string { return ` - - - - - http://` + host + `/onvif/device_service - + + + + + http://` + host + `/onvif/device_service + http://` + host + `/onvif/media_service @@ -59,9 +59,9 @@ func GetCapabilitiesResponse(host string) string { true - - - + + + ` } From 3139189975bce359009dffb031df6645bad5d0da Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Sat, 6 May 2023 14:29:35 +0300 Subject: [PATCH 75/80] Move ParseQuery from ffmpeg to streams module --- internal/ffmpeg/ffmpeg.go | 15 +-------------- internal/streams/helpers.go | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 14 deletions(-) create mode 100644 internal/streams/helpers.go diff --git a/internal/ffmpeg/ffmpeg.go b/internal/ffmpeg/ffmpeg.go index 5311aba0..a85be5b9 100644 --- a/internal/ffmpeg/ffmpeg.go +++ b/internal/ffmpeg/ffmpeg.go @@ -127,7 +127,7 @@ func parseArgs(s string) *ffmpeg.Args { var query url.Values if i := strings.IndexByte(s, '#'); i > 0 { - query = parseQuery(s[i+1:]) + query = streams.ParseQuery(s[i+1:]) args.Video = len(query["video"]) args.Audio = len(query["audio"]) s = s[:i] @@ -278,16 +278,3 @@ func parseArgs(s string) *ffmpeg.Args { return args } - -func parseQuery(s string) map[string][]string { - query := map[string][]string{} - for _, key := range strings.Split(s, "#") { - var value string - i := strings.IndexByte(key, '=') - if i > 0 { - key, value = key[:i], key[i+1:] - } - query[key] = append(query[key], value) - } - return query -} diff --git a/internal/streams/helpers.go b/internal/streams/helpers.go new file mode 100644 index 00000000..e59dab77 --- /dev/null +++ b/internal/streams/helpers.go @@ -0,0 +1,19 @@ +package streams + +import ( + "net/url" + "strings" +) + +func ParseQuery(s string) url.Values { + params := url.Values{} + for _, key := range strings.Split(s, "#") { + var value string + i := strings.IndexByte(key, '=') + if i > 0 { + key, value = key[:i], key[i+1:] + } + params[key] = append(params[key], value) + } + return params +} From 8b126c0d377623e2f9785c927a38245a690f992e Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Sat, 6 May 2023 14:31:46 +0300 Subject: [PATCH 76/80] Add support RTSP over WebSocket --- internal/rtsp/rtsp.go | 25 ++++--- pkg/rtsp/client.go | 30 ++------- pkg/rtsp/conn.go | 1 + pkg/rtsp/dial.go | 44 ++++++++++++ pkg/tcp/websocket/client.go | 130 ++++++++++++++++++++++++++++++++++++ pkg/tcp/websocket/dial.go | 64 ++++++++++++++++++ 6 files changed, 258 insertions(+), 36 deletions(-) create mode 100644 pkg/rtsp/dial.go create mode 100644 pkg/tcp/websocket/client.go create mode 100644 pkg/tcp/websocket/dial.go diff --git a/internal/rtsp/rtsp.go b/internal/rtsp/rtsp.go index f50337f4..9d1234d5 100644 --- a/internal/rtsp/rtsp.go +++ b/internal/rtsp/rtsp.go @@ -91,19 +91,19 @@ var log zerolog.Logger var handlers []Handler var defaultMedias []*core.Media -func rtspHandler(url string) (core.Producer, error) { - backchannel := true +func rtspHandler(rawURL string) (core.Producer, error) { + rawURL, rawQuery, _ := strings.Cut(rawURL, "#") - if i := strings.IndexByte(url, '#'); i > 0 { - if url[i+1:] == "backchannel=0" { - backchannel = false - } - url = url[:i] - } - - conn := rtsp.NewClient(url) + conn := rtsp.NewClient(rawURL) + conn.Backchannel = true conn.UserAgent = app.UserAgent + if rawQuery != "" { + query := streams.ParseQuery(rawQuery) + conn.Backchannel = query.Get("backchannel") == "1" + conn.Transport = query.Get("transport") + } + if log.Trace().Enabled() { conn.Listen(func(msg any) { switch msg := msg.(type) { @@ -121,12 +121,11 @@ func rtspHandler(url string) (core.Producer, error) { return nil, err } - conn.Backchannel = backchannel if err := conn.Describe(); err != nil { - if !backchannel { + if !conn.Backchannel { return nil, err } - log.Trace().Msgf("[rtsp] describe (backchannel=%t) err: %v", backchannel, err) + log.Trace().Msgf("[rtsp] describe (backchannel=%t) err: %v", conn.Backchannel, err) // second try without backchannel, we need to reconnect conn.Backchannel = false diff --git a/pkg/rtsp/client.go b/pkg/rtsp/client.go index b5f3db6b..a4a3b656 100644 --- a/pkg/rtsp/client.go +++ b/pkg/rtsp/client.go @@ -2,10 +2,9 @@ package rtsp import ( "bufio" - "crypto/tls" "errors" "fmt" - "net" + "github.com/AlexxIT/go2rtc/pkg/tcp/websocket" "net/http" "net/url" "strconv" @@ -23,33 +22,18 @@ func NewClient(uri string) *Conn { } func (c *Conn) Dial() (err error) { - if c.URL, err = url.Parse(c.uri); err != nil { - return + if c.Transport == "" { + c.conn, err = Dial(c.uri) + } else { + c.conn, err = websocket.Dial(c.Transport) } - if strings.IndexByte(c.URL.Host, ':') < 0 { - c.URL.Host += ":554" - } - - c.conn, err = net.DialTimeout("tcp", c.URL.Host, time.Second*5) if err != nil { return } - var tlsConf *tls.Config - switch c.URL.Scheme { - case "rtsps": - tlsConf = &tls.Config{ServerName: c.URL.Hostname()} - case "rtspx": - c.URL.Scheme = "rtsps" - tlsConf = &tls.Config{InsecureSkipVerify: true} - } - if tlsConf != nil { - tlsConn := tls.Client(c.conn, tlsConf) - if err = tlsConn.Handshake(); err != nil { - return err - } - c.conn = tlsConn + if c.URL, err = url.Parse(c.uri); err != nil { + return } // remove UserInfo from URL diff --git a/pkg/rtsp/conn.go b/pkg/rtsp/conn.go index 2bdb91bb..9b23087a 100644 --- a/pkg/rtsp/conn.go +++ b/pkg/rtsp/conn.go @@ -24,6 +24,7 @@ type Conn struct { Backchannel bool PacketSize uint16 SessionName string + Transport string // custom transport support, ex. RTSP over WebSocket Medias []*core.Media UserAgent string diff --git a/pkg/rtsp/dial.go b/pkg/rtsp/dial.go new file mode 100644 index 00000000..58d5dd65 --- /dev/null +++ b/pkg/rtsp/dial.go @@ -0,0 +1,44 @@ +package rtsp + +import ( + "crypto/tls" + "errors" + "net" + "net/url" + "strings" + "time" +) + +func Dial(uri string) (net.Conn, error) { + u, err := url.Parse(uri) + if err != nil { + return nil, err + } + + switch u.Scheme { + case "rtsp": + return dialTCP(u.Host, nil) + case "rtsps": + tlsConf := &tls.Config{ServerName: u.Hostname()} + return dialTCP(u.Host, tlsConf) + case "rtspx": + tlsConf := &tls.Config{InsecureSkipVerify: true} + return dialTCP(u.Host, tlsConf) + } + + return nil, errors.New("unsupported scheme: " + u.Scheme) +} + +func dialTCP(address string, tlsConf *tls.Config) (net.Conn, error) { + if strings.IndexByte(address, ':') < 0 { + address += ":554" + } + + conn, err := net.DialTimeout("tcp", address, time.Second*5) + if tlsConf == nil || err != nil { + return conn, err + } + + tlsConn := tls.Client(conn, tlsConf) + return tlsConn, tlsConn.Handshake() +} diff --git a/pkg/tcp/websocket/client.go b/pkg/tcp/websocket/client.go new file mode 100644 index 00000000..e95ce1e4 --- /dev/null +++ b/pkg/tcp/websocket/client.go @@ -0,0 +1,130 @@ +package websocket + +import ( + cryptorand "crypto/rand" + "encoding/binary" + "fmt" + "io" + "net" + "time" +) + +const BinaryMessage = 2 + +type Client struct { + conn net.Conn + remain int +} + +func NewClient(conn net.Conn) *Client { + return &Client{conn: conn} +} + +const finalBit = 0x80 +const maskBit = 0x80 + +func (w *Client) Read(b []byte) (n int, err error) { + if w.remain == 0 { + b2 := make([]byte, 2) + if _, err = io.ReadFull(w.conn, b2); err != nil { + return 0, err + } + + frameType := b2[0] & 0xF + w.remain = int(b2[1] & 0x7F) + + switch frameType { + case BinaryMessage: + default: + return 0, fmt.Errorf("unsupported frame type: %d", frameType) + } + + switch w.remain { + case 126: + if _, err = io.ReadFull(w.conn, b2); err != nil { + return 0, err + } + w.remain = int(binary.BigEndian.Uint16(b2)) + case 127: + b8 := make([]byte, 8) + if _, err = io.ReadFull(w.conn, b8); err != nil { + return 0, err + } + w.remain = int(binary.BigEndian.Uint64(b8)) + } + } + + if w.remain > len(b) { + n, err = io.ReadFull(w.conn, b) + w.remain -= n + return + } + + n, err = io.ReadFull(w.conn, b[:w.remain]) + w.remain = 0 + + return +} + +func (w *Client) Write(b []byte) (n int, err error) { + var data []byte + var start byte + + size := len(b) + + switch { + case size > 65535: + start = 10 + data = make([]byte, size+14) + data[1] = maskBit | 127 + binary.BigEndian.PutUint64(data[2:], uint64(size)) + case size > 125: + start = 4 + data = make([]byte, size+8) + data[1] = maskBit | 126 + binary.BigEndian.PutUint16(data[2:], uint16(size)) + default: + start = 2 + data = make([]byte, size+6) + data[1] = maskBit | byte(size) + } + + data[0] = BinaryMessage | finalBit + + mask := data[start : start+4] + msg := data[start+4:] + + if _, err = cryptorand.Read(mask); err != nil { + return 0, err + } + + for i := 0; i < len(b); i++ { + msg[i] = b[i] ^ mask[i%4] + } + + return w.conn.Write(data) +} + +func (w *Client) Close() error { + return w.conn.Close() +} + +func (w *Client) LocalAddr() net.Addr { + return w.conn.LocalAddr() +} + +func (w *Client) RemoteAddr() net.Addr { + return w.conn.RemoteAddr() +} + +func (w *Client) SetDeadline(t time.Time) error { + return w.conn.SetDeadline(t) +} + +func (w *Client) SetReadDeadline(t time.Time) error { + return w.conn.SetReadDeadline(t) +} + +func (w *Client) SetWriteDeadline(t time.Time) error { + return w.conn.SetWriteDeadline(t) +} diff --git a/pkg/tcp/websocket/dial.go b/pkg/tcp/websocket/dial.go new file mode 100644 index 00000000..737a5cbc --- /dev/null +++ b/pkg/tcp/websocket/dial.go @@ -0,0 +1,64 @@ +package websocket + +import ( + cryptorand "crypto/rand" + "crypto/sha1" + "encoding/base64" + "errors" + "github.com/AlexxIT/go2rtc/pkg/tcp" + "net" + "net/http" + "strings" +) + +func Dial(address string) (net.Conn, error) { + if strings.HasPrefix(address, "ws") { + address = "http" + address[2:] // support http and https + } + + // using custom client for support Digest Auth + // https://github.com/AlexxIT/go2rtc/issues/415 + ctx, pconn := tcp.WithConn() + + req, err := http.NewRequestWithContext(ctx, "GET", address, nil) + if err != nil { + return nil, err + } + + key, accept := GetKeyAccept() + + // Version, Key, Protocol important for Axis cameras + req.Header.Set("Connection", "Upgrade") + req.Header.Set("Upgrade", "websocket") + req.Header.Set("Sec-WebSocket-Version", "13") + req.Header.Set("Sec-WebSocket-Key", key) + req.Header.Set("Sec-WebSocket-Protocol", "binary") + + res, err := tcp.Do(req) + if err != nil { + return nil, err + } + + if res.StatusCode != http.StatusSwitchingProtocols { + return nil, errors.New("wrong status: " + res.Status) + } + + if res.Header.Get("Sec-Websocket-Accept") != accept { + return nil, errors.New("wrong websocket accept") + } + + return NewClient(*pconn), nil +} + +func GetKeyAccept() (key, accept string) { + b := make([]byte, 16) + _, _ = cryptorand.Read(b) + key = base64.StdEncoding.EncodeToString(b) + + h := sha1.New() + h.Write([]byte(key)) + h.Write([]byte("258EAFA5-E914-47DA-95CA-C5AB0DC85B11")) + accept = base64.StdEncoding.EncodeToString(h.Sum(nil)) + + return +} From c09438d3d084e8f3dbeff2af435149563c0b2d0a Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Tue, 16 May 2023 18:39:39 +0300 Subject: [PATCH 77/80] Set prefer_tcp flag for ffmpeg --- internal/ffmpeg/ffmpeg.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/ffmpeg/ffmpeg.go b/internal/ffmpeg/ffmpeg.go index a85be5b9..d185a5f6 100644 --- a/internal/ffmpeg/ffmpeg.go +++ b/internal/ffmpeg/ffmpeg.go @@ -45,7 +45,7 @@ var defaults = map[string]string{ // inputs "file": "-re -i {input}", "http": "-fflags nobuffer -flags low_delay -i {input}", - "rtsp": "-fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_transport tcp -i {input}", + "rtsp": "-fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i {input}", "rtsp/udp": "-fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -i {input}", From 1eaacdb2171933c88b3a6fd0d08ed5601b851391 Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Sat, 20 May 2023 06:26:05 +0300 Subject: [PATCH 78/80] Add Hass API source for WebRTC cameras --- internal/hass/api.go | 133 ++++++++++++++++++-------------------- internal/hass/hass.go | 84 ++++++++++++++++++------ pkg/hass/api.go | 143 +++++++++++++++++++++++++++++++++++++++++ pkg/hass/client.go | 115 +++++++++++++++++++++++++++++++++ pkg/webrtc/client.go | 3 + pkg/webrtc/conn.go | 11 ++++ pkg/webrtc/consumer.go | 7 +- 7 files changed, 405 insertions(+), 91 deletions(-) create mode 100644 pkg/hass/api.go create mode 100644 pkg/hass/client.go diff --git a/internal/hass/api.go b/internal/hass/api.go index 5c8294eb..8824d47c 100644 --- a/internal/hass/api.go +++ b/internal/hass/api.go @@ -3,7 +3,6 @@ package hass import ( "encoding/base64" "encoding/json" - "github.com/AlexxIT/go2rtc/internal/api" "github.com/AlexxIT/go2rtc/internal/streams" "github.com/AlexxIT/go2rtc/internal/webrtc" "net" @@ -11,79 +10,69 @@ import ( "strings" ) -func initAPI() { - ok := func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - _, _ = w.Write([]byte(`{"status":1,"payload":{}}`)) - } +func apiOK(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"status":1,"payload":{}}`)) +} - // support https://www.home-assistant.io/integrations/rtsp_to_webrtc/ - api.HandleFunc("/static", func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - }) - - api.HandleFunc("/streams", ok) - - // api from RTSPtoWeb - api.HandleFunc("/stream/", func(w http.ResponseWriter, r *http.Request) { - switch { - // /stream/{id}/add - case strings.HasSuffix(r.RequestURI, "/add"): - var v addJSON - if err := json.NewDecoder(r.Body).Decode(&v); err != nil { - return - } - - // we can get three types of links: - // 1. link to go2rtc stream: rtsp://...:8554/{stream_name} - // 2. static link to Hass camera - // 3. dynamic link to Hass camera - stream := streams.Get(v.Name) - if stream == nil { - stream = streams.NewTemplate(v.Name, v.Channels.First.Url) - } - - stream.SetSource(v.Channels.First.Url) - - ok(w, r) - - // /stream/{id}/channel/0/webrtc - default: - i := strings.IndexByte(r.RequestURI[8:], '/') - if i <= 0 { - log.Warn().Msgf("wrong request: %s", r.RequestURI) - return - } - name := r.RequestURI[8 : 8+i] - - stream := streams.Get(name) - if stream == nil { - w.WriteHeader(http.StatusNotFound) - return - } - - if err := r.ParseForm(); err != nil { - log.Error().Err(err).Msg("[api.hass] parse form") - return - } - - s := r.FormValue("data") - offer, err := base64.StdEncoding.DecodeString(s) - if err != nil { - log.Error().Err(err).Msg("[api.hass] sdp64 decode") - return - } - - s, err = webrtc.ExchangeSDP(stream, string(offer), "WebRTC/Hass sync", r.UserAgent()) - if err != nil { - log.Error().Err(err).Msg("[api.hass] exchange SDP") - return - } - - s = base64.StdEncoding.EncodeToString([]byte(s)) - _, _ = w.Write([]byte(s)) +func apiStream(w http.ResponseWriter, r *http.Request) { + switch { + // /stream/{id}/add + case strings.HasSuffix(r.RequestURI, "/add"): + var v addJSON + if err := json.NewDecoder(r.Body).Decode(&v); err != nil { + return } - }) + + // we can get three types of links: + // 1. link to go2rtc stream: rtsp://...:8554/{stream_name} + // 2. static link to Hass camera + // 3. dynamic link to Hass camera + stream := streams.Get(v.Name) + if stream == nil { + stream = streams.NewTemplate(v.Name, v.Channels.First.Url) + } + + stream.SetSource(v.Channels.First.Url) + + apiOK(w, r) + + // /stream/{id}/channel/0/webrtc + default: + i := strings.IndexByte(r.RequestURI[8:], '/') + if i <= 0 { + log.Warn().Msgf("wrong request: %s", r.RequestURI) + return + } + name := r.RequestURI[8 : 8+i] + + stream := streams.Get(name) + if stream == nil { + w.WriteHeader(http.StatusNotFound) + return + } + + if err := r.ParseForm(); err != nil { + log.Error().Err(err).Msg("[api.hass] parse form") + return + } + + s := r.FormValue("data") + offer, err := base64.StdEncoding.DecodeString(s) + if err != nil { + log.Error().Err(err).Msg("[api.hass] sdp64 decode") + return + } + + s, err = webrtc.ExchangeSDP(stream, string(offer), "WebRTC/Hass sync", r.UserAgent()) + if err != nil { + log.Error().Err(err).Msg("[api.hass] exchange SDP") + return + } + + s = base64.StdEncoding.EncodeToString([]byte(s)) + _, _ = w.Write([]byte(s)) + } } func HassioAddr() string { diff --git a/internal/hass/hass.go b/internal/hass/hass.go index e9a9f9b4..67dbbf41 100644 --- a/internal/hass/hass.go +++ b/internal/hass/hass.go @@ -9,10 +9,12 @@ import ( "github.com/AlexxIT/go2rtc/internal/roborock" "github.com/AlexxIT/go2rtc/internal/streams" "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/AlexxIT/go2rtc/pkg/hass" "github.com/rs/zerolog" "net/http" "os" "path" + "sync" ) func Init() { @@ -29,10 +31,15 @@ func Init() { log = app.GetLogger("hass") - initAPI() + // support API for https://www.home-assistant.io/integrations/rtsp_to_webrtc/ + api.HandleFunc("/static", apiOK) + api.HandleFunc("/streams", apiOK) + api.HandleFunc("/stream/", apiStream) + + // load static entries from Hass config + if err := importConfig(conf.Mod.Config); err != nil { + log.Debug().Msgf("[hass] can't import config: %s", err) - entries := importEntries(conf.Mod.Config) - if entries == nil { api.HandleFunc("api/hass", func(w http.ResponseWriter, _ *http.Request) { http.Error(w, "no hass config", http.StatusNotFound) }) @@ -40,18 +47,35 @@ func Init() { } api.HandleFunc("api/hass", func(w http.ResponseWriter, _ *http.Request) { + once.Do(func() { + // load WebRTC entities from Hass API, works only for add-on version + if token := hass.SupervisorToken(); token != "" { + if err := importWebRTC(token); err != nil { + log.Warn().Err(err).Caller().Send() + } + } + }) + var items []api.Stream - for name, url := range entries { + for name, url := range entities { items = append(items, api.Stream{Name: name, URL: url}) } api.ResponseStreams(w, items) }) streams.HandleFunc("hass", func(url string) (core.Producer, error) { - if hurl := entries[url[5:]]; hurl != "" { - return streams.GetProducer(hurl) + // check entity by name + if url2 := entities[url[5:]]; url2 != "" { + return streams.GetProducer(url2) } - return nil, fmt.Errorf("can't get url: %s", url) + + // support hass://supervisor?entity_id=camera.driveway_doorbell + client, err := hass.NewClient(url) + if err != nil { + return nil, err + } + + return client, nil }) // for Addon listen on hassio interface, so WebUI feature will work @@ -68,12 +92,12 @@ func Init() { } } -func importEntries(config string) map[string]string { +func importConfig(config string) error { // support load cameras from Hass config file filename := path.Join(config, ".storage/core.config_entries") b, err := os.ReadFile(filename) if err != nil { - return nil + return err } var storage struct { @@ -88,11 +112,9 @@ func importEntries(config string) map[string]string { } if err = json.Unmarshal(b, &storage); err != nil { - return nil + return err } - urls := map[string]string{} - for _, entrie := range storage.Data.Entries { switch entrie.Domain { case "generic": @@ -102,7 +124,7 @@ func importEntries(config string) map[string]string { if err = json.Unmarshal(entrie.Options, &options); err != nil { continue } - urls[entrie.Title] = options.StreamSource + entities[entrie.Title] = options.StreamSource case "homekit_controller": if !bytes.Contains(entrie.Data, []byte("iOSPairingId")) { @@ -121,7 +143,7 @@ func importEntries(config string) map[string]string { if err = json.Unmarshal(entrie.Data, &data); err != nil { continue } - urls[entrie.Title] = fmt.Sprintf( + entities[entrie.Title] = fmt.Sprintf( "homekit://%s:%d?client_id=%s&client_private=%s%s&device_id=%s&device_public=%s", data.DeviceHost, data.DevicePort, data.ClientID, data.ClientPrivate, data.ClientPublic, @@ -143,22 +165,48 @@ func importEntries(config string) map[string]string { } if data.Username != "" && data.Password != "" { - urls[entrie.Title] = fmt.Sprintf( + entities[entrie.Title] = fmt.Sprintf( "onvif://%s:%s@%s:%d", data.Username, data.Password, data.Host, data.Port, ) } else { - urls[entrie.Title] = fmt.Sprintf("onvif://%s:%d", data.Host, data.Port) + entities[entrie.Title] = fmt.Sprintf("onvif://%s:%d", data.Host, data.Port) } default: continue } - log.Info().Str("url", "hass:"+entrie.Title).Msg("[hass] load stream") + log.Debug().Str("url", "hass:"+entrie.Title).Msg("[hass] load config") //streams.Get("hass:" + entrie.Title) } - return urls + return nil } +func importWebRTC(token string) error { + hassAPI, err := hass.NewAPI("ws://supervisor/core/websocket", token) + if err != nil { + return err + } + + webrtcEntities, err := hassAPI.GetWebRTCEntities() + if err != nil { + return err + } + + if len(webrtcEntities) == 0 { + log.Debug().Msg("[hass] webrtc cameras not found") + } + + for name, entityID := range webrtcEntities { + entities[name] = "hass://supervisor?entity_id=" + entityID + + log.Debug().Msgf("[hass] load webrtc name=%s entity_id=%d", name, entityID) + } + + return nil +} + +var entities = map[string]string{} var log zerolog.Logger +var once sync.Once diff --git a/pkg/hass/api.go b/pkg/hass/api.go new file mode 100644 index 00000000..6d5a9204 --- /dev/null +++ b/pkg/hass/api.go @@ -0,0 +1,143 @@ +package hass + +import ( + "errors" + "github.com/gorilla/websocket" + "os" +) + +type API struct { + ws *websocket.Conn +} + +func NewAPI(url, token string) (*API, error) { + ws, _, err := websocket.DefaultDialer.Dial(url, nil) + if err != nil { + return nil, err + } + + api := &API{ws: ws} + if err = api.Auth(token); err != nil { + _ = ws.Close() + return nil, err + } + + return api, nil +} + +func (a *API) Auth(token string) error { + var res ResponseAuth + + if err := a.ws.ReadJSON(&res); err != nil { + return err + } + if res.Type != "auth_required" { + return errors.New("hass: wrong type: " + res.Type) + } + + s := `{"type":"auth","access_token":"` + token + `"}` + if err := a.ws.WriteMessage(websocket.TextMessage, []byte(s)); err != nil { + return err + } + if err := a.ws.ReadJSON(&res); err != nil { + return err + } + if res.Type != "auth_ok" { + return errors.New("hass: wrong type: " + res.Type) + } + + return nil +} + +func (a *API) Close() error { + return a.ws.Close() +} + +func (a *API) ExchangeSDP(entityID, offer string) (string, error) { + var msg = map[string]any{ + "id": 1, + "type": "camera/web_rtc_offer", + "entity_id": entityID, + "offer": offer, + } + if err := a.ws.WriteJSON(msg); err != nil { + return "", err + } + + var res ResponseOffer + if err := a.ws.ReadJSON(&res); err != nil { + return "", err + } + + if res.Type != "result" || !res.Success { + return "", errors.New("hass: wrong response") + } + + return res.Result.Answer, nil +} + +func (a *API) GetWebRTCEntities() (map[string]string, error) { + s := `{"id":1,"type":"get_states"}` + if err := a.ws.WriteMessage(websocket.TextMessage, []byte(s)); err != nil { + return nil, err + } + + var res ResponseStates + if err := a.ws.ReadJSON(&res); err != nil { + return nil, err + } + if res.Type != "result" || !res.Success { + return nil, errors.New("hass: wrong response") + } + + entities := map[string]string{} + + for _, entity := range res.Result { + if entity.Attributes.FrontendStreamType == "web_rtc" { + entities[entity.Attributes.FriendlyName] = entity.EntityId + } + } + + return entities, nil +} + +type ResponseAuth struct { + Type string `json:"type"` +} + +type ResponseStates struct { + //Id int `json:"id"` + Type string `json:"type"` + Success bool `json:"success"` + Result []struct { + EntityId string `json:"entity_id"` + //State string `json:"state"` + Attributes struct { + //ModelName string `json:"model_name"` + //Brand string `json:"brand"` + FrontendStreamType string `json:"frontend_stream_type"` + FriendlyName string `json:"friendly_name"` + //SupportedFeatures int `json:"supported_features"` + } `json:"attributes"` + //LastChanged time.Time `json:"last_changed"` + //LastUpdated time.Time `json:"last_updated"` + //Context struct { + // Id string `json:"id"` + // ParentId interface{} `json:"parent_id"` + // UserId interface{} `json:"user_id"` + //} `json:"context"` + } `json:"result"` +} + +type ResponseOffer struct { + //Id int `json:"id"` + Type string `json:"type"` + Success bool `json:"success"` + Result struct { + Answer string `json:"answer"` + } `json:"result"` +} + +func SupervisorToken() string { + return os.Getenv("SUPERVISOR_TOKEN") +} diff --git a/pkg/hass/client.go b/pkg/hass/client.go new file mode 100644 index 00000000..5b9a227a --- /dev/null +++ b/pkg/hass/client.go @@ -0,0 +1,115 @@ +package hass + +import ( + "errors" + "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/AlexxIT/go2rtc/pkg/webrtc" + pion "github.com/pion/webrtc/v3" + "net/url" +) + +type Client struct { + conn *webrtc.Conn +} + +func NewClient(rawURL string) (*Client, error) { + u, err := url.Parse(rawURL) + if err != nil { + return nil, err + } + + query := u.Query() + + entityID := query.Get("entity_id") + if entityID == "" { + return nil, errors.New("hass: no entity_id") + } + + var uri, token string + + if u.Host == "supervisor" { + uri = "ws://supervisor/core/websocket" + token = SupervisorToken() + } else { + uri = "ws://" + u.Host + "/api/websocket" + token = query.Get("token") + } + + if token == "" { + return nil, errors.New("hass: no token") + } + + // 1. Check connection to Hass + hassAPI, err := NewAPI(uri, token) + if err != nil { + return nil, err + } + + defer hassAPI.Close() + + // 2. Create WebRTC client + rtcAPI, err := webrtc.NewAPI("") + if err != nil { + return nil, err + } + + conf := pion.Configuration{} + pc, err := rtcAPI.NewPeerConnection(conf) + if err != nil { + return nil, err + } + + conn := webrtc.NewConn(pc) + conn.Desc = "Hass" + conn.Mode = core.ModeActiveProducer + + // https://developers.google.com/nest/device-access/traits/device/camera-live-stream#generatewebrtcstream-request-fields + medias := []*core.Media{ + {Kind: core.KindAudio, Direction: core.DirectionRecvonly}, + {Kind: core.KindVideo, Direction: core.DirectionRecvonly}, + {Kind: "app"}, // important for Nest + } + + // 3. Create offer with candidates + offer, err := conn.CreateCompleteOffer(medias) + if err != nil { + return nil, err + } + + // 4. Exchange SDP via Hass + answer, err := hassAPI.ExchangeSDP(entityID, offer) + if err != nil { + return nil, err + } + + // 5. Set answer with remote medias + if err = conn.SetAnswer(answer); err != nil { + return nil, err + } + + return &Client{conn: conn}, nil +} + +func (c *Client) GetMedias() []*core.Media { + return c.conn.GetMedias() +} + +func (c *Client) 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 { + return c.conn.Start() +} + +func (c *Client) Stop() error { + return c.conn.Stop() +} + +func (c *Client) MarshalJSON() ([]byte, error) { + return c.conn.MarshalJSON() +} diff --git a/pkg/webrtc/client.go b/pkg/webrtc/client.go index 1e33fd10..50c7773d 100644 --- a/pkg/webrtc/client.go +++ b/pkg/webrtc/client.go @@ -24,6 +24,9 @@ func (c *Conn) CreateOffer(medias []*core.Media) (string, error) { case core.DirectionSendRecv: // default transceiver is sendrecv _, err = c.pc.AddTransceiverFromTrack(NewTrack(media.Kind)) + default: + // Nest cameras require data channel + _, err = c.pc.CreateDataChannel(media.Kind, nil) } if err != nil { diff --git a/pkg/webrtc/conn.go b/pkg/webrtc/conn.go index b7c3c628..e3b1c960 100644 --- a/pkg/webrtc/conn.go +++ b/pkg/webrtc/conn.go @@ -148,6 +148,17 @@ func (c *Conn) getTranseiver(mid string) *webrtc.RTPTransceiver { return nil } +func (c *Conn) getSenderTrack(mid string) *Track { + if tr := c.getTranseiver(mid); tr != nil { + if s := tr.Sender(); s != nil { + if t := s.Track().(*Track); t != nil { + return t + } + } + } + return nil +} + func (c *Conn) getMediaCodec(remote *webrtc.TrackRemote) (*core.Media, *core.Codec) { for _, tr := range c.pc.GetTransceivers() { // search Transeiver for this TrackRemote diff --git a/pkg/webrtc/consumer.go b/pkg/webrtc/consumer.go index b25cb7e3..070573c6 100644 --- a/pkg/webrtc/consumer.go +++ b/pkg/webrtc/consumer.go @@ -2,6 +2,7 @@ package webrtc import ( "encoding/json" + "errors" "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/h264" "github.com/AlexxIT/go2rtc/pkg/h265" @@ -31,7 +32,11 @@ func (c *Conn) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiv panic(core.Caller()) } - localTrack := c.getTranseiver(media.ID).Sender().Track().(*Track) + localTrack := c.getSenderTrack(media.ID) + if localTrack == nil { + return errors.New("webrtc: can't get track") + } + payloadType := codec.PayloadType sender := core.NewSender(media, codec) From e29307125cc97033390fc14de28d3567ed8a327e Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Sat, 20 May 2023 06:28:33 +0300 Subject: [PATCH 79/80] Add Nest source for WebRTC cameras --- internal/nest/init.go | 55 ++++++++++++ main.go | 2 + pkg/nest/api.go | 205 ++++++++++++++++++++++++++++++++++++++++++ pkg/nest/client.go | 101 +++++++++++++++++++++ www/add.html | 29 ++++++ 5 files changed, 392 insertions(+) create mode 100644 internal/nest/init.go create mode 100644 pkg/nest/api.go create mode 100644 pkg/nest/client.go diff --git a/internal/nest/init.go b/internal/nest/init.go new file mode 100644 index 00000000..e48224fb --- /dev/null +++ b/internal/nest/init.go @@ -0,0 +1,55 @@ +package nest + +import ( + "github.com/AlexxIT/go2rtc/internal/api" + "github.com/AlexxIT/go2rtc/internal/streams" + "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/AlexxIT/go2rtc/pkg/nest" + "net/http" +) + +func Init() { + streams.HandleFunc("nest", streamNest) + + 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") + cliendSecret := query.Get("client_secret") + refreshToken := query.Get("refresh_token") + projectID := query.Get("project_id") + + nestAPI, err := nest.NewAPI(cliendID, cliendSecret, refreshToken) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + devices, err := nestAPI.GetDevices(projectID) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + var items []api.Stream + + for name, deviceID := range devices { + query.Set("device_id", deviceID) + + items = append(items, api.Stream{ + Name: name, URL: "nest:?" + query.Encode(), + }) + } + + api.ResponseStreams(w, items) +} diff --git a/main.go b/main.go index 973537cd..6255841d 100644 --- a/main.go +++ b/main.go @@ -17,6 +17,7 @@ import ( "github.com/AlexxIT/go2rtc/internal/mjpeg" "github.com/AlexxIT/go2rtc/internal/mp4" "github.com/AlexxIT/go2rtc/internal/mpegts" + "github.com/AlexxIT/go2rtc/internal/nest" "github.com/AlexxIT/go2rtc/internal/ngrok" "github.com/AlexxIT/go2rtc/internal/onvif" "github.com/AlexxIT/go2rtc/internal/roborock" @@ -51,6 +52,7 @@ func main() { isapi.Init() mpegts.Init() roborock.Init() + nest.Init() srtp.Init() homekit.Init() diff --git a/pkg/nest/api.go b/pkg/nest/api.go new file mode 100644 index 00000000..9c7f4546 --- /dev/null +++ b/pkg/nest/api.go @@ -0,0 +1,205 @@ +package nest + +import ( + "bytes" + "encoding/json" + "errors" + "net/http" + "net/url" + "strings" + "sync" + "time" +) + +type API struct { + Token string + ExpiresAt time.Time +} + +type Auth struct { + AccessToken string +} + +var cache = map[string]*API{} +var cacheMu sync.Mutex + +func NewAPI(clientID, clientSecret, refreshToken string) (*API, error) { + cacheMu.Lock() + defer cacheMu.Unlock() + + key := clientID + ":" + clientSecret + ":" + refreshToken + now := time.Now() + + if api := cache[key]; api != nil && now.Before(api.ExpiresAt) { + return api, nil + } + + data := url.Values{ + "grant_type": []string{"refresh_token"}, + "client_id": []string{clientID}, + "client_secret": []string{clientSecret}, + "refresh_token": []string{refreshToken}, + } + + client := &http.Client{Timeout: time.Second * 5000} + res, err := client.PostForm("https://www.googleapis.com/oauth2/v4/token", data) + if err != nil { + return nil, err + } + if res.StatusCode != 200 { + return nil, errors.New("nest: wrong status: " + res.Status) + } + + var resv struct { + AccessToken string `json:"access_token"` + ExpiresIn time.Duration `json:"expires_in"` + Scope string `json:"scope"` + TokenType string `json:"token_type"` + } + + if err = json.NewDecoder(res.Body).Decode(&resv); err != nil { + return nil, err + } + + api := &API{ + Token: resv.AccessToken, + ExpiresAt: now.Add(resv.ExpiresIn * time.Second), + } + + cache[key] = api + + return api, nil +} + +func (a *API) GetDevices(projectID string) (map[string]string, error) { + uri := "https://smartdevicemanagement.googleapis.com/v1/enterprises/" + projectID + "/devices" + req, err := http.NewRequest("GET", uri, nil) + if err != nil { + return nil, err + } + + req.Header.Set("Authorization", "Bearer "+a.Token) + + client := &http.Client{Timeout: time.Second * 5000} + res, err := client.Do(req) + if err != nil { + return nil, err + } + + if res.StatusCode != 200 { + return nil, errors.New("nest: wrong status: " + res.Status) + } + + var resv struct { + Devices []Device + } + + if err = json.NewDecoder(res.Body).Decode(&resv); err != nil { + return nil, err + } + + devices := map[string]string{} + + for _, device := range resv.Devices { + if len(device.Traits.SdmDevicesTraitsCameraLiveStream.SupportedProtocols) == 0 { + continue + } + + if device.Traits.SdmDevicesTraitsCameraLiveStream.SupportedProtocols[0] != "WEB_RTC" { + continue + } + + i := strings.LastIndexByte(device.Name, '/') + if i <= 0 { + continue + } + + name := device.Traits.SdmDevicesTraitsInfo.CustomName + devices[name] = device.Name[i+1:] + } + + return devices, nil +} + +func (a *API) ExchangeSDP(projectID, deviceID, offer string) (string, error) { + var reqv struct { + Command string `json:"command"` + Params struct { + Offer string `json:"offerSdp"` + } `json:"params"` + } + reqv.Command = "sdm.devices.commands.CameraLiveStream.GenerateWebRtcStream" + reqv.Params.Offer = offer + + 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 { + Answer string `json:"answerSdp"` + ExpiresAt time.Time `json:"expiresAt"` + MediaSessionId string `json:"mediaSessionId"` + } `json:"results"` + } + + if err = json.NewDecoder(res.Body).Decode(&resv); err != nil { + return "", err + } + + return resv.Results.Answer, nil +} + +type Device struct { + Name string `json:"name"` + Type string `json:"type"` + //Assignee string `json:"assignee"` + Traits struct { + SdmDevicesTraitsInfo struct { + CustomName string `json:"customName"` + } `json:"sdm.devices.traits.Info"` + SdmDevicesTraitsCameraLiveStream struct { + VideoCodecs []string `json:"videoCodecs"` + AudioCodecs []string `json:"audioCodecs"` + SupportedProtocols []string `json:"supportedProtocols"` + } `json:"sdm.devices.traits.CameraLiveStream"` + //SdmDevicesTraitsCameraImage struct { + // MaxImageResolution struct { + // Width int `json:"width"` + // Height int `json:"height"` + // } `json:"maxImageResolution"` + //} `json:"sdm.devices.traits.CameraImage"` + //SdmDevicesTraitsCameraPerson struct { + //} `json:"sdm.devices.traits.CameraPerson"` + //SdmDevicesTraitsCameraMotion struct { + //} `json:"sdm.devices.traits.CameraMotion"` + //SdmDevicesTraitsDoorbellChime struct { + //} `json:"sdm.devices.traits.DoorbellChime"` + //SdmDevicesTraitsCameraClipPreview struct { + //} `json:"sdm.devices.traits.CameraClipPreview"` + } `json:"traits"` + //ParentRelations []struct { + // Parent string `json:"parent"` + // DisplayName string `json:"displayName"` + //} `json:"parentRelations"` +} diff --git a/pkg/nest/client.go b/pkg/nest/client.go new file mode 100644 index 00000000..5e8cad3a --- /dev/null +++ b/pkg/nest/client.go @@ -0,0 +1,101 @@ +package nest + +import ( + "errors" + "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/AlexxIT/go2rtc/pkg/webrtc" + pion "github.com/pion/webrtc/v3" + "net/url" +) + +type Client struct { + conn *webrtc.Conn +} + +func NewClient(rawURL string) (*Client, error) { + u, err := url.Parse(rawURL) + if err != nil { + return nil, err + } + + query := u.Query() + cliendID := query.Get("client_id") + cliendSecret := query.Get("client_secret") + refreshToken := query.Get("refresh_token") + projectID := query.Get("project_id") + deviceID := query.Get("device_id") + + if cliendID == "" || cliendSecret == "" || refreshToken == "" || projectID == "" || deviceID == "" { + return nil, errors.New("nest: wrong query") + } + + nestAPI, err := NewAPI(cliendID, cliendSecret, refreshToken) + if err != nil { + return nil, err + } + + rtcAPI, err := webrtc.NewAPI("") + if err != nil { + return nil, err + } + + conf := pion.Configuration{} + pc, err := rtcAPI.NewPeerConnection(conf) + if err != nil { + return nil, err + } + + conn := webrtc.NewConn(pc) + conn.Desc = "Nest" + conn.Mode = core.ModeActiveProducer + + // https://developers.google.com/nest/device-access/traits/device/camera-live-stream#generatewebrtcstream-request-fields + medias := []*core.Media{ + {Kind: core.KindAudio, Direction: core.DirectionRecvonly}, + {Kind: core.KindVideo, Direction: core.DirectionRecvonly}, + {Kind: "app"}, // important for Nest + } + + // 3. Create offer with candidates + offer, err := conn.CreateCompleteOffer(medias) + if err != nil { + return nil, err + } + + // 4. Exchange SDP via Hass + answer, err := nestAPI.ExchangeSDP(projectID, deviceID, offer) + if err != nil { + return nil, err + } + + // 5. Set answer with remote medias + if err = conn.SetAnswer(answer); err != nil { + return nil, err + } + + return &Client{conn: conn}, nil +} + +func (c *Client) GetMedias() []*core.Media { + return c.conn.GetMedias() +} + +func (c *Client) 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 { + return c.conn.Start() +} + +func (c *Client) Stop() error { + return c.conn.Stop() +} + +func (c *Client) MarshalJSON() ([]byte, error) { + return c.conn.MarshalJSON() +} diff --git a/www/add.html b/www/add.html index 06daa33d..0a99facb 100644 --- a/www/add.html +++ b/www/add.html @@ -197,6 +197,35 @@ + +
    + + + + + + + + +
    +
    + + +
    From 82a8e07b66da8c44c83e63a54c6d0ae78ad1bcfc Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Sat, 20 May 2023 06:29:14 +0300 Subject: [PATCH 80/80] Rewrite shell signal handling --- cmd/go2rtc_hass/main.go | 20 ++++++++++++++++++++ cmd/go2rtc_rtsp/main.go | 10 ++-------- main.go | 10 ++-------- pkg/shell/env.go | 32 -------------------------------- pkg/shell/shell.go | 35 +++++++++++++++++++++++++++++++++++ 5 files changed, 59 insertions(+), 48 deletions(-) create mode 100644 cmd/go2rtc_hass/main.go delete mode 100644 pkg/shell/env.go diff --git a/cmd/go2rtc_hass/main.go b/cmd/go2rtc_hass/main.go new file mode 100644 index 00000000..42c2d150 --- /dev/null +++ b/cmd/go2rtc_hass/main.go @@ -0,0 +1,20 @@ +package main + +import ( + "github.com/AlexxIT/go2rtc/internal/api" + "github.com/AlexxIT/go2rtc/internal/app" + "github.com/AlexxIT/go2rtc/internal/hass" + "github.com/AlexxIT/go2rtc/internal/streams" + "github.com/AlexxIT/go2rtc/pkg/shell" +) + +func main() { + app.Init() + streams.Init() + + api.Init() + + hass.Init() + + shell.RunUntilSignal() +} diff --git a/cmd/go2rtc_rtsp/main.go b/cmd/go2rtc_rtsp/main.go index 2babffab..07d32564 100644 --- a/cmd/go2rtc_rtsp/main.go +++ b/cmd/go2rtc_rtsp/main.go @@ -4,9 +4,7 @@ import ( "github.com/AlexxIT/go2rtc/internal/app" "github.com/AlexxIT/go2rtc/internal/rtsp" "github.com/AlexxIT/go2rtc/internal/streams" - "os" - "os/signal" - "syscall" + "github.com/AlexxIT/go2rtc/pkg/shell" ) func main() { @@ -15,9 +13,5 @@ func main() { rtsp.Init() - sigs := make(chan os.Signal, 1) - signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) - <-sigs - - println("exit OK") + shell.RunUntilSignal() } diff --git a/main.go b/main.go index 6255841d..97476cd1 100644 --- a/main.go +++ b/main.go @@ -28,9 +28,7 @@ import ( "github.com/AlexxIT/go2rtc/internal/tapo" "github.com/AlexxIT/go2rtc/internal/webrtc" "github.com/AlexxIT/go2rtc/internal/webtorrent" - "os" - "os/signal" - "syscall" + "github.com/AlexxIT/go2rtc/pkg/shell" ) func main() { @@ -66,9 +64,5 @@ func main() { ngrok.Init() debug.Init() - sigs := make(chan os.Signal, 1) - signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) - <-sigs - - println("exit OK") + shell.RunUntilSignal() } diff --git a/pkg/shell/env.go b/pkg/shell/env.go deleted file mode 100644 index a8867f6a..00000000 --- a/pkg/shell/env.go +++ /dev/null @@ -1,32 +0,0 @@ -package shell - -import ( - "os" - "regexp" - "strings" -) - -func ReplaceEnvVars(text string) string { - re := regexp.MustCompile(`\${([^}{]+)}`) - return re.ReplaceAllStringFunc(text, func(match string) string { - key := match[2 : len(match)-1] - - var def string - var dok bool - - if i := strings.IndexByte(key, ':'); i > 0 { - key, def = key[:i], key[i+1:] - dok = true - } - - if value, vok := os.LookupEnv(key); vok { - return value - } - - if dok { - return def - } - - return match - }) -} diff --git a/pkg/shell/shell.go b/pkg/shell/shell.go index 0b080876..719c0e68 100644 --- a/pkg/shell/shell.go +++ b/pkg/shell/shell.go @@ -1,7 +1,11 @@ package shell import ( + "os" + "os/signal" + "regexp" "strings" + "syscall" ) func QuoteSplit(s string) []string { @@ -39,3 +43,34 @@ func QuoteSplit(s string) []string { } return a } + +func ReplaceEnvVars(text string) string { + re := regexp.MustCompile(`\${([^}{]+)}`) + return re.ReplaceAllStringFunc(text, func(match string) string { + key := match[2 : len(match)-1] + + var def string + var dok bool + + if i := strings.IndexByte(key, ':'); i > 0 { + key, def = key[:i], key[i+1:] + dok = true + } + + if value, vok := os.LookupEnv(key); vok { + return value + } + + if dok { + return def + } + + return match + }) +} + +func RunUntilSignal() { + sigs := make(chan os.Signal, 1) + signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) + println("exit with signal:", (<-sigs).String()) +}