From 500b8720d5e66b8b9c7e4032e89d4cac40ebe08a Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Sun, 29 Jan 2023 18:55:37 +0300 Subject: [PATCH 01/24] Fix bug with no stream from some Dahua cameras --- pkg/rtsp/conn.go | 15 ++++++++------- pkg/rtsp/streamer.go | 2 +- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/pkg/rtsp/conn.go b/pkg/rtsp/conn.go index 39703673..39523ca6 100644 --- a/pkg/rtsp/conn.go +++ b/pkg/rtsp/conn.go @@ -347,7 +347,7 @@ func (c *Conn) Describe() error { func (c *Conn) Setup() error { for _, media := range c.Medias { - _, err := c.SetupMedia(media, media.Codecs[0]) + _, err := c.SetupMedia(media, media.Codecs[0], true) if err != nil { return err } @@ -356,11 +356,12 @@ func (c *Conn) Setup() error { return nil } -func (c *Conn) SetupMedia( - media *streamer.Media, codec *streamer.Codec, -) (*streamer.Track, error) { - c.stateMu.Lock() - defer c.stateMu.Unlock() +func (c *Conn) SetupMedia(media *streamer.Media, codec *streamer.Codec, first bool) (*streamer.Track, error) { + // TODO: rewrite recoonection and first flag + if first { + c.stateMu.Lock() + defer c.stateMu.Unlock() + } if c.state != StateConn && c.state != StateSetup { return nil, fmt.Errorf("RTSP SETUP from wrong state: %s", c.state) @@ -412,7 +413,7 @@ func (c *Conn) SetupMedia( for _, newMedia := range c.Medias { if newMedia.Control == media.Control { - return c.SetupMedia(newMedia, newMedia.Codecs[0]) + return c.SetupMedia(newMedia, newMedia.Codecs[0], false) } } } diff --git a/pkg/rtsp/streamer.go b/pkg/rtsp/streamer.go index 2af46629..7fea6d97 100644 --- a/pkg/rtsp/streamer.go +++ b/pkg/rtsp/streamer.go @@ -45,7 +45,7 @@ func (c *Conn) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer. return streamer.NewTrack(codec, media.Direction) } - track, err := c.SetupMedia(media, codec) + track, err := c.SetupMedia(media, codec, true) if err != nil { return nil } From c1d6adc189441abaacb68a3ac4072a37e11ca6b7 Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Mon, 30 Jan 2023 19:13:35 +0300 Subject: [PATCH 02/24] Move ParseQuery from rtsp to mp4 module --- cmd/rtsp/rtsp.go | 11 +---------- pkg/mp4/consumer.go | 10 ++++++++++ 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/cmd/rtsp/rtsp.go b/cmd/rtsp/rtsp.go index aa910a98..21143d09 100644 --- a/cmd/rtsp/rtsp.go +++ b/cmd/rtsp/rtsp.go @@ -165,7 +165,7 @@ func tcpHandler(conn *rtsp.Conn) { conn.SessionName = app.UserAgent - conn.Medias = ParseQuery(conn.URL.Query()) + conn.Medias = mp4.ParseQuery(conn.URL.Query()) if err := stream.AddConsumer(conn); err != nil { log.Warn().Err(err).Str("stream", name).Msg("[rtsp]") @@ -229,12 +229,3 @@ func tcpHandler(conn *rtsp.Conn) { _ = conn.Close() } - -func ParseQuery(query map[string][]string) []*streamer.Media { - if query["mp4"] != nil { - cons := mp4.Consumer{} - return cons.GetMedias() - } - - return streamer.ParseQuery(query) -} diff --git a/pkg/mp4/consumer.go b/pkg/mp4/consumer.go index 8d5fbf2d..777ee340 100644 --- a/pkg/mp4/consumer.go +++ b/pkg/mp4/consumer.go @@ -24,6 +24,16 @@ type Consumer struct { send uint32 } +// ParseQuery - like usual parse, but with mp4 param handler +func ParseQuery(query map[string][]string) []*streamer.Media { + if query["mp4"] != nil { + cons := Consumer{} + return cons.GetMedias() + } + + return streamer.ParseQuery(query) +} + const ( waitNone byte = iota waitKeyframe From 0f934be9b649b2d734c05a76569dd814deecd8ad Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Mon, 30 Jan 2023 19:15:05 +0300 Subject: [PATCH 03/24] Add MimeCodecs to mp4 Muxer --- pkg/mp4/consumer.go | 6 +++++- pkg/mp4/muxer.go | 6 +++--- pkg/mp4/segment.go | 2 +- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/pkg/mp4/consumer.go b/pkg/mp4/consumer.go index 777ee340..496ae65b 100644 --- a/pkg/mp4/consumer.go +++ b/pkg/mp4/consumer.go @@ -171,8 +171,12 @@ func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *strea panic("unsupported codec") } +func (c *Consumer) MimeCodecs() string { + return c.muxer.MimeCodecs(c.codecs) +} + func (c *Consumer) MimeType() string { - return c.muxer.MimeType(c.codecs) + return `video/mp4; codecs="` + c.MimeCodecs() + `"` } func (c *Consumer) Init() ([]byte, error) { diff --git a/pkg/mp4/muxer.go b/pkg/mp4/muxer.go index 83bb33c5..cd7d9762 100644 --- a/pkg/mp4/muxer.go +++ b/pkg/mp4/muxer.go @@ -24,8 +24,8 @@ const ( MimeOpus = "opus" ) -func (m *Muxer) MimeType(codecs []*streamer.Codec) string { - s := `video/mp4; codecs="` +func (m *Muxer) MimeCodecs(codecs []*streamer.Codec) string { + var s string for i, codec := range codecs { if i > 0 { @@ -46,7 +46,7 @@ func (m *Muxer) MimeType(codecs []*streamer.Codec) string { } } - return s + `"` + return s } func (m *Muxer) GetInit(codecs []*streamer.Codec) ([]byte, error) { diff --git a/pkg/mp4/segment.go b/pkg/mp4/segment.go index 9cc3a88a..6fac7a40 100644 --- a/pkg/mp4/segment.go +++ b/pkg/mp4/segment.go @@ -50,7 +50,7 @@ func (c *Segment) AddTrack(media *streamer.Media, track *streamer.Track) *stream return nil } - c.MimeType = muxer.MimeType(codecs) + c.MimeType = `video/mp4; codecs="` + muxer.MimeCodecs(codecs) + `"` switch track.Codec.Name { case streamer.CodecH264: From 2d49cfd4b64c69284ce4effa513cc60af5bd5cb4 Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Mon, 30 Jan 2023 19:15:32 +0300 Subject: [PATCH 04/24] Code refactoring --- cmd/mp4/mp4.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/mp4/mp4.go b/cmd/mp4/mp4.go index abcedef4..de9006d8 100644 --- a/cmd/mp4/mp4.go +++ b/cmd/mp4/mp4.go @@ -84,8 +84,8 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) { cons := &mp4.Consumer{ RemoteAddr: r.RemoteAddr, UserAgent: r.UserAgent(), + Medias: streamer.ParseQuery(r.URL.Query()), } - cons.Medias = streamer.ParseQuery(r.URL.Query()) cons.Listen(func(msg interface{}) { if data, ok := msg.([]byte); ok { From 56633229ed0ae75a467ea69615b42bdcbb02e9cc Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Mon, 30 Jan 2023 21:21:17 +0300 Subject: [PATCH 05/24] Fix AAC support for old MP4 consumer --- pkg/mp4/v1/consumer.go | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/pkg/mp4/v1/consumer.go b/pkg/mp4/v1/consumer.go index 75dbd59e..491b668a 100644 --- a/pkg/mp4/v1/consumer.go +++ b/pkg/mp4/v1/consumer.go @@ -1,6 +1,8 @@ package mp4 import ( + "encoding/hex" + "github.com/AlexxIT/go2rtc/pkg/aac" "github.com/AlexxIT/go2rtc/pkg/h264" "github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/deepch/vdk/av" @@ -101,7 +103,17 @@ func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *strea return track.Bind(push) case streamer.CodecAAC: - stream, _ := aacparser.NewCodecDataFromMPEG4AudioConfigBytes([]byte{20, 8}) + s := streamer.Between(codec.FmtpLine, "config=", ";") + + b, err := hex.DecodeString(s) + if err != nil { + return nil + } + + stream, err := aacparser.NewCodecDataFromMPEG4AudioConfigBytes(b) + if err != nil { + return nil + } c.mimeType += ",mp4a.40.2" c.streams = append(c.streams, stream) @@ -131,6 +143,11 @@ func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *strea return nil } + if codec.IsRTP() { + wrapper := aac.RTPDepay(track) + push = wrapper(push) + } + return track.Bind(push) } From fb4b6099141ae4e0253a7094ea5a2a3baa1d9503 Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Mon, 30 Jan 2023 21:21:57 +0300 Subject: [PATCH 06/24] Add support output as HLS (TS+fMP4) --- cmd/hls/hls.go | 261 +++++++++++++++++++++++++++++++++++++++++++++++++ main.go | 2 + pkg/ts/ts.go | 200 +++++++++++++++++++++++++++++++++++++ 3 files changed, 463 insertions(+) create mode 100644 cmd/hls/hls.go create mode 100644 pkg/ts/ts.go diff --git a/cmd/hls/hls.go b/cmd/hls/hls.go new file mode 100644 index 00000000..32618c64 --- /dev/null +++ b/cmd/hls/hls.go @@ -0,0 +1,261 @@ +package hls + +import ( + "fmt" + "github.com/AlexxIT/go2rtc/cmd/api" + "github.com/AlexxIT/go2rtc/cmd/streams" + "github.com/AlexxIT/go2rtc/pkg/mp4" + "github.com/AlexxIT/go2rtc/pkg/streamer" + "github.com/AlexxIT/go2rtc/pkg/ts" + "github.com/rs/zerolog/log" + "net/http" + "strconv" + "sync" + "time" +) + +func Init() { + api.HandleFunc("api/stream.m3u8", handlerStream) + api.HandleFunc("api/hls/playlist.m3u8", handlerPlaylist) + + // HLS (TS) + api.HandleFunc("api/hls/segment.ts", handlerSegmentTS) + + // HLS (fMP4) + api.HandleFunc("api/hls/init.mp4", handlerInit) + api.HandleFunc("api/hls/segment.m4s", handlerSegmentMP4) +} + +type Consumer interface { + streamer.Consumer + Init() ([]byte, error) + MimeCodecs() string + Start() +} + +type Session struct { + cons Consumer + playlist string + init []byte + segment []byte + seq int + alive *time.Timer + mu sync.Mutex +} + +const keepalive = 5 * time.Second + +var sessions = map[string]*Session{} + +func handlerStream(w http.ResponseWriter, r *http.Request) { + // CORS important for Chromecast + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Content-Type", "application/vnd.apple.mpegurl") + + if r.Method == "OPTIONS" { + w.Header().Set("Access-Control-Allow-Methods", "GET") + return + } + + src := r.URL.Query().Get("src") + stream := streams.GetOrNew(src) + if stream == nil { + http.Error(w, api.StreamNotFound, http.StatusNotFound) + return + } + + var cons Consumer + + // use fMP4 with codecs filter and TS without + medias := mp4.ParseQuery(r.URL.Query()) + if medias != nil { + cons = &mp4.Consumer{ + RemoteAddr: r.RemoteAddr, + UserAgent: r.UserAgent(), + Medias: medias, + } + } else { + cons = &ts.Consumer{ + RemoteAddr: r.RemoteAddr, + UserAgent: r.UserAgent(), + } + } + + session := &Session{cons: cons} + + cons.Listen(func(msg interface{}) { + if data, ok := msg.([]byte); ok { + session.mu.Lock() + session.segment = append(session.segment, data...) + session.mu.Unlock() + } + }) + + if err := stream.AddConsumer(cons); err != nil { + log.Error().Err(err).Caller().Send() + return + } + + session.alive = time.AfterFunc(keepalive, func() { + stream.RemoveConsumer(cons) + }) + session.init, _ = cons.Init() + + cons.Start() + + sid := strconv.FormatInt(time.Now().UnixNano(), 10) + + // two segments important for Chromecast + if medias != nil { + session.playlist = `#EXTM3U +#EXT-X-VERSION:6 +#EXT-X-TARGETDURATION:1 +#EXT-X-MEDIA-SEQUENCE:%d +#EXT-X-MAP:URI="init.mp4?id=` + sid + `" +#EXTINF:0.500, +segment.m4s?id=` + sid + `&n=%d +#EXTINF:0.500, +segment.m4s?id=` + sid + `&n=%d` + } else { + session.playlist = `#EXTM3U +#EXT-X-VERSION:3 +#EXT-X-TARGETDURATION:1 +#EXT-X-MEDIA-SEQUENCE:%d +#EXTINF:0.500, +segment.ts?id=` + sid + `&n=%d +#EXTINF:0.500, +segment.ts?id=` + sid + `&n=%d` + } + + sessions[sid] = session + + // bandwidth important for Safari, codecs useful for smooth playback + data := []byte(`#EXTM3U +#EXT-X-STREAM-INF:BANDWIDTH=1000000,CODECS="` + cons.MimeCodecs() + `" +hls/playlist.m3u8?id=` + sid) + + if _, err := w.Write(data); err != nil { + log.Error().Err(err).Caller().Send() + } +} + +func handlerPlaylist(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Content-Type", "application/vnd.apple.mpegurl") + + if r.Method == "OPTIONS" { + w.Header().Set("Access-Control-Allow-Methods", "GET") + return + } + + sid := r.URL.Query().Get("id") + session := sessions[sid] + if session == nil { + http.NotFound(w, r) + return + } + + s := fmt.Sprintf(session.playlist, session.seq, session.seq, session.seq+1) + + if _, err := w.Write([]byte(s)); err != nil { + log.Error().Err(err).Caller().Send() + } +} + +func handlerSegmentTS(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Content-Type", "video/mp2t") + + if r.Method == "OPTIONS" { + w.Header().Set("Access-Control-Allow-Methods", "GET") + return + } + + sid := r.URL.Query().Get("id") + session := sessions[sid] + if session == nil { + http.NotFound(w, r) + return + } + + session.alive.Reset(keepalive) + + var i byte + for len(session.segment) == 0 { + if i++; i > 10 { + http.NotFound(w, r) + return + } + time.Sleep(time.Millisecond * 100) + } + + session.mu.Lock() + data := session.segment + // important to start new segment with init + session.segment = session.init + session.seq++ + session.mu.Unlock() + + if _, err := w.Write(data); err != nil { + log.Error().Err(err).Caller().Send() + } +} + +func handlerInit(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Add("Content-Type", "video/mp4") + + if r.Method == "OPTIONS" { + w.Header().Set("Access-Control-Allow-Methods", "GET") + return + } + + sid := r.URL.Query().Get("id") + session := sessions[sid] + if session == nil { + http.NotFound(w, r) + return + } + + if _, err := w.Write(session.init); err != nil { + log.Error().Err(err).Caller().Send() + } +} + +func handlerSegmentMP4(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Add("Content-Type", "video/iso.segment") + + if r.Method == "OPTIONS" { + w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS") + return + } + + sid := r.URL.Query().Get("id") + session := sessions[sid] + if session == nil { + http.NotFound(w, r) + return + } + + session.alive.Reset(keepalive) + + var i byte + for len(session.segment) == 0 { + if i++; i > 10 { + http.NotFound(w, r) + return + } + time.Sleep(time.Millisecond * 100) + } + + session.mu.Lock() + data := session.segment + session.segment = nil + session.seq++ + session.mu.Unlock() + + if _, err := w.Write(data); err != nil { + log.Error().Err(err).Caller().Send() + } +} diff --git a/main.go b/main.go index 63826914..63b7072e 100644 --- a/main.go +++ b/main.go @@ -8,6 +8,7 @@ import ( "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/ivideon" @@ -42,6 +43,7 @@ func main() { webrtc.Init() mp4.Init() + hls.Init() mjpeg.Init() http.Init() diff --git a/pkg/ts/ts.go b/pkg/ts/ts.go new file mode 100644 index 00000000..ad80e0ca --- /dev/null +++ b/pkg/ts/ts.go @@ -0,0 +1,200 @@ +package ts + +import ( + "bytes" + "encoding/hex" + "github.com/AlexxIT/go2rtc/pkg/aac" + "github.com/AlexxIT/go2rtc/pkg/h264" + "github.com/AlexxIT/go2rtc/pkg/streamer" + "github.com/deepch/vdk/av" + "github.com/deepch/vdk/codec/aacparser" + "github.com/deepch/vdk/codec/h264parser" + "github.com/deepch/vdk/format/ts" + "github.com/pion/rtp" + "sync/atomic" + "time" +) + +type Consumer struct { + streamer.Element + + UserAgent string + RemoteAddr string + + buf *bytes.Buffer + muxer *ts.Muxer + mimeType string + streams []av.CodecData + start bool + init []byte + + send uint32 +} + +func (c *Consumer) GetMedias() []*streamer.Media { + return []*streamer.Media{ + { + Kind: streamer.KindVideo, + Direction: streamer.DirectionRecvonly, + Codecs: []*streamer.Codec{ + {Name: streamer.CodecH264}, + }, + }, + //{ + // Kind: streamer.KindAudio, + // Direction: streamer.DirectionRecvonly, + // Codecs: []*streamer.Codec{ + // {Name: streamer.CodecAAC}, + // }, + //}, + } +} + +func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track { + codec := track.Codec + trackID := int8(len(c.streams)) + + switch codec.Name { + case streamer.CodecH264: + sps, pps := h264.GetParameterSet(codec.FmtpLine) + stream, err := h264parser.NewCodecDataFromSPSAndPPS(sps, pps) + if err != nil { + return nil + } + + if len(c.mimeType) > 0 { + c.mimeType += "," + } + + // TODO: fixme + // some devices won't play high level + if stream.RecordInfo.AVCLevelIndication <= 0x29 { + c.mimeType += "avc1." + h264.GetProfileLevelID(codec.FmtpLine) + } else { + c.mimeType += "avc1.640029" + } + + c.streams = append(c.streams, stream) + + pkt := av.Packet{Idx: trackID, CompositionTime: time.Millisecond} + + ts2time := time.Second / time.Duration(codec.ClockRate) + + push := func(packet *rtp.Packet) error { + if packet.Version != h264.RTPPacketVersionAVC { + return nil + } + + if !c.start { + return nil + } + + pkt.Data = packet.Payload + newTime := time.Duration(packet.Timestamp) * ts2time + if pkt.Time > 0 { + pkt.Duration = newTime - pkt.Time + } + pkt.Time = newTime + + if err = c.muxer.WritePacket(pkt); err != nil { + return err + } + + // clone bytes from buffer, so next packet won't overwrite it + buf := append([]byte{}, c.buf.Bytes()...) + atomic.AddUint32(&c.send, uint32(len(buf))) + c.Fire(buf) + + c.buf.Reset() + + return nil + } + + if codec.IsRTP() { + wrapper := h264.RTPDepay(track) + push = wrapper(push) + } + + return track.Bind(push) + + case streamer.CodecAAC: + s := streamer.Between(codec.FmtpLine, "config=", ";") + + b, err := hex.DecodeString(s) + if err != nil { + return nil + } + + stream, err := aacparser.NewCodecDataFromMPEG4AudioConfigBytes(b) + if err != nil { + return nil + } + + if len(c.mimeType) > 0 { + c.mimeType += "," + } + + c.mimeType += "mp4a.40.2" + c.streams = append(c.streams, stream) + + pkt := av.Packet{Idx: trackID, CompositionTime: time.Millisecond} + + ts2time := time.Second / time.Duration(codec.ClockRate) + + push := func(packet *rtp.Packet) error { + if !c.start { + return nil + } + + pkt.Data = packet.Payload + newTime := time.Duration(packet.Timestamp) * ts2time + if pkt.Time > 0 { + pkt.Duration = newTime - pkt.Time + } + pkt.Time = newTime + + if err := c.muxer.WritePacket(pkt); err != nil { + return err + } + + // clone bytes from buffer, so next packet won't overwrite it + buf := append([]byte{}, c.buf.Bytes()...) + atomic.AddUint32(&c.send, uint32(len(buf))) + c.Fire(buf) + + c.buf.Reset() + + return nil + } + + if codec.IsRTP() { + wrapper := aac.RTPDepay(track) + push = wrapper(push) + } + + return track.Bind(push) + } + + panic("unsupported codec") +} + +func (c *Consumer) MimeCodecs() string { + return c.mimeType +} + +func (c *Consumer) Init() ([]byte, error) { + c.buf = bytes.NewBuffer(nil) + c.muxer = ts.NewMuxer(c.buf) + + // first packet will be with header, it's ok + if err := c.muxer.WriteHeader(c.streams); err != nil { + return nil, err + } + data := append([]byte{}, c.buf.Bytes()...) + + return data, nil +} + +func (c *Consumer) Start() { + c.start = true +} From f4d2c801f094d98189b694f20738e3a99f302b57 Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Mon, 30 Jan 2023 22:00:07 +0300 Subject: [PATCH 07/24] Add redirect for Safari from MP4 to HLS --- cmd/mp4/mp4.go | 47 ++++++++++++++++++++++++----------------------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/cmd/mp4/mp4.go b/cmd/mp4/mp4.go index de9006d8..d98cae80 100644 --- a/cmd/mp4/mp4.go +++ b/cmd/mp4/mp4.go @@ -26,8 +26,14 @@ func Init() { var log zerolog.Logger func handlerKeyframe(w http.ResponseWriter, r *http.Request) { - if isChromeFirst(w, r) { - return + // Chrome 105 does two requests: without Range and with `Range: bytes=0-` + 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 + } } src := r.URL.Query().Get("src") @@ -68,7 +74,22 @@ func handlerKeyframe(w http.ResponseWriter, r *http.Request) { func handlerMP4(w http.ResponseWriter, r *http.Request) { log.Trace().Msgf("[mp4] %s %+v", r.Method, r.Header) - if isChromeFirst(w, r) || isSafari(w, r) { + // 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/") { + // auto redirect to HLS/fMP4 format, because Safari not support MP4 stream + url := "stream.m3u8?" + r.URL.RawQuery + if !r.URL.Query().Has("mp4") { + url += "&mp4" + } + + http.Redirect(w, r, url, http.StatusMovedPermanently) return } @@ -138,23 +159,3 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) { duration.Stop() } } - -func isChromeFirst(w http.ResponseWriter, r *http.Request) bool { - // Chrome 105 does two requests: without Range and with `Range: bytes=0-` - if strings.Contains(r.UserAgent(), " Chrome/") { - if r.Header.Values("Range") == nil { - w.Header().Set("Content-Type", "video/mp4") - w.WriteHeader(http.StatusOK) - return true - } - } - return false -} - -func isSafari(w http.ResponseWriter, r *http.Request) bool { - if r.Header.Get("Range") == "bytes=0-1" { - handlerKeyframe(w, r) - return true - } - return false -} From 4a633cd9b58af6c89469b6d9517a8aaf06df7e0c Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Mon, 30 Jan 2023 23:02:06 +0300 Subject: [PATCH 08/24] Move stream useful links to separate page --- www/index.html | 22 ++++--------- www/links.html | 89 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+), 15 deletions(-) create mode 100644 www/links.html diff --git a/www/index.html b/www/index.html index 4781b57f..86497bae 100644 --- a/www/index.html +++ b/www/index.html @@ -89,9 +89,7 @@ const templates = [ 'stream', '2-way-aud', - 'mp4', - 'mjpeg', - 'info', + 'links', 'delete', ]; @@ -138,15 +136,17 @@ for (const [name, value] of Object.entries(data)) { const online = value && value.consumers ? value.consumers.length : 0; + const src = encodeURIComponent(name); const links = templates.map(link => { - return link.replace("{name}", encodeURIComponent(name)); + return link.replace("{name}", src); }).join(" "); const tr = document.createElement("tr"); tr.dataset["id"] = name; tr.innerHTML = `` + - `${online}${links}`; + `${online} / info` + + `${links}`; tbody.appendChild(tr); } }); @@ -156,17 +156,9 @@ fetch(url, {cache: 'no-cache'}).then(r => r.json()).then(data => { const info = document.querySelector(".info"); info.innerText = `Version: ${data.version}, Config: ${data.config_path}`; - - try { - const host = data.host.match(/^[^:]+/)[0]; - const port = data.rtsp.listen.match(/[0-9]+$/)[0]; - templates.splice(4, 0, `rtsp`); - } catch (e) { - templates.splice(4, 0, `rtsp`); - } - - reload(); }); + + reload(); \ No newline at end of file diff --git a/www/links.html b/www/links.html new file mode 100644 index 00000000..4c327fed --- /dev/null +++ b/www/links.html @@ -0,0 +1,89 @@ + + + + go2rtc - links + + + + + + + + + + From 762edf157a21a3a62f24aebd0d67bc6a0d73ab9a Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Tue, 31 Jan 2023 07:32:43 +0300 Subject: [PATCH 09/24] Add default_query setting for RTSP server --- cmd/rtsp/rtsp.go | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/cmd/rtsp/rtsp.go b/cmd/rtsp/rtsp.go index 21143d09..e61809a8 100644 --- a/cmd/rtsp/rtsp.go +++ b/cmd/rtsp/rtsp.go @@ -9,20 +9,23 @@ import ( "github.com/AlexxIT/go2rtc/pkg/tcp" "github.com/rs/zerolog" "net" + "net/url" "strings" ) func Init() { var conf struct { Mod struct { - Listen string `yaml:"listen" json:"listen"` - Username string `yaml:"username" json:"-"` - Password string `yaml:"password" json:"-"` + Listen string `yaml:"listen" json:"listen"` + Username string `yaml:"username" json:"-"` + Password string `yaml:"password" json:"-"` + DefaultQuery string `yaml:"default_query"` } `yaml:"rtsp"` } // default config conf.Mod.Listen = ":8554" + conf.Mod.DefaultQuery = "video&audio" app.LoadConfig(&conf) app.Info["rtsp"] = conf.Mod @@ -50,6 +53,10 @@ func Init() { log.Info().Str("addr", address).Msg("[rtsp] listen") + if query, err := url.ParseQuery(conf.Mod.DefaultQuery); err == nil { + defaultMedias = mp4.ParseQuery(query) + } + go func() { for { conn, err := ln.Accept() @@ -79,6 +86,7 @@ var Port string var log zerolog.Logger var handlers []Handler +var defaultMedias []*streamer.Media func rtspHandler(url string) (streamer.Producer, error) { backchannel := true @@ -166,6 +174,9 @@ func tcpHandler(conn *rtsp.Conn) { conn.SessionName = app.UserAgent conn.Medias = mp4.ParseQuery(conn.URL.Query()) + if conn.Medias == nil { + conn.Medias = defaultMedias + } if err := stream.AddConsumer(conn); err != nil { log.Warn().Err(err).Str("stream", name).Msg("[rtsp]") From 98af8c3dbffc87c267318060e413bacbc65e99fe Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Tue, 31 Jan 2023 08:56:49 +0300 Subject: [PATCH 10/24] Update links page --- www/links.html | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/www/links.html b/www/links.html index 4c327fed..d4572992 100644 --- a/www/links.html +++ b/www/links.html @@ -48,7 +48,7 @@ const links = document.querySelector("#links"); links.innerHTML = `

Any codec in source

-
  • stream.html with auto-select mode / browsers: all / codecs: H264, H265, MJPEG, JPEG, AAC, PCMU, PCMA, OPUS
  • +
  • stream.html with auto-select mode / browsers: all / codecs: H264, H265*, MJPEG, JPEG, AAC, PCMU, PCMA, OPUS
  • info.json page with active connections
  • `; @@ -64,13 +64,13 @@ const links = document.querySelector("#links"); links.innerHTML += ` -
  • rtsp with all tracks / codecs: any
  • +
  • rtsp with only one video and one audio / codecs: any
  • rtsp for MP4 recording (Hass or Frigate) / codecs: H264, H265, AAC
  • -
  • rtsp with only one video and one audio / codecs: any
  • +
  • rtsp with all tracks / codecs: any
  • H264/H265 source

    -
  • stream.html WebRTC stream / browsers: all / codecs: H264, PCMU, PCMA, OPUS
  • -
  • stream.html MSE stream / browsers: Chrome, Firefox, Safari Mac/iPad / codecs: H264, H265*, AAC, OPUS
  • +
  • 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.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
  • frame.mp4 snapshot in MP4-format / browsers: all / codecs: H264, H265*
  • From 7b3505f4f4179d2f0e6ddb5540e26ae39015b0f5 Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Tue, 31 Jan 2023 10:32:28 +0300 Subject: [PATCH 11/24] Update version to 1.1.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 297b3d33..15402a7e 100644 --- a/cmd/app/app.go +++ b/cmd/app/app.go @@ -14,7 +14,7 @@ import ( "time" ) -var Version = "1.0.1" +var Version = "1.1.0" var UserAgent = "go2rtc/" + Version var ConfigPath string From 350e677838512479ec99087994ee7c739b560c6d Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Fri, 27 Jan 2023 17:35:54 +0300 Subject: [PATCH 12/24] Update readme --- README.md | 88 +++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 66 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 0da4d9af..13d021d1 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg - zero-dependency and zero-config [small app](#go2rtc-binary) for all OS (Windows, macOS, Linux, ARM) - zero-delay for many supported protocols (lowest possible streaming latency) - streaming from [RTSP](#source-rtsp), [RTMP](#source-rtmp), [HTTP](#source-http) (FLV/MJPEG/JPEG), [FFmpeg](#source-ffmpeg), [USB Cameras](#source-ffmpeg-device) and [other sources](#module-streams) -- streaming to [RTSP](#module-rtsp), [WebRTC](#module-webrtc), [MSE/MP4](#module-mp4) or [MJPEG](#module-mjpeg) +- streaming to [RTSP](#module-rtsp), [WebRTC](#module-webrtc), [MSE/MP4](#module-mp4), [HLS](#module-hls) or [MJPEG](#module-mjpeg) - first project in the World with support streaming from [HomeKit Cameras](#source-homekit) - first project in the World with support H265 for WebRTC in browser (Safari only, [read more](https://github.com/AlexxIT/Blog/issues/5)) - on the fly transcoding for unsupported codecs via [FFmpeg](#source-ffmpeg) @@ -53,9 +53,11 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg * [From go2rtc to Hass](#from-go2rtc-to-hass) * [From Hass to go2rtc](#from-hass-to-go2rtc) * [Module: MP4](#module-mp4) + * [Module: HLS](#module-hls) * [Module: MJPEG](#module-mjpeg) * [Module: Log](#module-log) * [Security](#security) +* [Codecs filters](#codecs-filters) * [Codecs madness](#codecs-madness) * [Codecs negotiation](#codecs-negotiation) * [TIPS](#tips) @@ -408,20 +410,25 @@ api: You can get any stream as RTSP-stream: `rtsp://192.168.1.123:8554/{stream_name}` -- you can omit the codec filters, so one first video and one first audio will be selected -- you can set `?video=copy` or just `?video`, so only one first video without audio will be selected -- you can set multiple video or audio, so all of them will be selected -- you can enable external password protection for your RTSP streams - -Password protection always disabled for localhost calls (ex. FFmpeg or Hass on same server) +You can enable external password protection for your RTSP streams. Password protection always disabled for localhost calls (ex. FFmpeg or Hass on same server). ```yaml rtsp: listen: ":8554" # RTSP Server TCP port, default - 8554 username: "admin" # optional, default - disabled password: "pass" # optional, default - disabled + default_query: "video&audio" # optional, default codecs filters ``` +By default go2rtc provide RTSP-stream with only one first video and only one first audio. You can change it with the `default_query` setting: + +- `default_query: "mp4"` - MP4 compatible codecs (H264, H265, AAC) +- `default_query: "video=all&audio=all"` - all tracks from all source (not all players can handle this) +- `default_query: "video=h264,h265"` - only one video track (H264 or H265) +- `default_query: "video&audio=all"` - only one first any video and all audio as separate tracks + +Read more about [codecs filters](#codecs-filters). + ### Module: WebRTC WebRTC usually works without problems in the local network. But external access may require additional settings. It depends on what type of Internet do you have. @@ -574,13 +581,28 @@ Provides several features: 1. MSE stream (fMP4 over WebSocket) 2. Camera snapshots in MP4 format (single frame), can be sent to [Telegram](https://github.com/AlexxIT/go2rtc/wiki/Snapshot-to-Telegram) -3. MP4 "file stream" - bad format for streaming because of high start delay, doesn't work in Safari +3. MP4 "file stream" - bad format for streaming because of high start delay. This format doesn't work in all Safari browsers, but go2rtc will automatically redirect it to HLS/fMP4 it this case. 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` +Read more about [codecs filters](#codecs-filters). + +### Module: HLS + +[HLS](https://en.wikipedia.org/wiki/HTTP_Live_Streaming) is the worst technology for real-time streaming. It can only be useful on devices that do not support more modern technology, like [WebRTC](#module-webrtc), [MSE/MP4](#module-mp4). + +The go2rtc implementation differs from the standards and may not work with all players. + +API examples: + +- HLS/TS stream: `http://192.168.1.123:1984/api/stream.m3u8?src=camera1` (H264) +- HLS/fMP4 stream: `http://192.168.1.123:1984/api/stream.m3u8?src=camera1&mp4` (H264, H265, AAC) + +Read more about [codecs filters](#codecs-filters). + ### Module: MJPEG **Important.** For stream as MJPEG format, your source MUST contain the MJPEG codec. If your stream has a MJPEG codec - you can receive **MJPEG stream** or **JPEG snapshots** via API. @@ -647,21 +669,42 @@ If you need Web interface protection without Home Assistant Add-on - you need to PS. Additionally WebRTC will try to use the 8555 UDP port for transmit encrypted media. It works without problems on the local network. And sometimes also works for external access, even if you haven't opened this port on your router ([read more](https://en.wikipedia.org/wiki/UDP_hole_punching)). But for stable external WebRTC access, you need to open the 8555 port on your router for both TCP and UDP. +## Codecs filters + +go2rtc can automatically detect which codecs your device supports for [WebRTC](#module-webrtc) and [MSE](#module-mp4) technologies. + +But it cannot be done for [RTSP](#module-rtsp), [stream.mp4](#module-mp4), [HLS](#module-hls) technologies. You can manually add a codec filter when you create a link to a stream. The filters work the same for all three technologies. Filters do not create a new codec. They only select the suitable codec from existing sources. You can add new codecs to the stream using the [FFmpeg transcoding](#source-ffmpeg). + +Without filters: + +- RTSP will provide only the first video and only the first audio +- MP4 will include only compatible codecs (H264, H265, AAC) +- HLS will output in the legacy TS format (H264 without audio) + +Some examples: + +- `rtsp://192.168.1.123:8554/camera1?mp4` - useful for recording as MP4 files (e.g. Hass or Frigate) +- `rtsp://192.168.1.123:8554/camera1?video=h264,h265&audio=aac` - full version of the filter above +- `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 + ## Codecs madness -`AVC/H.264` codec 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. +`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 | MP4 | -|---------------------|-------------|-------------|-------------| -| *latency* | best | medium | bad | -| Desktop Chrome 107+ | H264 | H264, H265* | H264, H265* | -| Desktop Edge | H264 | H264, H265* | H264, H265* | -| Desktop Safari | H264, H265* | H264, H265 | **no!** | -| Desktop Firefox | H264 | H264 | H264 | -| Android Chrome 107+ | H264 | H264, H265* | H264 | -| iPad Safari 13+ | H264, H265* | H264, H265 | **no!** | -| iPhone Safari 13+ | H264, H265* | **no!** | **no!** | -| masOS Hass App | no | no | no | +| Device | WebRTC | MSE | stream.mp4 | +|---------------------|-------------------------------|------------------------|-----------------------------------------| +| *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 | - 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/) @@ -670,8 +713,9 @@ PS. Additionally WebRTC will try to use the 8555 UDP port for transmit encrypted **Audio** -- WebRTC audio codecs: `PCMU/8000`, `PCMA/8000`, `OPUS/48000/2` -- MSE/MP4 audio codecs: `AAC` +- **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** From cf58a6f952d4cc137df9eb88ff11e819bdf029e6 Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Tue, 31 Jan 2023 17:16:17 +0300 Subject: [PATCH 13/24] Update readme about Hass integration --- README.md | 55 ++++++++++++++++++++++++++++++++----------------------- 1 file changed, 32 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 13d021d1..b166aa06 100644 --- a/README.md +++ b/README.md @@ -31,8 +31,9 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg * [Fast start](#fast-start) * [go2rtc: Binary](#go2rtc-binary) - * [go2rtc: Home Assistant Add-on](#go2rtc-home-assistant-add-on) * [go2rtc: Docker](#go2rtc-docker) + * [go2rtc: Home Assistant Add-on](#go2rtc-home-assistant-add-on) + * [go2rtc: Home Assistant Integration](#go2rtc-home-assistant-integration) * [Configuration](#configuration) * [Module: Streams](#module-streams) * [Source: RTSP](#source-rtsp) @@ -50,8 +51,6 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg * [Module: WebRTC](#module-webrtc) * [Module: Ngrok](#module-ngrok) * [Module: Hass](#module-hass) - * [From go2rtc to Hass](#from-go2rtc-to-hass) - * [From Hass to go2rtc](#from-hass-to-go2rtc) * [Module: MP4](#module-mp4) * [Module: HLS](#module-hls) * [Module: MJPEG](#module-mjpeg) @@ -65,7 +64,7 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg ## Fast start -1. Download [binary](#go2rtc-binary) or use [Docker](#go2rtc-docker) or [Home Assistant Add-on](#go2rtc-home-assistant-add-on) +1. Download [binary](#go2rtc-binary) or use [Docker](#go2rtc-docker) or Home Assistant [Add-on](#go2rtc-home-assistant-add-on) or [Integration](#go2rtc-home-assistant-integration) 2. Open web interface: `http://localhost:1984/` **Optionally:** @@ -94,6 +93,10 @@ Download binary for your OS from [latest release](https://github.com/AlexxIT/go2 Don't forget to fix the rights `chmod +x go2rtc_xxx_xxx` on Linux and Mac. +### go2rtc: Docker + +Container [alexxit/go2rtc](https://hub.docker.com/r/alexxit/go2rtc) with support `amd64`, `386`, `arm64`, `arm`. This container is the same as [Home Assistant Add-on](#go2rtc-home-assistant-add-on), but can be used separately from Home Assistant. Container has preinstalled [FFmpeg](#source-ffmpeg), [Ngrok](#module-ngrok) and [Python](#source-echo). + ### go2rtc: Home Assistant Add-on [![](https://my.home-assistant.io/badges/supervisor_addon.svg)](https://my.home-assistant.io/redirect/supervisor_addon/?addon=a889bffc_go2rtc&repository_url=https%3A%2F%2Fgithub.com%2FAlexxIT%2Fhassio-addons) @@ -103,9 +106,9 @@ Don't forget to fix the rights `chmod +x go2rtc_xxx_xxx` on Linux and Mac. - go2rtc > Install > Start 2. Setup [Integration](#module-hass) -### go2rtc: Docker +### go2rtc: Home Assistant Integration -Container [alexxit/go2rtc](https://hub.docker.com/r/alexxit/go2rtc) with support `amd64`, `386`, `arm64`, `arm`. This container is the same as [Home Assistant Add-on](#go2rtc-home-assistant-add-on), but can be used separately from Home Assistant. Container has preinstalled [FFmpeg](#source-ffmpeg), [Ngrok](#module-ngrok) and [Python](#source-echo). +[WebRTC Camera](https://github.com/AlexxIT/WebRTC) custom component can be used on any [Home Assistant installation](https://www.home-assistant.io/installation/), including [HassWP](https://github.com/AlexxIT/HassWP) on Windows. It can automatically download and use the latest version of go2rtc. Or it can connect to an existing version of go2rtc. Addon installation in this case is optional. ## Configuration @@ -124,6 +127,7 @@ Available modules: - [rtsp](#module-rtsp) - RTSP Server (important for FFmpeg support) - [webrtc](#module-webrtc) - WebRTC Server - [mp4](#module-mp4) - MSE, MP4 stream and MP4 shapshot Server +- [hls](#module-hls) - HLS TS or fMP4 stream Server - [mjpeg](#module-mjpeg) - MJPEG Server - [ffmpeg](#source-ffmpeg) - FFmpeg integration - [ngrok](#module-ngrok) - Ngrok integration (external access for private network) @@ -547,24 +551,29 @@ tunnels: ### Module: Hass -If you install **go2rtc** as [Hass Add-on](#go2rtc-home-assistant-add-on) - you need to use localhost IP-address. In other cases you need to use IP-address of server with **go2rtc** application. +The best and easiest way to use go2rtc inside the Home Assistant is to install the custom integration [WebRTC Camera](#go2rtc-home-assistant-integration) and custom lovelace card. -#### From go2rtc to Hass +But go2rtc is also compatible and can be used with [RTSPtoWebRTC](https://www.home-assistant.io/integrations/rtsp_to_webrtc/) built-in integration. -Add any supported [stream source](#module-streams) as [Generic Camera](https://www.home-assistant.io/integrations/generic/) and view stream with built-in [Stream](https://www.home-assistant.io/integrations/stream/) integration. Technology `HLS`, supported codecs: `H264`, poor latency. +You have several options on how to add a camera to Home Assistant: -1. Add your stream to [go2rtc config](#configuration) -2. 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` +1. Camera RTSP source => [Generic Camera](https://www.home-assistant.io/integrations/generic/) +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 > [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) -#### From Hass to go2rtc +You have several options on how to watch the stream from the cameras in Home Assistant: -View almost any Hass camera using `WebRTC` technology, supported codecs `H264`/`PCMU`/`PCMA`/`OPUS`, best latency. - -When the stream starts - the camera `entity_id` will be added to go2rtc "on the fly". You don't need to add cameras manually to [go2rtc config](#configuration). Some cameras (like [Nest](https://www.home-assistant.io/integrations/nest/)) have a dynamic link to the stream, it will be updated each time a stream is started from the Hass interface. - -1. Hass > Settings > Integrations > Add Integration > [RTSPtoWebRTC](https://my.home-assistant.io/redirect/config_flow_start/?domain=rtsp_to_webrtc) > `http://127.0.0.1:1984/` -2. RTSPtoWebRTC > Configure > STUN server: `stun.l.google.com:19302` -3. Use Picture Entity or Picture Glance lovelace card +1. `Camera Entity` => `Picture Entity Card` => Technology `HLS`, codecs: `H264/H265/AAC`, poor latency. +2. `Camera Entity` => [RTSPtoWebRTC](https://www.home-assistant.io/integrations/rtsp_to_webrtc/) => `Picture Entity Card` => Technology `WebRTC`, codecs: `H264/PCMU/PCMA/OPUS`, best latency. + - Install any [go2rtc](#fast-start) + - Hass > Settings > Integrations > Add Integration > [RTSPtoWebRTC](https://my.home-assistant.io/redirect/config_flow_start/?domain=rtsp_to_webrtc) > `http://127.0.0.1:1984/` + - RTSPtoWebRTC > Configure > STUN server: `stun.l.google.com:19302` + - Use Picture Entity or Picture Glance lovelace card +3. `Camera Entity` or `Camera URL` => [WebRTC Camera](https://github.com/AlexxIT/WebRTC) => Technology: `WebRTC/MSE/MP4/MJPEG`, codecs: `H264/H265/AAC/PCMU/PCMA/OPUS`, best latency, best compatibility. + - Install and add [WebRTC Camera](https://github.com/AlexxIT/WebRTC) custom integration + - Use WebRTC Camera custom lovelace card You can add camera `entity_id` to [go2rtc config](#configuration) if you need transcoding: @@ -769,17 +778,17 @@ streams: **go2rtc** is a new version of the server-side [WebRTC Camera](https://github.com/AlexxIT/WebRTC) integration, completely rewritten from scratch, with a number of fixes and a huge number of new features. It is compatible with native Home Assistant [RTSPtoWebRTC](https://www.home-assistant.io/integrations/rtsp_to_webrtc/) integration. So you [can use](#module-hass) default lovelace Picture Entity or Picture Glance. -**Q. Why go2rtc is an addon and not an integration?** +**Q. Should I use go2rtc addon or WebRTC Camera integration?** -Because **go2rtc** is more than just viewing your stream online with WebRTC. You can use it all the time for your various tasks. But every time the Hass is rebooted - all integrations are also rebooted. So your streams may be interrupted if you use them in additional tasks. +**go2rtc** is more than just viewing your stream online with WebRTC/MSE/HLS/etc. You can use it all the time for your various tasks. But every time the Hass is rebooted - all integrations are also rebooted. So your streams may be interrupted if you use them in additional tasks. -When **go2rtc** is released, the **WebRTC Camera** integration will be updated. And you can decide whether to use the integration or the addon. +Basic users can use **WebRTC Camera** integration. Advanced users can use go2rtc addon or Frigate 12+ addon. **Q. Which RTSP link should I use inside Hass?** You can use direct link to your cameras there (as you always do). **go2rtc** support zero-config feature. You may leave `streams` config section empty. And your streams will be created on the fly on first start from Hass. And your cameras will have multiple connections. Some from Hass directly and one from **go2rtc**. -Also you can specify your streams in **go2rtc** [config file](#configuration) and use RTSP links to this addon. With additional features: multi-source [codecs negotiation](#codecs-negotiation) or FFmpeg [transcoding](#source-ffmpeg) for unsupported codecs. Or use them as source for Frigate. And your cameras will have one connection from **go2rtc**. And **go2rtc** will have multiple connection - some from Hass via RTSP protocol, some from your browser via WebRTC protocol. +Also you can specify your streams in **go2rtc** [config file](#configuration) and use RTSP links to this addon. With additional features: multi-source [codecs negotiation](#codecs-negotiation) or FFmpeg [transcoding](#source-ffmpeg) for unsupported codecs. Or use them as source for Frigate. And your cameras will have one connection from **go2rtc**. And **go2rtc** will have multiple connection - some from Hass via RTSP protocol, some from your browser via WebRTC/MSE/HLS protocols. Use any config what you like. From b48f1c1a0b80f204d3c70147bc63b961b1cd7031 Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Wed, 1 Feb 2023 10:39:54 +0300 Subject: [PATCH 14/24] Update default_query param name in API response --- cmd/rtsp/rtsp.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/rtsp/rtsp.go b/cmd/rtsp/rtsp.go index e61809a8..67a80c39 100644 --- a/cmd/rtsp/rtsp.go +++ b/cmd/rtsp/rtsp.go @@ -19,7 +19,7 @@ func Init() { Listen string `yaml:"listen" json:"listen"` Username string `yaml:"username" json:"-"` Password string `yaml:"password" json:"-"` - DefaultQuery string `yaml:"default_query"` + DefaultQuery string `yaml:"default_query" json:"default_query"` } `yaml:"rtsp"` } From 08c2174e94715156cb0a60d56dc480bedb28927a Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Wed, 1 Feb 2023 10:40:29 +0300 Subject: [PATCH 15/24] Fix default_query bug #227 --- cmd/rtsp/rtsp.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cmd/rtsp/rtsp.go b/cmd/rtsp/rtsp.go index 67a80c39..1951d0e9 100644 --- a/cmd/rtsp/rtsp.go +++ b/cmd/rtsp/rtsp.go @@ -175,7 +175,9 @@ func tcpHandler(conn *rtsp.Conn) { conn.Medias = mp4.ParseQuery(conn.URL.Query()) if conn.Medias == nil { - conn.Medias = defaultMedias + for _, media := range defaultMedias { + conn.Medias = append(conn.Medias, media.Clone()) + } } if err := stream.AddConsumer(conn); err != nil { From 38ea8b56b85f74c5fef841b08c5be0ad5a007a10 Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Wed, 1 Feb 2023 17:57:00 +0300 Subject: [PATCH 16/24] Update version to 1.1.1 --- 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 15402a7e..b77aed61 100644 --- a/cmd/app/app.go +++ b/cmd/app/app.go @@ -14,7 +14,7 @@ import ( "time" ) -var Version = "1.1.0" +var Version = "1.1.1" var UserAgent = "go2rtc/" + Version var ConfigPath string From 5a2d7de56be87a682726c1a29132e71e20967829 Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Fri, 3 Feb 2023 11:38:26 +0300 Subject: [PATCH 17/24] Add projects using go2rtc section to readme --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index b166aa06..c91268d2 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,7 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg * [Codecs filters](#codecs-filters) * [Codecs madness](#codecs-madness) * [Codecs negotiation](#codecs-negotiation) +* [Projects using go2rtc](#projects-using-go2rtc) * [TIPS](#tips) * [FAQ](#faq) @@ -761,6 +762,12 @@ streams: **PS.** You can select `PCMU` or `PCMA` codec in camera setting and don't use transcoding at all. Or you can select `AAC` codec for main stream and `PCMU` codec for second stream and add both RTSP to YAML config, this also will work fine. +## Projects using go2rtc + +- [Frigate 12+](https://frigate.video/) - open source NVR built around real-time AI object detection +- [ring-mqtt](https://github.com/tsightler/ring-mqtt) - Ring devices to MQTT Bridge +- [EufyP2PStream](https://github.com/oischinger/eufyp2pstream) - A small project that provides a Video/Audio Stream from Eufy cameras that don't directly support RTSP + ## TIPS **Using apps for low RTSP delay** From 2a20251dbdd74c8871ccbebb0a2efb232118e085 Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Fri, 3 Feb 2023 11:40:52 +0300 Subject: [PATCH 18/24] Fix autoplay after background --- www/video-rtc.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/video-rtc.js b/www/video-rtc.js index 10eeead6..e7534b30 100644 --- a/www/video-rtc.js +++ b/www/video-rtc.js @@ -189,8 +189,8 @@ export class VideoRTC extends HTMLElement { const seek = this.video.seekable; if (seek.length > 0) { this.video.currentTime = seek.end(seek.length - 1); - this.play(); } + this.play(); } else { this.oninit(); } From 3240301f27a86b7a68b942caea7b6471884fa426 Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Fri, 3 Feb 2023 11:41:17 +0300 Subject: [PATCH 19/24] Fix autofullscreen with MP4 for iPhones --- www/video-rtc.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/www/video-rtc.js b/www/video-rtc.js index e7534b30..1becffdf 100644 --- a/www/video-rtc.js +++ b/www/video-rtc.js @@ -11,6 +11,7 @@ * - MediaSource for Safari iOS all * - Customized built-in elements (extends HTMLVideoElement) because all Safari * - Public class fields because old Safari (before 14.0) + * - Autoplay for Safari */ export class VideoRTC extends HTMLElement { constructor() { @@ -558,6 +559,7 @@ export class VideoRTC extends HTMLElement { /** @type {HTMLVideoElement} */ const video2 = document.createElement("video"); video2.autoplay = true; + video2.playsInline = true; video2.muted = true; video2.addEventListener("loadeddata", ev => { From 1153ee365276f4890beb4c37b153c5d4455c9bec Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Fri, 3 Feb 2023 11:39:53 +0300 Subject: [PATCH 20/24] Fix support WebRTC for Chromecast 1 --- www/README.md | 13 +++++++++++++ www/video-rtc.js | 5 ++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/www/README.md b/www/README.md index 79d93b45..cb34a782 100644 --- a/www/README.md +++ b/www/README.md @@ -48,6 +48,18 @@ pc.ontrack = ev => { } ``` +## Chromecast 1 + +2023-02-02. Error: + +``` +InvalidStateError: Failed to execute 'addTransceiver' on 'RTCPeerConnection': This operation is only supported in 'unified-plan'. 'unified-plan' will become the default behavior in the future, but it is currently experimental. To try it out, construct the RTCPeerConnection with sdpSemantics:'unified-plan' present in the RTCConfiguration argument. +``` + +User-Agent: `Mozilla/5.0 (X11; Linux armv7l) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.47 Safari/537.36 CrKey/1.36.159268` + +https://webrtc.org/getting-started/unified-plan-transition-guide?hl=en + ## Useful links - https://www.webrtc-experiment.com/DetectRTC/ @@ -58,3 +70,4 @@ pc.ontrack = ev => { - https://chromium.googlesource.com/external/w3c/web-platform-tests/+/refs/heads/master/media-source/mediasource-is-type-supported.html - https://googlechrome.github.io/samples/media/sourcebuffer-changetype.html - https://chromestatus.com/feature/5100845653819392 +- https://developer.apple.com/documentation/webkit/delivering_video_content_for_safari diff --git a/www/video-rtc.js b/www/video-rtc.js index 1becffdf..445aa94d 100644 --- a/www/video-rtc.js +++ b/www/video-rtc.js @@ -61,7 +61,10 @@ export class VideoRTC extends HTMLElement { * [config] WebRTC configuration * @type {RTCConfiguration} */ - this.pcConfig = {iceServers: [{urls: "stun:stun.l.google.com:19302"}]}; + this.pcConfig = { + iceServers: [{urls: 'stun:stun.l.google.com:19302'}], + sdpSemantics: 'unified-plan', // important for Chromecast 1 + }; /** * [info] WebSocket connection state. Values: CONNECTING, OPEN, CLOSED From 8cee4179f2c71bee7d214ca1235b7866b038b45b Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Fri, 3 Feb 2023 12:54:42 +0300 Subject: [PATCH 21/24] Fix another buggy Chinese cameras --- pkg/h264/rtp.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pkg/h264/rtp.go b/pkg/h264/rtp.go index 6044caae..024ccf74 100644 --- a/pkg/h264/rtp.go +++ b/pkg/h264/rtp.go @@ -10,6 +10,8 @@ import ( const RTPPacketVersionAVC = 0 +const PSMaxSize = 128 // the biggest SPS I've seen is 48 (EZVIZ CS-CV210) + func RTPDepay(track *streamer.Track) streamer.WrapperFunc { depack := &codecs.H264Packet{IsAVC: true} @@ -29,7 +31,7 @@ func RTPDepay(track *streamer.Track) streamer.WrapperFunc { // Fix TP-Link Tapo TC70: sends SPS and PPS with packet.Marker = true // Reolink Duo 2: sends SPS with Marker and PPS without - if packet.Marker && len(payload) < 128 { + if packet.Marker && len(payload) < PSMaxSize { switch NALUType(payload) { case NALUTypeSPS, NALUTypePPS: buf = append(buf, payload...) @@ -70,7 +72,10 @@ func RTPDepay(track *streamer.Track) streamer.WrapperFunc { if len(buf) > 0 { payload = append(buf, payload...) buf = buf[:0] - } else { + } + + // should not be that huge SPS + if NALUType(payload) == NALUTypeSPS && binary.BigEndian.Uint32(payload) >= PSMaxSize { // some Chinese buggy cameras has single packet with SPS+PPS+IFrame separated by 00 00 00 01 // https://github.com/AlexxIT/WebRTC/issues/391 // https://github.com/AlexxIT/WebRTC/issues/392 From d21ce3d27d3300ecd5e000130f00958c7abf36ae Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Fri, 3 Feb 2023 12:10:54 +0300 Subject: [PATCH 22/24] Jump over wrong packets from RTSP --- cmd/rtsp/rtsp.go | 2 ++ pkg/rtsp/conn.go | 65 +++++++++++++++++++++++++++++++++++++----------- 2 files changed, 53 insertions(+), 14 deletions(-) diff --git a/cmd/rtsp/rtsp.go b/cmd/rtsp/rtsp.go index 1951d0e9..ce9a723b 100644 --- a/cmd/rtsp/rtsp.go +++ b/cmd/rtsp/rtsp.go @@ -112,6 +112,8 @@ func rtspHandler(url string) (streamer.Producer, error) { log.Trace().Msgf("[rtsp] client request:\n%s", msg) case *tcp.Response: log.Trace().Msgf("[rtsp] client response:\n%s", msg) + case string: + log.Trace().Msgf("[rtsp] client msg: %s", msg) } }) } diff --git a/pkg/rtsp/conn.go b/pkg/rtsp/conn.go index 39523ca6..bc9fe734 100644 --- a/pkg/rtsp/conn.go +++ b/pkg/rtsp/conn.go @@ -743,6 +743,9 @@ func (c *Conn) Handle() (err error) { return } + var channelID byte + var size uint16 + if buf4[0] != '$' { switch string(buf4) { case "RTSP": @@ -751,26 +754,62 @@ func (c *Conn) Handle() (err error) { return } c.Fire(res) + continue + case "OPTI", "TEAR", "DESC", "SETU", "PLAY", "PAUS", "RECO", "ANNO", "GET_", "SET_": var req *tcp.Request if req, err = tcp.ReadRequest(c.reader); err != nil { return } c.Fire(req) + continue + default: - return fmt.Errorf("RTSP wrong input") + for i := 0; ; i++ { + // search next start symbol + if _, err = c.reader.ReadBytes('$'); err != nil { + return err + } + + if channelID, err = c.reader.ReadByte(); err != nil { + return err + } + + // check if channel ID exists + if c.channels[channelID] == nil { + continue + } + + buf4 = make([]byte, 2) + if _, err = io.ReadFull(c.reader, buf4); err != nil { + return err + } + + // check if size good for RTP + size = binary.BigEndian.Uint16(buf4) + if size <= 1500 { + break + } + + // 10 tries to find good packet + if i >= 10 { + return fmt.Errorf("RTSP wrong input") + } + } + + c.Fire("RTSP wrong input") } - continue - } + } else { + // hope that the odd channels are always RTCP + channelID = buf4[1] - // hope that the odd channels are always RTCP - channelID := buf4[1] + // get data size + size = binary.BigEndian.Uint16(buf4[2:]) - // get data size - size := int(binary.BigEndian.Uint16(buf4[2:])) - - if _, err = c.reader.Discard(4); err != nil { - return + // skip 4 bytes from c.reader.Peek + if _, err = c.reader.Discard(4); err != nil { + return + } } // init memory for data @@ -779,7 +818,7 @@ func (c *Conn) Handle() (err error) { return } - c.receive += size + c.receive += int(size) if channelID&1 == 0 { packet := &rtp.Packet{} @@ -790,10 +829,8 @@ func (c *Conn) Handle() (err error) { track := c.channels[channelID] if track != nil { _ = track.WriteRTP(packet) - //return fmt.Errorf("wrong channelID: %d", channelID) } else { - continue // TODO: maybe fix this - //panic("wrong channelID") + c.Fire("wrong channelID: " + strconv.Itoa(int(channelID))) } } else { msg := &RTCP{Channel: channelID} From da3137b6f08697cb7d5db0a3ee433148734996c4 Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Fri, 3 Feb 2023 14:11:30 +0300 Subject: [PATCH 23/24] Add User-Agent to RTSP Describe #235 --- pkg/rtsp/conn.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pkg/rtsp/conn.go b/pkg/rtsp/conn.go index bc9fe734..40c07fd2 100644 --- a/pkg/rtsp/conn.go +++ b/pkg/rtsp/conn.go @@ -304,6 +304,12 @@ func (c *Conn) Describe() error { req.Header.Set("Require", "www.onvif.org/ver20/backchannel") } + if c.UserAgent != "" { + // this camera will answer with 401 on DESCRIBE without User-Agent + // https://github.com/AlexxIT/go2rtc/issues/235 + req.Header.Set("User-Agent", c.UserAgent) + } + res, err := c.Do(req) if err != nil { return err From 6d1c0a2459864376d137ab76bce855e874110904 Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Sat, 4 Feb 2023 10:00:53 +0300 Subject: [PATCH 24/24] Fix SDP parsing from cheap Chinese cameras --- pkg/rtsp/helpers.go | 8 ++++++-- pkg/rtsp/rtsp_test.go | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/pkg/rtsp/helpers.go b/pkg/rtsp/helpers.go index f748c9a2..893f3b87 100644 --- a/pkg/rtsp/helpers.go +++ b/pkg/rtsp/helpers.go @@ -5,6 +5,7 @@ import ( "github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/pion/rtcp" "net/url" + "regexp" "strings" ) @@ -22,9 +23,12 @@ t=0 0` func UnmarshalSDP(rawSDP []byte) ([]*streamer.Media, error) { medias, err := streamer.UnmarshalSDP(rawSDP) if err != nil { + // fix multiple `s=` https://github.com/AlexxIT/WebRTC/issues/417 + re, _ := regexp.Compile("\ns=[^\n]+") + rawSDP = re.ReplaceAll(rawSDP, nil) + // fix SDP header for some cameras - i := bytes.Index(rawSDP, []byte("\nm=")) - if i > 0 { + if i := bytes.Index(rawSDP, []byte("\nm=")); i > 0 { rawSDP = append([]byte(sdpHeader), rawSDP[i:]...) medias, err = streamer.UnmarshalSDP(rawSDP) } diff --git a/pkg/rtsp/rtsp_test.go b/pkg/rtsp/rtsp_test.go index 7e32b272..e7024f4e 100644 --- a/pkg/rtsp/rtsp_test.go +++ b/pkg/rtsp/rtsp_test.go @@ -18,3 +18,35 @@ func TestURLParse(t *testing.T) { assert.Empty(t, err) assert.Equal(t, "turret2-cam.lan:554", u.Host) } + +func TestMultipleSinSDP(t *testing.T) { + s := `v=0 +o=- 91674849066 1 IN IP4 192.168.1.123 +s=RtspServer +i=live +t=0 0 +a=control:* +a=range:npt=0- +m=video 0 RTP/AVP 96 +c=IN IP4 0.0.0.0 +s=RtspServer +i=live +a=control:track0 +a=range:npt=0- +a=rtpmap:96 H264/90000 +a=fmtp:96 packetization-mode=1;profile-level-id=42001E;sprop-parameter-sets=Z0IAHvQCgC3I,aM48gA== +a=control:track0 +m=audio 0 RTP/AVP 97 +c=IN IP4 0.0.0.0 +s=RtspServer +i=live +a=control:track1 +a=range:npt=0- +a=rtpmap:97 MPEG4-GENERIC/8000/1 +a=fmtp:97 profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=1588 +a=control:track1 +` + medias, err := UnmarshalSDP([]byte(s)) + assert.Nil(t, err) + assert.NotNil(t, medias) +}