From da5420a788d16c40278d36fce46deb6d42fc4db5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 17 Jul 2024 21:29:04 +0200 Subject: [PATCH 01/88] build(deps): bump github.com/datarhei/gosrt (#3559) Bumps [github.com/datarhei/gosrt](https://github.com/datarhei/gosrt) from 0.6.1-0.20240708145230-390712a1b3f7 to 0.7.0. - [Commits](https://github.com/datarhei/gosrt/commits/v0.7.0) --- updated-dependencies: - dependency-name: github.com/datarhei/gosrt dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 92d94fe84eb..1029528d05d 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/bluenviron/gohlslib v1.4.0 github.com/bluenviron/gortsplib/v4 v4.10.2 github.com/bluenviron/mediacommon v1.12.1 - github.com/datarhei/gosrt v0.6.1-0.20240708145230-390712a1b3f7 + github.com/datarhei/gosrt v0.7.0 github.com/fsnotify/fsnotify v1.7.0 github.com/gin-gonic/gin v1.10.0 github.com/golang-jwt/jwt/v5 v5.2.1 diff --git a/go.sum b/go.sum index 6d9a22d0fb7..42f279778b2 100644 --- a/go.sum +++ b/go.sum @@ -37,8 +37,8 @@ github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJ github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= -github.com/datarhei/gosrt v0.6.1-0.20240708145230-390712a1b3f7 h1:Tyvgum9NHQi/iDoYUQhuxjUnu/s4tJXNdYCeUZma5Z0= -github.com/datarhei/gosrt v0.6.1-0.20240708145230-390712a1b3f7/go.mod h1:wTDoyog1z4au8Fd/QJBQAndzvccuxjqUL/qMm0EyJxE= +github.com/datarhei/gosrt v0.7.0 h1:1/IY66HVVgqGA9zkmL5l6jUFuI8t/76WkuamSkJqHqs= +github.com/datarhei/gosrt v0.7.0/go.mod h1:wTDoyog1z4au8Fd/QJBQAndzvccuxjqUL/qMm0EyJxE= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= From 0230aa0a9caa1434f1b2b81e0d25b4f63132570f Mon Sep 17 00:00:00 2001 From: Alessandro Ros Date: Thu, 1 Aug 2024 16:34:40 +0200 Subject: [PATCH 02/88] record, hls: fix panic with MPEG-4 audio tracks without config (#3590) (#3594) --- go.mod | 2 +- go.sum | 4 +- internal/protocols/mpegts/from_stream.go | 7 +- internal/record/agent_instance.go | 63 +++++++++-------- internal/record/format_fmp4.go | 87 +++++++++++++----------- internal/record/format_fmp4_part.go | 6 +- internal/record/format_fmp4_segment.go | 6 +- internal/record/format_fmp4_track.go | 2 +- internal/record/format_mpegts.go | 67 +++++++++--------- internal/record/format_mpegts_segment.go | 10 +-- internal/servers/hls/muxer_instance.go | 41 ++++++----- 11 files changed, 160 insertions(+), 135 deletions(-) diff --git a/go.mod b/go.mod index 1029528d05d..004a11130e8 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/abema/go-mp4 v1.2.0 github.com/alecthomas/kong v0.9.0 github.com/bluenviron/gohlslib v1.4.0 - github.com/bluenviron/gortsplib/v4 v4.10.2 + github.com/bluenviron/gortsplib/v4 v4.10.3-0.20240801095652-e2d1e6dab418 github.com/bluenviron/mediacommon v1.12.1 github.com/datarhei/gosrt v0.7.0 github.com/fsnotify/fsnotify v1.7.0 diff --git a/go.sum b/go.sum index 42f279778b2..c547eec5588 100644 --- a/go.sum +++ b/go.sum @@ -22,8 +22,8 @@ github.com/benburkert/openpgp v0.0.0-20160410205803-c2471f86866c h1:8XZeJrs4+ZYh github.com/benburkert/openpgp v0.0.0-20160410205803-c2471f86866c/go.mod h1:x1vxHcL/9AVzuk5HOloOEPrtJY0MaalYr78afXZ+pWI= github.com/bluenviron/gohlslib v1.4.0 h1:3a9W1x8eqlxJUKt1sJCunPGtti5ALIY2ik4GU0RVe7E= github.com/bluenviron/gohlslib v1.4.0/go.mod h1:q5ZElzNw5GRbV1VEI45qkcPbKBco6BP58QEY5HyFsmo= -github.com/bluenviron/gortsplib/v4 v4.10.2 h1:O7HPRG8Pv4zUbyYD0HYH4Ufu1Hg9FJGTlizx6a09hL0= -github.com/bluenviron/gortsplib/v4 v4.10.2/go.mod h1:re/L/vYh2wLPElQNAYah+bRFHJs0aRkM1MLX3WJ3N6M= +github.com/bluenviron/gortsplib/v4 v4.10.3-0.20240801095652-e2d1e6dab418 h1:j+qo+yB1S1KiTOKSN4ABPbJ9rNdzoT6AoH9F/+k1QjE= +github.com/bluenviron/gortsplib/v4 v4.10.3-0.20240801095652-e2d1e6dab418/go.mod h1:FE/oUV476F/paP+D2YAV2LvXDgpZef4lN5uAKAuVAsQ= github.com/bluenviron/mediacommon v1.12.1 h1:sgDJaKV6OXrPCSO0KPp9zi/pwNWtKHenn5/dvjtY+Tg= github.com/bluenviron/mediacommon v1.12.1/go.mod h1:HDyW2CzjvhYJXtdxstdFPio3G0qSocPhqkhUt/qffec= github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= diff --git a/internal/protocols/mpegts/from_stream.go b/internal/protocols/mpegts/from_stream.go index 47abf82aafc..a5680050193 100644 --- a/internal/protocols/mpegts/from_stream.go +++ b/internal/protocols/mpegts/from_stream.go @@ -183,8 +183,13 @@ func FromStream( }) case *format.MPEG4Audio: + co := forma.GetConfig() + if co == nil { + return fmt.Errorf("MPEG-4 audio tracks without explicit configuration are not supported") + } + track := addTrack(&mcmpegts.CodecMPEG4Audio{ - Config: *forma.GetConfig(), + Config: *co, }) stream.AddReader(writer, medi, forma, func(u unit.Unit) error { diff --git a/internal/record/agent_instance.go b/internal/record/agent_instance.go index bce6e3d1416..37af84030d6 100644 --- a/internal/record/agent_instance.go +++ b/internal/record/agent_instance.go @@ -28,55 +28,60 @@ type agentInstance struct { done chan struct{} } -func (a *agentInstance) initialize() { - a.pathFormat = a.agent.PathFormat +// Log implements logger.Writer. +func (ai *agentInstance) Log(level logger.Level, format string, args ...interface{}) { + ai.agent.Log(level, format, args...) +} + +func (ai *agentInstance) initialize() { + ai.pathFormat = ai.agent.PathFormat - a.pathFormat = PathAddExtension( - strings.ReplaceAll(a.pathFormat, "%path", a.agent.PathName), - a.agent.Format, + ai.pathFormat = PathAddExtension( + strings.ReplaceAll(ai.pathFormat, "%path", ai.agent.PathName), + ai.agent.Format, ) - a.terminate = make(chan struct{}) - a.done = make(chan struct{}) + ai.terminate = make(chan struct{}) + ai.done = make(chan struct{}) - a.writer = asyncwriter.New(a.agent.WriteQueueSize, a.agent) + ai.writer = asyncwriter.New(ai.agent.WriteQueueSize, ai.agent) - switch a.agent.Format { + switch ai.agent.Format { case conf.RecordFormatMPEGTS: - a.format = &formatMPEGTS{ - a: a, + ai.format = &formatMPEGTS{ + ai: ai, } - a.format.initialize() + ai.format.initialize() default: - a.format = &formatFMP4{ - a: a, + ai.format = &formatFMP4{ + ai: ai, } - a.format.initialize() + ai.format.initialize() } - go a.run() + go ai.run() } -func (a *agentInstance) close() { - close(a.terminate) - <-a.done +func (ai *agentInstance) close() { + close(ai.terminate) + <-ai.done } -func (a *agentInstance) run() { - defer close(a.done) +func (ai *agentInstance) run() { + defer close(ai.done) - a.writer.Start() + ai.writer.Start() select { - case err := <-a.writer.Error(): - a.agent.Log(logger.Error, err.Error()) - a.agent.Stream.RemoveReader(a.writer) + case err := <-ai.writer.Error(): + ai.Log(logger.Error, err.Error()) + ai.agent.Stream.RemoveReader(ai.writer) - case <-a.terminate: - a.agent.Stream.RemoveReader(a.writer) - a.writer.Stop() + case <-ai.terminate: + ai.agent.Stream.RemoveReader(ai.writer) + ai.writer.Stop() } - a.format.close() + ai.format.close() } diff --git a/internal/record/format_fmp4.go b/internal/record/format_fmp4.go index 5d67b06560f..6041b34f874 100644 --- a/internal/record/format_fmp4.go +++ b/internal/record/format_fmp4.go @@ -100,7 +100,7 @@ func jpegExtractSize(image []byte) (int, int, error) { } type formatFMP4 struct { - a *agentInstance + ai *agentInstance tracks []*formatFMP4Track hasVideo bool @@ -140,7 +140,7 @@ func (f *formatFMP4) initialize() { } } - for _, media := range f.a.agent.Stream.Desc().Medias { + for _, media := range f.ai.agent.Stream.Desc().Medias { for _, forma := range media.Formats { switch forma := forma.(type) { case *rtspformat.AV1: @@ -153,7 +153,7 @@ func (f *formatFMP4) initialize() { firstReceived := false - f.a.agent.Stream.AddReader(f.a.writer, media, forma, func(u unit.Unit) error { + f.ai.agent.Stream.AddReader(f.ai.writer, media, forma, func(u unit.Unit) error { tunit := u.(*unit.AV1) if tunit.TU == nil { return nil @@ -211,7 +211,7 @@ func (f *formatFMP4) initialize() { firstReceived := false - f.a.agent.Stream.AddReader(f.a.writer, media, forma, func(u unit.Unit) error { + f.ai.agent.Stream.AddReader(f.ai.writer, media, forma, func(u unit.Unit) error { tunit := u.(*unit.VP9) if tunit.Frame == nil { return nil @@ -309,7 +309,7 @@ func (f *formatFMP4) initialize() { var dtsExtractor *h265.DTSExtractor - f.a.agent.Stream.AddReader(f.a.writer, media, forma, func(u unit.Unit) error { + f.ai.agent.Stream.AddReader(f.ai.writer, media, forma, func(u unit.Unit) error { tunit := u.(*unit.H265) if tunit.AU == nil { return nil @@ -387,7 +387,7 @@ func (f *formatFMP4) initialize() { var dtsExtractor *h264.DTSExtractor - f.a.agent.Stream.AddReader(f.a.writer, media, forma, func(u unit.Unit) error { + f.ai.agent.Stream.AddReader(f.ai.writer, media, forma, func(u unit.Unit) error { tunit := u.(*unit.H264) if tunit.AU == nil { return nil @@ -464,7 +464,7 @@ func (f *formatFMP4) initialize() { firstReceived := false var lastPTS time.Duration - f.a.agent.Stream.AddReader(f.a.writer, media, forma, func(u unit.Unit) error { + f.ai.agent.Stream.AddReader(f.ai.writer, media, forma, func(u unit.Unit) error { tunit := u.(*unit.MPEG4Video) if tunit.Frame == nil { return nil @@ -517,7 +517,7 @@ func (f *formatFMP4) initialize() { firstReceived := false var lastPTS time.Duration - f.a.agent.Stream.AddReader(f.a.writer, media, forma, func(u unit.Unit) error { + f.ai.agent.Stream.AddReader(f.ai.writer, media, forma, func(u unit.Unit) error { tunit := u.(*unit.MPEG1Video) if tunit.Frame == nil { return nil @@ -566,7 +566,7 @@ func (f *formatFMP4) initialize() { parsed := false - f.a.agent.Stream.AddReader(f.a.writer, media, forma, func(u unit.Unit) error { + f.ai.agent.Stream.AddReader(f.ai.writer, media, forma, func(u unit.Unit) error { tunit := u.(*unit.MJPEG) if tunit.Frame == nil { return nil @@ -598,7 +598,7 @@ func (f *formatFMP4) initialize() { } track := addTrack(forma, codec) - f.a.agent.Stream.AddReader(f.a.writer, media, forma, func(u unit.Unit) error { + f.ai.agent.Stream.AddReader(f.ai.writer, media, forma, func(u unit.Unit) error { tunit := u.(*unit.Opus) if tunit.Packets == nil { return nil @@ -625,37 +625,42 @@ func (f *formatFMP4) initialize() { }) case *rtspformat.MPEG4Audio: - codec := &fmp4.CodecMPEG4Audio{ - Config: *forma.GetConfig(), - } - track := addTrack(forma, codec) - - sampleRate := time.Duration(forma.ClockRate()) - - f.a.agent.Stream.AddReader(f.a.writer, media, forma, func(u unit.Unit) error { - tunit := u.(*unit.MPEG4Audio) - if tunit.AUs == nil { - return nil + co := forma.GetConfig() + if co == nil { + f.ai.Log(logger.Warn, "skipping MPEG-4 audio track: tracks without explicit configuration are not supported") + } else { + codec := &fmp4.CodecMPEG4Audio{ + Config: *co, } + track := addTrack(forma, codec) - for i, au := range tunit.AUs { - dt := time.Duration(i) * mpeg4audio.SamplesPerAccessUnit * - time.Second / sampleRate + sampleRate := time.Duration(forma.ClockRate()) - err := track.write(&sample{ - PartSample: &fmp4.PartSample{ - Payload: au, - }, - dts: tunit.PTS + dt, - ntp: tunit.NTP.Add(dt), - }) - if err != nil { - return err + f.ai.agent.Stream.AddReader(f.ai.writer, media, forma, func(u unit.Unit) error { + tunit := u.(*unit.MPEG4Audio) + if tunit.AUs == nil { + return nil } - } - return nil - }) + for i, au := range tunit.AUs { + dt := time.Duration(i) * mpeg4audio.SamplesPerAccessUnit * + time.Second / sampleRate + + err := track.write(&sample{ + PartSample: &fmp4.PartSample{ + Payload: au, + }, + dts: tunit.PTS + dt, + ntp: tunit.NTP.Add(dt), + }) + if err != nil { + return err + } + } + + return nil + }) + } case *rtspformat.MPEG1Audio: codec := &fmp4.CodecMPEG1Audio{ @@ -666,7 +671,7 @@ func (f *formatFMP4) initialize() { parsed := false - f.a.agent.Stream.AddReader(f.a.writer, media, forma, func(u unit.Unit) error { + f.ai.agent.Stream.AddReader(f.ai.writer, media, forma, func(u unit.Unit) error { tunit := u.(*unit.MPEG1Audio) if tunit.Frames == nil { return nil @@ -721,7 +726,7 @@ func (f *formatFMP4) initialize() { parsed := false - f.a.agent.Stream.AddReader(f.a.writer, media, forma, func(u unit.Unit) error { + f.ai.agent.Stream.AddReader(f.ai.writer, media, forma, func(u unit.Unit) error { tunit := u.(*unit.AC3) if tunit.Frames == nil { return nil @@ -783,7 +788,7 @@ func (f *formatFMP4) initialize() { } track := addTrack(forma, codec) - f.a.agent.Stream.AddReader(f.a.writer, media, forma, func(u unit.Unit) error { + f.ai.agent.Stream.AddReader(f.ai.writer, media, forma, func(u unit.Unit) error { tunit := u.(*unit.G711) if tunit.Samples == nil { return nil @@ -814,7 +819,7 @@ func (f *formatFMP4) initialize() { } track := addTrack(forma, codec) - f.a.agent.Stream.AddReader(f.a.writer, media, forma, func(u unit.Unit) error { + f.ai.agent.Stream.AddReader(f.ai.writer, media, forma, func(u unit.Unit) error { tunit := u.(*unit.LPCM) if tunit.Samples == nil { return nil @@ -832,7 +837,7 @@ func (f *formatFMP4) initialize() { } } - f.a.agent.Log(logger.Info, "recording %s", + f.ai.Log(logger.Info, "recording %s", defs.FormatsInfo(formats)) } diff --git a/internal/record/format_fmp4_part.go b/internal/record/format_fmp4_part.go index a960c9a72ba..c8871d3d56f 100644 --- a/internal/record/format_fmp4_part.go +++ b/internal/record/format_fmp4_part.go @@ -54,8 +54,8 @@ func (p *formatFMP4Part) initialize() { func (p *formatFMP4Part) close() error { if p.s.fi == nil { - p.s.path = Path{Start: p.s.startNTP}.Encode(p.s.f.a.pathFormat) - p.s.f.a.agent.Log(logger.Debug, "creating segment %s", p.s.path) + p.s.path = Path{Start: p.s.startNTP}.Encode(p.s.f.ai.pathFormat) + p.s.f.ai.Log(logger.Debug, "creating segment %s", p.s.path) err := os.MkdirAll(filepath.Dir(p.s.path), 0o755) if err != nil { @@ -67,7 +67,7 @@ func (p *formatFMP4Part) close() error { return err } - p.s.f.a.agent.OnSegmentCreate(p.s.path) + p.s.f.ai.agent.OnSegmentCreate(p.s.path) err = writeInit(fi, p.s.f.tracks) if err != nil { diff --git a/internal/record/format_fmp4_segment.go b/internal/record/format_fmp4_segment.go index 7920fcb3162..476e518373b 100644 --- a/internal/record/format_fmp4_segment.go +++ b/internal/record/format_fmp4_segment.go @@ -54,7 +54,7 @@ func (s *formatFMP4Segment) close() error { } if s.fi != nil { - s.f.a.agent.Log(logger.Debug, "closing segment %s", s.path) + s.f.ai.Log(logger.Debug, "closing segment %s", s.path) err2 := s.fi.Close() if err == nil { err = err2 @@ -62,7 +62,7 @@ func (s *formatFMP4Segment) close() error { if err2 == nil { duration := s.lastDTS - s.startDTS - s.f.a.agent.OnSegmentComplete(s.path, duration) + s.f.ai.agent.OnSegmentComplete(s.path, duration) } } @@ -80,7 +80,7 @@ func (s *formatFMP4Segment) write(track *formatFMP4Track, sample *sample) error } s.curPart.initialize() s.f.nextSequenceNumber++ - } else if s.curPart.duration() >= s.f.a.agent.PartDuration { + } else if s.curPart.duration() >= s.f.ai.agent.PartDuration { err := s.curPart.close() s.curPart = nil diff --git a/internal/record/format_fmp4_track.go b/internal/record/format_fmp4_track.go index fd7a075ee4b..7107fa75070 100644 --- a/internal/record/format_fmp4_track.go +++ b/internal/record/format_fmp4_track.go @@ -42,7 +42,7 @@ func (t *formatFMP4Track) write(sample *sample) error { if (!t.f.hasVideo || t.initTrack.Codec.IsVideo()) && !t.nextSample.IsNonSyncSample && - (t.nextSample.dts-t.f.currentSegment.startDTS) >= t.f.a.agent.SegmentDuration { + (t.nextSample.dts-t.f.currentSegment.startDTS) >= t.f.ai.agent.SegmentDuration { t.f.currentSegment.lastDTS = t.nextSample.dts err := t.f.currentSegment.close() if err != nil { diff --git a/internal/record/format_mpegts.go b/internal/record/format_mpegts.go index 381cb839370..ced04950f7a 100644 --- a/internal/record/format_mpegts.go +++ b/internal/record/format_mpegts.go @@ -40,7 +40,7 @@ func (d *dynamicWriter) setTarget(w io.Writer) { } type formatMPEGTS struct { - a *agentInstance + ai *agentInstance dw *dynamicWriter bw *bufio.Writer @@ -63,7 +63,7 @@ func (f *formatMPEGTS) initialize() { return track } - for _, media := range f.a.agent.Stream.Desc().Medias { + for _, media := range f.ai.agent.Stream.Desc().Medias { for _, forma := range media.Formats { switch forma := forma.(type) { case *rtspformat.H265: //nolint:dupl @@ -71,7 +71,7 @@ func (f *formatMPEGTS) initialize() { var dtsExtractor *h265.DTSExtractor - f.a.agent.Stream.AddReader(f.a.writer, media, forma, func(u unit.Unit) error { + f.ai.agent.Stream.AddReader(f.ai.writer, media, forma, func(u unit.Unit) error { tunit := u.(*unit.H265) if tunit.AU == nil { return nil @@ -107,7 +107,7 @@ func (f *formatMPEGTS) initialize() { var dtsExtractor *h264.DTSExtractor - f.a.agent.Stream.AddReader(f.a.writer, media, forma, func(u unit.Unit) error { + f.ai.agent.Stream.AddReader(f.ai.writer, media, forma, func(u unit.Unit) error { tunit := u.(*unit.H264) if tunit.AU == nil { return nil @@ -144,7 +144,7 @@ func (f *formatMPEGTS) initialize() { firstReceived := false var lastPTS time.Duration - f.a.agent.Stream.AddReader(f.a.writer, media, forma, func(u unit.Unit) error { + f.ai.agent.Stream.AddReader(f.ai.writer, media, forma, func(u unit.Unit) error { tunit := u.(*unit.MPEG4Video) if tunit.Frame == nil { return nil @@ -176,7 +176,7 @@ func (f *formatMPEGTS) initialize() { firstReceived := false var lastPTS time.Duration - f.a.agent.Stream.AddReader(f.a.writer, media, forma, func(u unit.Unit) error { + f.ai.agent.Stream.AddReader(f.ai.writer, media, forma, func(u unit.Unit) error { tunit := u.(*unit.MPEG1Video) if tunit.Frame == nil { return nil @@ -207,7 +207,7 @@ func (f *formatMPEGTS) initialize() { ChannelCount: forma.ChannelCount, }) - f.a.agent.Stream.AddReader(f.a.writer, media, forma, func(u unit.Unit) error { + f.ai.agent.Stream.AddReader(f.ai.writer, media, forma, func(u unit.Unit) error { tunit := u.(*unit.Opus) if tunit.Packets == nil { return nil @@ -225,31 +225,36 @@ func (f *formatMPEGTS) initialize() { }) case *rtspformat.MPEG4Audio: - track := addTrack(forma, &mpegts.CodecMPEG4Audio{ - Config: *forma.GetConfig(), - }) - - f.a.agent.Stream.AddReader(f.a.writer, media, forma, func(u unit.Unit) error { - tunit := u.(*unit.MPEG4Audio) - if tunit.AUs == nil { - return nil - } + co := forma.GetConfig() + if co == nil { + f.ai.Log(logger.Warn, "skipping MPEG-4 audio track: tracks without explicit configuration are not supported") + } else { + track := addTrack(forma, &mpegts.CodecMPEG4Audio{ + Config: *co, + }) + + f.ai.agent.Stream.AddReader(f.ai.writer, media, forma, func(u unit.Unit) error { + tunit := u.(*unit.MPEG4Audio) + if tunit.AUs == nil { + return nil + } - return f.write( - tunit.PTS, - tunit.NTP, - false, - true, - func() error { - return f.mw.WriteMPEG4Audio(track, durationGoToMPEGTS(tunit.PTS), tunit.AUs) - }, - ) - }) + return f.write( + tunit.PTS, + tunit.NTP, + false, + true, + func() error { + return f.mw.WriteMPEG4Audio(track, durationGoToMPEGTS(tunit.PTS), tunit.AUs) + }, + ) + }) + } case *rtspformat.MPEG1Audio: track := addTrack(forma, &mpegts.CodecMPEG1Audio{}) - f.a.agent.Stream.AddReader(f.a.writer, media, forma, func(u unit.Unit) error { + f.ai.agent.Stream.AddReader(f.ai.writer, media, forma, func(u unit.Unit) error { tunit := u.(*unit.MPEG1Audio) if tunit.Frames == nil { return nil @@ -271,7 +276,7 @@ func (f *formatMPEGTS) initialize() { sampleRate := time.Duration(forma.SampleRate) - f.a.agent.Stream.AddReader(f.a.writer, media, forma, func(u unit.Unit) error { + f.ai.agent.Stream.AddReader(f.ai.writer, media, forma, func(u unit.Unit) error { tunit := u.(*unit.AC3) if tunit.Frames == nil { return nil @@ -305,7 +310,7 @@ func (f *formatMPEGTS) initialize() { f.bw = bufio.NewWriterSize(f.dw, mpegtsMaxBufferSize) f.mw = mpegts.NewWriter(f.bw, tracks) - f.a.agent.Log(logger.Info, "recording %s", + f.ai.Log(logger.Info, "recording %s", defs.FormatsInfo(formats)) } @@ -336,7 +341,7 @@ func (f *formatMPEGTS) write( f.currentSegment.initialize() case (!f.hasVideo || isVideo) && randomAccess && - (dts-f.currentSegment.startDTS) >= f.a.agent.SegmentDuration: + (dts-f.currentSegment.startDTS) >= f.ai.agent.SegmentDuration: f.currentSegment.lastDTS = dts err := f.currentSegment.close() if err != nil { @@ -350,7 +355,7 @@ func (f *formatMPEGTS) write( } f.currentSegment.initialize() - case (dts - f.currentSegment.lastFlush) >= f.a.agent.PartDuration: + case (dts - f.currentSegment.lastFlush) >= f.ai.agent.PartDuration: err := f.bw.Flush() if err != nil { return err diff --git a/internal/record/format_mpegts_segment.go b/internal/record/format_mpegts_segment.go index 754c93aa7be..cf3dc7050e6 100644 --- a/internal/record/format_mpegts_segment.go +++ b/internal/record/format_mpegts_segment.go @@ -29,7 +29,7 @@ func (s *formatMPEGTSSegment) close() error { err := s.f.bw.Flush() if s.fi != nil { - s.f.a.agent.Log(logger.Debug, "closing segment %s", s.path) + s.f.ai.Log(logger.Debug, "closing segment %s", s.path) err2 := s.fi.Close() if err == nil { err = err2 @@ -37,7 +37,7 @@ func (s *formatMPEGTSSegment) close() error { if err2 == nil { duration := s.lastDTS - s.startDTS - s.f.a.agent.OnSegmentComplete(s.path, duration) + s.f.ai.agent.OnSegmentComplete(s.path, duration) } } @@ -46,8 +46,8 @@ func (s *formatMPEGTSSegment) close() error { func (s *formatMPEGTSSegment) Write(p []byte) (int, error) { if s.fi == nil { - s.path = Path{Start: s.startNTP}.Encode(s.f.a.pathFormat) - s.f.a.agent.Log(logger.Debug, "creating segment %s", s.path) + s.path = Path{Start: s.startNTP}.Encode(s.f.ai.pathFormat) + s.f.ai.Log(logger.Debug, "creating segment %s", s.path) err := os.MkdirAll(filepath.Dir(s.path), 0o755) if err != nil { @@ -59,7 +59,7 @@ func (s *formatMPEGTSSegment) Write(p []byte) (int, error) { return 0, err } - s.f.a.agent.OnSegmentCreate(s.path) + s.f.ai.agent.OnSegmentCreate(s.path) s.fi = fi } diff --git a/internal/servers/hls/muxer_instance.go b/internal/servers/hls/muxer_instance.go index c1d87c7f3f6..94f4d184dcb 100644 --- a/internal/servers/hls/muxer_instance.go +++ b/internal/servers/hls/muxer_instance.go @@ -236,28 +236,33 @@ func (mi *muxerInstance) createAudioTrack() *gohlslib.Track { audioMedia = mi.stream.Desc().FindFormat(&audioFormatMPEG4Audio) if audioMedia != nil { - mi.stream.AddReader(mi.writer, audioMedia, audioFormatMPEG4Audio, func(u unit.Unit) error { - tunit := u.(*unit.MPEG4Audio) + co := audioFormatMPEG4Audio.GetConfig() + if co == nil { + mi.Log(logger.Warn, "skipping MPEG-4 audio track: tracks without explicit configuration are not supported") + } else { + mi.stream.AddReader(mi.writer, audioMedia, audioFormatMPEG4Audio, func(u unit.Unit) error { + tunit := u.(*unit.MPEG4Audio) + + if tunit.AUs == nil { + return nil + } + + err := mi.hmuxer.WriteMPEG4Audio( + tunit.NTP, + tunit.PTS, + tunit.AUs) + if err != nil { + return fmt.Errorf("muxer error: %w", err) + } - if tunit.AUs == nil { return nil - } + }) - err := mi.hmuxer.WriteMPEG4Audio( - tunit.NTP, - tunit.PTS, - tunit.AUs) - if err != nil { - return fmt.Errorf("muxer error: %w", err) + return &gohlslib.Track{ + Codec: &codecs.MPEG4Audio{ + Config: *co, + }, } - - return nil - }) - - return &gohlslib.Track{ - Codec: &codecs.MPEG4Audio{ - Config: *audioFormatMPEG4Audio.GetConfig(), - }, } } From 59ae3add7e5debdb0ea4e55f1f1fafec2b953a25 Mon Sep 17 00:00:00 2001 From: Alessandro Ros Date: Thu, 1 Aug 2024 16:42:53 +0200 Subject: [PATCH 03/88] move codec-related constants into formatprocessor (#3595) --- internal/formatprocessor/av1.go | 7 ++++ internal/formatprocessor/h264.go | 11 ++++++ internal/formatprocessor/h265.go | 24 +++++++++++++ internal/formatprocessor/mpeg1_video.go | 9 +++++ internal/formatprocessor/mpeg4_video.go | 12 +++++++ internal/record/format_fmp4.go | 48 +++++-------------------- 6 files changed, 72 insertions(+), 39 deletions(-) diff --git a/internal/formatprocessor/av1.go b/internal/formatprocessor/av1.go index 05e96b97b1a..a549dd10634 100644 --- a/internal/formatprocessor/av1.go +++ b/internal/formatprocessor/av1.go @@ -13,6 +13,13 @@ import ( "github.com/bluenviron/mediamtx/internal/unit" ) +// AV1-related parameters +var ( + AV1DefaultSequenceHeader = []byte{ + 8, 0, 0, 0, 66, 167, 191, 228, 96, 13, 0, 64, + } +) + type formatProcessorAV1 struct { udpMaxPayloadSize int format *format.AV1 diff --git a/internal/formatprocessor/h264.go b/internal/formatprocessor/h264.go index 0d5cf2895f5..55c2de78eb2 100644 --- a/internal/formatprocessor/h264.go +++ b/internal/formatprocessor/h264.go @@ -14,6 +14,17 @@ import ( "github.com/bluenviron/mediamtx/internal/unit" ) +// H264-related parameters +var ( + H264DefaultSPS = []byte{ // 1920x1080 baseline + 0x67, 0x42, 0xc0, 0x28, 0xd9, 0x00, 0x78, 0x02, + 0x27, 0xe5, 0x84, 0x00, 0x00, 0x03, 0x00, 0x04, + 0x00, 0x00, 0x03, 0x00, 0xf0, 0x3c, 0x60, 0xc9, 0x20, + } + + H264DefaultPPS = []byte{0x08, 0x06, 0x07, 0x08} +) + // extract SPS and PPS without decoding RTP packets func rtpH264ExtractParams(payload []byte) ([]byte, []byte) { if len(payload) < 1 { diff --git a/internal/formatprocessor/h265.go b/internal/formatprocessor/h265.go index eaa336d17ce..4e07b91b524 100644 --- a/internal/formatprocessor/h265.go +++ b/internal/formatprocessor/h265.go @@ -14,6 +14,30 @@ import ( "github.com/bluenviron/mediamtx/internal/unit" ) +// H265-related parameters +var ( + H265DefaultVPS = []byte{ + 0x40, 0x01, 0x0c, 0x01, 0xff, 0xff, 0x02, 0x20, + 0x00, 0x00, 0x03, 0x00, 0xb0, 0x00, 0x00, 0x03, + 0x00, 0x00, 0x03, 0x00, 0x7b, 0x18, 0xb0, 0x24, + } + + H265DefaultSPS = []byte{ + 0x42, 0x01, 0x01, 0x02, 0x20, 0x00, 0x00, 0x03, + 0x00, 0xb0, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, + 0x00, 0x7b, 0xa0, 0x07, 0x82, 0x00, 0x88, 0x7d, + 0xb6, 0x71, 0x8b, 0x92, 0x44, 0x80, 0x53, 0x88, + 0x88, 0x92, 0xcf, 0x24, 0xa6, 0x92, 0x72, 0xc9, + 0x12, 0x49, 0x22, 0xdc, 0x91, 0xaa, 0x48, 0xfc, + 0xa2, 0x23, 0xff, 0x00, 0x01, 0x00, 0x01, 0x6a, + 0x02, 0x02, 0x02, 0x01, + } + + H265DefaultPPS = []byte{ + 0x44, 0x01, 0xc0, 0x25, 0x2f, 0x05, 0x32, 0x40, + } +) + // extract VPS, SPS and PPS without decoding RTP packets func rtpH265ExtractParams(payload []byte) ([]byte, []byte, []byte) { if len(payload) < 2 { diff --git a/internal/formatprocessor/mpeg1_video.go b/internal/formatprocessor/mpeg1_video.go index 8083aeffd91..079049ffa0f 100644 --- a/internal/formatprocessor/mpeg1_video.go +++ b/internal/formatprocessor/mpeg1_video.go @@ -13,6 +13,15 @@ import ( "github.com/bluenviron/mediamtx/internal/unit" ) +// MPEG-1 video related parameters +var ( + MPEG1VideoDefaultConfig = []byte{ + 0x00, 0x00, 0x01, 0xb3, 0x78, 0x04, 0x38, 0x35, + 0xff, 0xff, 0xe0, 0x18, 0x00, 0x00, 0x01, 0xb5, + 0x14, 0x4a, 0x00, 0x01, 0x00, 0x00, + } +) + type formatProcessorMPEG1Video struct { udpMaxPayloadSize int format *format.MPEG1Video diff --git a/internal/formatprocessor/mpeg4_video.go b/internal/formatprocessor/mpeg4_video.go index eac47fc1425..6369123145a 100644 --- a/internal/formatprocessor/mpeg4_video.go +++ b/internal/formatprocessor/mpeg4_video.go @@ -15,6 +15,18 @@ import ( "github.com/bluenviron/mediamtx/internal/unit" ) +// MPEG-4 video related parameters +var ( + MPEG4VideoDefaultConfig = []byte{ + 0x00, 0x00, 0x01, 0xb0, 0x01, 0x00, 0x00, 0x01, + 0xb5, 0x89, 0x13, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x01, 0x20, 0x00, 0xc4, 0x8d, 0x88, 0x00, + 0xf5, 0x3c, 0x04, 0x87, 0x14, 0x63, 0x00, 0x00, + 0x01, 0xb2, 0x4c, 0x61, 0x76, 0x63, 0x35, 0x38, + 0x2e, 0x31, 0x33, 0x34, 0x2e, 0x31, 0x30, 0x30, + } +) + type formatProcessorMPEG4Video struct { udpMaxPayloadSize int format *format.MPEG4Video diff --git a/internal/record/format_fmp4.go b/internal/record/format_fmp4.go index 6041b34f874..462f24fd94c 100644 --- a/internal/record/format_fmp4.go +++ b/internal/record/format_fmp4.go @@ -20,8 +20,8 @@ import ( "github.com/bluenviron/mediacommon/pkg/formats/fmp4" "github.com/bluenviron/mediamtx/internal/defs" + "github.com/bluenviron/mediamtx/internal/formatprocessor" "github.com/bluenviron/mediamtx/internal/logger" - "github.com/bluenviron/mediamtx/internal/test" "github.com/bluenviron/mediamtx/internal/unit" ) @@ -145,9 +145,7 @@ func (f *formatFMP4) initialize() { switch forma := forma.(type) { case *rtspformat.AV1: codec := &fmp4.CodecAV1{ - SequenceHeader: []byte{ - 8, 0, 0, 0, 66, 167, 191, 228, 96, 13, 0, 64, - }, + SequenceHeader: formatprocessor.AV1DefaultSequenceHeader, } track := addTrack(forma, codec) @@ -278,26 +276,9 @@ func (f *formatFMP4) initialize() { vps, sps, pps := forma.SafeParams() if vps == nil || sps == nil || pps == nil { - vps = []byte{ - 0x40, 0x01, 0x0c, 0x01, 0xff, 0xff, 0x02, 0x20, - 0x00, 0x00, 0x03, 0x00, 0xb0, 0x00, 0x00, 0x03, - 0x00, 0x00, 0x03, 0x00, 0x7b, 0x18, 0xb0, 0x24, - } - - sps = []byte{ - 0x42, 0x01, 0x01, 0x02, 0x20, 0x00, 0x00, 0x03, - 0x00, 0xb0, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, - 0x00, 0x7b, 0xa0, 0x07, 0x82, 0x00, 0x88, 0x7d, - 0xb6, 0x71, 0x8b, 0x92, 0x44, 0x80, 0x53, 0x88, - 0x88, 0x92, 0xcf, 0x24, 0xa6, 0x92, 0x72, 0xc9, - 0x12, 0x49, 0x22, 0xdc, 0x91, 0xaa, 0x48, 0xfc, - 0xa2, 0x23, 0xff, 0x00, 0x01, 0x00, 0x01, 0x6a, - 0x02, 0x02, 0x02, 0x01, - } - - pps = []byte{ - 0x44, 0x01, 0xc0, 0x25, 0x2f, 0x05, 0x32, 0x40, - } + vps = formatprocessor.H265DefaultVPS + sps = formatprocessor.H265DefaultSPS + pps = formatprocessor.H265DefaultPPS } codec := &fmp4.CodecH265{ @@ -375,8 +356,8 @@ func (f *formatFMP4) initialize() { sps, pps := forma.SafeParams() if sps == nil || pps == nil { - sps = test.FormatH264.SPS - pps = test.FormatH264.PPS + sps = formatprocessor.H264DefaultSPS + pps = formatprocessor.H264DefaultPPS } codec := &fmp4.CodecH264{ @@ -446,14 +427,7 @@ func (f *formatFMP4) initialize() { config := forma.SafeParams() if config == nil { - config = []byte{ - 0x00, 0x00, 0x01, 0xb0, 0x01, 0x00, 0x00, 0x01, - 0xb5, 0x89, 0x13, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x00, 0x01, 0x20, 0x00, 0xc4, 0x8d, 0x88, 0x00, - 0xf5, 0x3c, 0x04, 0x87, 0x14, 0x63, 0x00, 0x00, - 0x01, 0xb2, 0x4c, 0x61, 0x76, 0x63, 0x35, 0x38, - 0x2e, 0x31, 0x33, 0x34, 0x2e, 0x31, 0x30, 0x30, - } + config = formatprocessor.MPEG4VideoDefaultConfig } codec := &fmp4.CodecMPEG4Video{ @@ -506,11 +480,7 @@ func (f *formatFMP4) initialize() { case *rtspformat.MPEG1Video: codec := &fmp4.CodecMPEG1Video{ - Config: []byte{ - 0x00, 0x00, 0x01, 0xb3, 0x78, 0x04, 0x38, 0x35, - 0xff, 0xff, 0xe0, 0x18, 0x00, 0x00, 0x01, 0xb5, - 0x14, 0x4a, 0x00, 0x01, 0x00, 0x00, - }, + Config: formatprocessor.MPEG1VideoDefaultConfig, } track := addTrack(forma, codec) From c9a938a5019e72386e805351a85d9ac02d11fba1 Mon Sep 17 00:00:00 2001 From: Alessandro Ros Date: Thu, 1 Aug 2024 17:01:56 +0200 Subject: [PATCH 04/88] improve fuzz tests (#3596) --- .../protocols/rtmp/amf0/unmarshal_test.go | 5 ++++- internal/protocols/rtmp/chunk/chunk_test.go | 20 +++++++++++++++---- .../rtmp/handshake/handshake_test.go | 1 + .../protocols/rtmp/message/reader_test.go | 15 +++++++++++--- .../protocols/rtmp/rawmessage/reader_test.go | 14 +++++++++---- 5 files changed, 43 insertions(+), 12 deletions(-) diff --git a/internal/protocols/rtmp/amf0/unmarshal_test.go b/internal/protocols/rtmp/amf0/unmarshal_test.go index ef55eee5129..e43f8904217 100644 --- a/internal/protocols/rtmp/amf0/unmarshal_test.go +++ b/internal/protocols/rtmp/amf0/unmarshal_test.go @@ -322,6 +322,9 @@ func FuzzUnmarshal(f *testing.F) { } f.Fuzz(func(_ *testing.T, b []byte) { - Unmarshal(b) //nolint:errcheck + what, err := Unmarshal(b) + if err == nil { + Marshal(what) //nolint:errcheck + } }) } diff --git a/internal/protocols/rtmp/chunk/chunk_test.go b/internal/protocols/rtmp/chunk/chunk_test.go index 750ec753519..05bd92968ae 100644 --- a/internal/protocols/rtmp/chunk/chunk_test.go +++ b/internal/protocols/rtmp/chunk/chunk_test.go @@ -160,27 +160,39 @@ func TestChunkMarshal(t *testing.T) { func FuzzChunk0Read(f *testing.F) { f.Fuzz(func(_ *testing.T, b []byte) { var chunk Chunk0 - chunk.Read(bytes.NewReader(b), 65536, false) //nolint:errcheck + err := chunk.Read(bytes.NewReader(b), 65536, false) + if err == nil { + chunk.Marshal(false) //nolint:errcheck + } }) } func FuzzChunk1Read(f *testing.F) { f.Fuzz(func(_ *testing.T, b []byte) { var chunk Chunk1 - chunk.Read(bytes.NewReader(b), 65536, false) //nolint:errcheck + err := chunk.Read(bytes.NewReader(b), 65536, false) + if err == nil { + chunk.Marshal(false) //nolint:errcheck + } }) } func FuzzChunk2Read(f *testing.F) { f.Fuzz(func(_ *testing.T, b []byte) { var chunk Chunk2 - chunk.Read(bytes.NewReader(b), 65536, false) //nolint:errcheck + err := chunk.Read(bytes.NewReader(b), 65536, false) + if err == nil { + chunk.Marshal(false) //nolint:errcheck + } }) } func FuzzChunk3Read(f *testing.F) { f.Fuzz(func(_ *testing.T, b []byte) { var chunk Chunk3 - chunk.Read(bytes.NewReader(b), 65536, true) //nolint:errcheck + err := chunk.Read(bytes.NewReader(b), 65536, true) + if err == nil { + chunk.Marshal(false) //nolint:errcheck + } }) } diff --git a/internal/protocols/rtmp/handshake/handshake_test.go b/internal/protocols/rtmp/handshake/handshake_test.go index d9ee41368d7..ffea3a7c38f 100644 --- a/internal/protocols/rtmp/handshake/handshake_test.go +++ b/internal/protocols/rtmp/handshake/handshake_test.go @@ -38,6 +38,7 @@ func TestHandshake(t *testing.T) { clientInKey, clientOutKey, err := DoClient(rw, ca == "encrypted", true) require.NoError(t, err) + <-done if ca == "encrypted" { diff --git a/internal/protocols/rtmp/message/reader_test.go b/internal/protocols/rtmp/message/reader_test.go index 5ce3af1db08..25e85e7ace5 100644 --- a/internal/protocols/rtmp/message/reader_test.go +++ b/internal/protocols/rtmp/message/reader_test.go @@ -297,9 +297,18 @@ func FuzzReader(f *testing.F) { 0x01, 0x00, 0x00, 0x00, 0x88, 0x68, 0x76, 0x63, 0x31, 0x01, 0x02, 0x03, }) + f.Fuzz(func(_ *testing.T, b []byte) { - bc := bytecounter.NewReader(bytes.NewReader(b)) - r := NewReader(bc, bc, nil) - r.Read() //nolint:errcheck + bcr := bytecounter.NewReader(bytes.NewReader(b)) + r := NewReader(bcr, bcr, nil) + + var buf bytes.Buffer + bcw := bytecounter.NewWriter(&buf) + w := NewWriter(bcw, bcw, true) + + msg, err := r.Read() + if err == nil { + w.Write(msg) //nolint:errcheck + } }) } diff --git a/internal/protocols/rtmp/rawmessage/reader_test.go b/internal/protocols/rtmp/rawmessage/reader_test.go index ee59c393be6..1b2b071b1d3 100644 --- a/internal/protocols/rtmp/rawmessage/reader_test.go +++ b/internal/protocols/rtmp/rawmessage/reader_test.go @@ -270,14 +270,20 @@ func TestReaderAcknowledge(t *testing.T) { func FuzzReader(f *testing.F) { f.Fuzz(func(_ *testing.T, b []byte) { - br := bytecounter.NewReader(bytes.NewReader(b)) - r := NewReader(br, br, func(_ uint32) error { + bcr := bytecounter.NewReader(bytes.NewReader(b)) + r := NewReader(bcr, bcr, func(_ uint32) error { return nil }) + var buf bytes.Buffer + bcw := bytecounter.NewWriter(&buf) + w := NewWriter(bcw, bcw, true) + for { - _, err := r.Read() - if err != nil { + msg, err := r.Read() + if err == nil { + w.Write(msg) //nolint:errcheck + } else { break } } From 67198ccc6234146292b1767b9b1d41d3ab8e5fc4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 1 Aug 2024 21:55:48 +0200 Subject: [PATCH 05/88] build(deps): bump github.com/bluenviron/mediacommon (#3597) Bumps [github.com/bluenviron/mediacommon](https://github.com/bluenviron/mediacommon) from 1.12.1 to 1.12.2. - [Commits](https://github.com/bluenviron/mediacommon/compare/v1.12.1...v1.12.2) --- updated-dependencies: - dependency-name: github.com/bluenviron/mediacommon dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 004a11130e8..56c9f0842c1 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/alecthomas/kong v0.9.0 github.com/bluenviron/gohlslib v1.4.0 github.com/bluenviron/gortsplib/v4 v4.10.3-0.20240801095652-e2d1e6dab418 - github.com/bluenviron/mediacommon v1.12.1 + github.com/bluenviron/mediacommon v1.12.2 github.com/datarhei/gosrt v0.7.0 github.com/fsnotify/fsnotify v1.7.0 github.com/gin-gonic/gin v1.10.0 diff --git a/go.sum b/go.sum index c547eec5588..0c3c97a1a88 100644 --- a/go.sum +++ b/go.sum @@ -24,8 +24,8 @@ github.com/bluenviron/gohlslib v1.4.0 h1:3a9W1x8eqlxJUKt1sJCunPGtti5ALIY2ik4GU0R github.com/bluenviron/gohlslib v1.4.0/go.mod h1:q5ZElzNw5GRbV1VEI45qkcPbKBco6BP58QEY5HyFsmo= github.com/bluenviron/gortsplib/v4 v4.10.3-0.20240801095652-e2d1e6dab418 h1:j+qo+yB1S1KiTOKSN4ABPbJ9rNdzoT6AoH9F/+k1QjE= github.com/bluenviron/gortsplib/v4 v4.10.3-0.20240801095652-e2d1e6dab418/go.mod h1:FE/oUV476F/paP+D2YAV2LvXDgpZef4lN5uAKAuVAsQ= -github.com/bluenviron/mediacommon v1.12.1 h1:sgDJaKV6OXrPCSO0KPp9zi/pwNWtKHenn5/dvjtY+Tg= -github.com/bluenviron/mediacommon v1.12.1/go.mod h1:HDyW2CzjvhYJXtdxstdFPio3G0qSocPhqkhUt/qffec= +github.com/bluenviron/mediacommon v1.12.2 h1:H2TUvZmB5et5C7kORFrJ9XdOAtxYipnQBJlY4XAvY1M= +github.com/bluenviron/mediacommon v1.12.2/go.mod h1:HDyW2CzjvhYJXtdxstdFPio3G0qSocPhqkhUt/qffec= github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= From 012cc6c701fe42f7bb2ddce878b69abb12c4e2d0 Mon Sep 17 00:00:00 2001 From: Alessandro Ros Date: Sat, 3 Aug 2024 20:47:09 +0200 Subject: [PATCH 06/88] simplify tests (#3604) --- internal/core/path_test.go | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/internal/core/path_test.go b/internal/core/path_test.go index 5b12306123f..b473a801fe7 100644 --- a/internal/core/path_test.go +++ b/internal/core/path_test.go @@ -8,7 +8,6 @@ import ( "net/http" "net/url" "os" - "os/exec" "path/filepath" "strings" "testing" @@ -112,16 +111,7 @@ func TestPathRunOnDemand(t *testing.T) { err := os.WriteFile(srcFile, []byte(strings.ReplaceAll(runOnDemandSampleScript, "ON_DEMAND_FILE", onDemand)), 0o644) require.NoError(t, err) - - execFile := filepath.Join(os.TempDir(), "ondemand_cmd") - cmd := exec.Command("go", "build", "-o", execFile, srcFile) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - err = cmd.Run() - require.NoError(t, err) - defer os.Remove(execFile) - - os.Remove(srcFile) + defer os.Remove(srcFile) for _, ca := range []string{"describe", "setup", "describe and setup"} { t.Run(ca, func(t *testing.T) { @@ -133,9 +123,9 @@ func TestPathRunOnDemand(t *testing.T) { "webrtc: no\n"+ "paths:\n"+ " '~^(on)demand$':\n"+ - " runOnDemand: %s\n"+ + " runOnDemand: go run %s\n"+ " runOnDemandCloseAfter: 1s\n"+ - " runOnUnDemand: touch %s\n", execFile, onUnDemand)) + " runOnUnDemand: touch %s\n", srcFile, onUnDemand)) require.Equal(t, true, ok) defer p1.Close() From 547e56e82b81fce123cd9082e1cec6cf47477a24 Mon Sep 17 00:00:00 2001 From: Alessandro Ros Date: Sat, 3 Aug 2024 20:51:54 +0200 Subject: [PATCH 07/88] enable runOnDemandRestart by default (#3605) --- README.md | 3 --- internal/conf/conf_test.go | 1 + internal/conf/path.go | 1 + mediamtx.yml | 2 +- 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 3e09e429893..ef349d4fed6 100644 --- a/README.md +++ b/README.md @@ -1428,7 +1428,6 @@ Edit `mediamtx.yml` and replace everything inside section `paths` with the follo paths: ondemand: runOnDemand: ffmpeg -re -stream_loop -1 -i file.ts -c copy -f rtsp rtsp://localhost:$RTSP_PORT/$MTX_PATH - runOnDemandRestart: yes ``` The command inserted into `runOnDemand` will start only when a client requests the path `ondemand`, therefore the file will start streaming only when requested. @@ -1588,8 +1587,6 @@ pathDefaults: # * G1, G2, ...: regular expression groups, if path name is # a regular expression. runOnDemand: ffmpeg -i my_file.mp4 -c copy -f rtsp rtsp://localhost:8554/mypath - # Restart the command if it exits. - runOnDemandRestart: no ``` `runOnUnDemand` allows to run a command when there are no readers anymore: diff --git a/internal/conf/conf_test.go b/internal/conf/conf_test.go index 0c26086cfa6..6b5d272735c 100644 --- a/internal/conf/conf_test.go +++ b/internal/conf/conf_test.go @@ -77,6 +77,7 @@ func TestConfFromFile(t *testing.T) { RPICameraAfRange: "normal", RPICameraAfSpeed: "normal", RPICameraTextOverlay: "%Y-%m-%d %H:%M:%S - MediaMTX", + RunOnDemandRestart: true, RunOnDemandStartTimeout: 5 * StringDuration(time.Second), RunOnDemandCloseAfter: 10 * StringDuration(time.Second), }, pa) diff --git a/internal/conf/path.go b/internal/conf/path.go index 925f050f439..4c6d9cb938f 100644 --- a/internal/conf/path.go +++ b/internal/conf/path.go @@ -220,6 +220,7 @@ func (pconf *Path) setDefaults() { pconf.RPICameraTextOverlay = "%Y-%m-%d %H:%M:%S - MediaMTX" // Hooks + pconf.RunOnDemandRestart = true pconf.RunOnDemandStartTimeout = 10 * StringDuration(time.Second) pconf.RunOnDemandCloseAfter = 10 * StringDuration(time.Second) } diff --git a/mediamtx.yml b/mediamtx.yml index f936a305d3e..d5efd96fff0 100644 --- a/mediamtx.yml +++ b/mediamtx.yml @@ -615,7 +615,7 @@ pathDefaults: # a regular expression. runOnDemand: # Restart the command if it exits. - runOnDemandRestart: no + runOnDemandRestart: yes # Readers will be put on hold until the runOnDemand command starts publishing # or until this amount of time has passed. runOnDemandStartTimeout: 10s From c80bb53b0fd5512352bb83c3dc0216a34b93e738 Mon Sep 17 00:00:00 2001 From: Alessandro Ros Date: Sun, 4 Aug 2024 12:32:16 +0200 Subject: [PATCH 08/88] apidocs: add missing authentication-related parameters (#3607) --- README.md | 2 +- apidocs/openapi.yaml | 60 +++++++++++++++++++++++++++++--------------- 2 files changed, 41 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index ef349d4fed6..a239d0263fb 100644 --- a/README.md +++ b/README.md @@ -1124,7 +1124,7 @@ Authentication can be delegated to an external HTTP server: ```yml authMethod: http -externalAuthenticationURL: http://myauthserver/auth +authHTTPAddress: http://myauthserver/auth ``` Each time a user needs to be authenticated, the specified URL will be requested with the POST method and this payload: diff --git a/apidocs/openapi.yaml b/apidocs/openapi.yaml index 93802e49ca6..8d67b6326cb 100644 --- a/apidocs/openapi.yaml +++ b/apidocs/openapi.yaml @@ -21,6 +21,30 @@ components: error: type: string + AuthInternalUser: + type: object + properties: + user: + type: string + pass: + type: string + ips: + type: array + items: + type: string + permissions: + type: array + items: + $ref: '#/components/schemas/AuthInternalUserPermission' + + AuthInternalUserPermission: + type: object + properties: + action: + type: string + path: + type: string + GlobalConf: type: object properties: @@ -41,8 +65,6 @@ components: type: integer udpMaxPayloadSize: type: integer - externalAuthenticationURL: - type: string runOnConnect: type: string runOnConnectRestart: @@ -50,6 +72,22 @@ components: runOnDisconnect: type: string + # Authentication + authMethod: + type: string + authInternalUsers: + type: array + items: + $ref: '#/components/schemas/AuthInternalUser' + authHTTPAddress: + type: string + authHTTPExclude: + type: array + items: + $ref: '#/components/schemas/AuthInternalUserPermission' + authJWTJWKS: + type: string + # Control API api: type: boolean @@ -295,24 +333,6 @@ components: recordDeleteAfter: type: string - # Authentication - publishUser: - type: string - publishPass: - type: string - publishIPs: - type: array - items: - type: string - readUser: - type: string - readPass: - type: string - readIPs: - type: array - items: - type: string - # Publisher source overridePublisher: type: boolean From aade940296b51ebcf69dded49505acdc47a4ba39 Mon Sep 17 00:00:00 2001 From: Alessandro Ros Date: Sun, 4 Aug 2024 12:32:37 +0200 Subject: [PATCH 09/88] Revert "enable runOnDemandRestart by default (#3605)" (#3609) This reverts commit 547e56e82b81fce123cd9082e1cec6cf47477a24. --- README.md | 3 +++ internal/conf/conf_test.go | 1 - internal/conf/path.go | 1 - mediamtx.yml | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a239d0263fb..a931ce0f334 100644 --- a/README.md +++ b/README.md @@ -1428,6 +1428,7 @@ Edit `mediamtx.yml` and replace everything inside section `paths` with the follo paths: ondemand: runOnDemand: ffmpeg -re -stream_loop -1 -i file.ts -c copy -f rtsp rtsp://localhost:$RTSP_PORT/$MTX_PATH + runOnDemandRestart: yes ``` The command inserted into `runOnDemand` will start only when a client requests the path `ondemand`, therefore the file will start streaming only when requested. @@ -1587,6 +1588,8 @@ pathDefaults: # * G1, G2, ...: regular expression groups, if path name is # a regular expression. runOnDemand: ffmpeg -i my_file.mp4 -c copy -f rtsp rtsp://localhost:8554/mypath + # Restart the command if it exits. + runOnDemandRestart: no ``` `runOnUnDemand` allows to run a command when there are no readers anymore: diff --git a/internal/conf/conf_test.go b/internal/conf/conf_test.go index 6b5d272735c..0c26086cfa6 100644 --- a/internal/conf/conf_test.go +++ b/internal/conf/conf_test.go @@ -77,7 +77,6 @@ func TestConfFromFile(t *testing.T) { RPICameraAfRange: "normal", RPICameraAfSpeed: "normal", RPICameraTextOverlay: "%Y-%m-%d %H:%M:%S - MediaMTX", - RunOnDemandRestart: true, RunOnDemandStartTimeout: 5 * StringDuration(time.Second), RunOnDemandCloseAfter: 10 * StringDuration(time.Second), }, pa) diff --git a/internal/conf/path.go b/internal/conf/path.go index 4c6d9cb938f..925f050f439 100644 --- a/internal/conf/path.go +++ b/internal/conf/path.go @@ -220,7 +220,6 @@ func (pconf *Path) setDefaults() { pconf.RPICameraTextOverlay = "%Y-%m-%d %H:%M:%S - MediaMTX" // Hooks - pconf.RunOnDemandRestart = true pconf.RunOnDemandStartTimeout = 10 * StringDuration(time.Second) pconf.RunOnDemandCloseAfter = 10 * StringDuration(time.Second) } diff --git a/mediamtx.yml b/mediamtx.yml index d5efd96fff0..f936a305d3e 100644 --- a/mediamtx.yml +++ b/mediamtx.yml @@ -615,7 +615,7 @@ pathDefaults: # a regular expression. runOnDemand: # Restart the command if it exits. - runOnDemandRestart: yes + runOnDemandRestart: no # Readers will be put on hold until the runOnDemand command starts publishing # or until this amount of time has passed. runOnDemandStartTimeout: 10s From 44a879170f90d2f445d1e15156d35bbfa14b1722 Mon Sep 17 00:00:00 2001 From: Alessandro Ros Date: Sun, 4 Aug 2024 12:52:36 +0200 Subject: [PATCH 10/88] apidocs: add missing fields in /list and SRT connections (#3610) --- apidocs/openapi.yaml | 37 ++++++++++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/apidocs/openapi.yaml b/apidocs/openapi.yaml index 8d67b6326cb..a13c5795b06 100644 --- a/apidocs/openapi.yaml +++ b/apidocs/openapi.yaml @@ -462,6 +462,8 @@ components: properties: pageCount: type: integer + itemCount: + type: integer items: type: array items: @@ -502,6 +504,8 @@ components: properties: pageCount: type: integer + itemCount: + type: integer items: type: array items: @@ -562,17 +566,13 @@ components: properties: pageCount: type: integer + itemCount: + type: integer items: type: array items: $ref: '#/components/schemas/HLSMuxer' - RecordingSegment: - type: object - properties: - start: - type: string - Recording: type: object properties: @@ -588,11 +588,19 @@ components: properties: pageCount: type: integer + itemCount: + type: integer items: type: array items: $ref: '#/components/schemas/Recording' + RecordingSegment: + type: object + properties: + start: + type: string + RTMPConn: type: object properties: @@ -621,6 +629,8 @@ components: properties: pageCount: type: integer + itemCount: + type: integer items: type: array items: @@ -647,6 +657,8 @@ components: properties: pageCount: type: integer + itemCount: + type: integer items: type: array items: @@ -683,6 +695,8 @@ components: properties: pageCount: type: integer + itemCount: + type: integer items: type: array items: @@ -712,6 +726,9 @@ components: type: integer format: int64 description: The total number of received DATA packets, including retransmitted packets + packetsReceivedBelated: + type: integer + format: int64 packetsSentUnique: type: integer format: int64 @@ -784,6 +801,9 @@ components: type: integer format: int64 description: Same as packetsReceived, but expressed in bytes, including payload and all the headers (IP, TCP, SRT) + bytesReceivedBelated: + type: integer + format: int64 bytesSentUnique: type: integer format: int64 @@ -909,12 +929,13 @@ components: format: float64 description: Percentage of retransmitted data vs. received data - SRTConnList: type: object properties: pageCount: type: integer + itemCount: + type: integer items: type: array items: @@ -954,6 +975,8 @@ components: properties: pageCount: type: integer + itemCount: + type: integer items: type: array items: From 49c8acf2f6ea7e57d6b4d3098e97819dc49b8382 Mon Sep 17 00:00:00 2001 From: Alessandro Ros Date: Sun, 4 Aug 2024 13:10:14 +0200 Subject: [PATCH 11/88] update golangci-lint (#3611) --- .golangci.yml | 2 ++ Makefile | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.golangci.yml b/.golangci.yml index 1ac50f4cf41..b418e77e919 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -14,9 +14,11 @@ linters: - misspell - nilerr - prealloc + - predeclared - revive - usestdlibvars - unconvert + - tenv - tparallel - wastedassign - whitespace diff --git a/Makefile b/Makefile index 0b10828b306..2bd05b85e93 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ BASE_IMAGE = golang:1.22-alpine3.19 -LINT_IMAGE = golangci/golangci-lint:v1.56.2 +LINT_IMAGE = golangci/golangci-lint:v1.59.1 NODE_IMAGE = node:20-alpine3.19 ALPINE_IMAGE = alpine:3.19 RPI32_IMAGE = balenalib/raspberry-pi:bullseye-run-20230712 From 972ffbf3323b8cf241b76fdf4111b8d04e579270 Mon Sep 17 00:00:00 2001 From: Alessandro Ros Date: Sun, 4 Aug 2024 13:12:30 +0200 Subject: [PATCH 12/88] improve API docs linter (#3608) --- .github/workflows/code_lint.yml | 6 +- .github/workflows/release.yml | 2 +- Makefile | 3 +- internal/testapidocs/apidocs_test.go | 154 ++++++++++++++++++ .../build_images_test.go | 2 +- .../hls_manager_test.go | 2 +- .../images/ffmpeg/Dockerfile | 0 .../images/ffmpeg/emptyvideo.mkv | Bin .../images/ffmpeg/emptyvideoaudio.mkv | Bin .../images/ffmpeg/start.sh | 0 .../images/gstreamer/Dockerfile | 0 .../images/gstreamer/emptyvideo.mkv | Bin .../images/gstreamer/exitafterframe.c | 0 .../images/gstreamer/start.sh | 0 .../images/vlc/Dockerfile | 0 .../images/vlc/start.sh | 0 .../rtsp_server_test.go | 2 +- .../tests_test.go | 2 +- scripts/apidocs.mk | 13 +- scripts/lint.mk | 19 ++- scripts/test-highlevel.mk | 2 +- 21 files changed, 182 insertions(+), 25 deletions(-) create mode 100644 internal/testapidocs/apidocs_test.go rename internal/{highleveltests => testhighlevel}/build_images_test.go (96%) rename internal/{highleveltests => testhighlevel}/hls_manager_test.go (98%) rename internal/{highleveltests => testhighlevel}/images/ffmpeg/Dockerfile (100%) rename internal/{highleveltests => testhighlevel}/images/ffmpeg/emptyvideo.mkv (100%) rename internal/{highleveltests => testhighlevel}/images/ffmpeg/emptyvideoaudio.mkv (100%) rename internal/{highleveltests => testhighlevel}/images/ffmpeg/start.sh (100%) rename internal/{highleveltests => testhighlevel}/images/gstreamer/Dockerfile (100%) rename internal/{highleveltests => testhighlevel}/images/gstreamer/emptyvideo.mkv (100%) rename internal/{highleveltests => testhighlevel}/images/gstreamer/exitafterframe.c (100%) rename internal/{highleveltests => testhighlevel}/images/gstreamer/start.sh (100%) rename internal/{highleveltests => testhighlevel}/images/vlc/Dockerfile (100%) rename internal/{highleveltests => testhighlevel}/images/vlc/start.sh (100%) rename internal/{highleveltests => testhighlevel}/rtsp_server_test.go (99%) rename internal/{highleveltests => testhighlevel}/tests_test.go (98%) diff --git a/.github/workflows/code_lint.yml b/.github/workflows/code_lint.yml index d9cafd2291d..0c77515467c 100644 --- a/.github/workflows/code_lint.yml +++ b/.github/workflows/code_lint.yml @@ -33,9 +33,7 @@ jobs: with: go-version: "1.22" - - run: | - go mod tidy - git diff --exit-code + - run: make lint-mod-tidy api_docs: runs-on: ubuntu-22.04 @@ -43,4 +41,4 @@ jobs: steps: - uses: actions/checkout@v3 - - run: make apidocs-lint + - run: make lint-apidocs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 99639ceb757..b375609c39f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -137,7 +137,7 @@ jobs: steps: - uses: actions/checkout@v3 - - run: make apidocs-gen + - run: make apidocs - run: mv apidocs/*.html apidocs/index.html diff --git a/Makefile b/Makefile index 2bd05b85e93..5414070daaa 100644 --- a/Makefile +++ b/Makefile @@ -20,8 +20,7 @@ help: @echo " lint run linters" @echo " bench NAME=n run bench environment" @echo " run run app" - @echo " apidocs-lint run api docs linters" - @echo " apidocs-gen generate api docs HTML" + @echo " apidocs generate api docs HTML" @echo " binaries build binaries for all platforms" @echo " dockerhub build and push images to Docker Hub" @echo " dockerhub-legacy build and push images to Docker Hub (legacy)" diff --git a/internal/testapidocs/apidocs_test.go b/internal/testapidocs/apidocs_test.go new file mode 100644 index 00000000000..63c38c4dc40 --- /dev/null +++ b/internal/testapidocs/apidocs_test.go @@ -0,0 +1,154 @@ +package main + +import ( + "os" + "reflect" + "sort" + "strings" + "testing" + + "github.com/bluenviron/mediamtx/internal/conf" + "github.com/bluenviron/mediamtx/internal/conf/yaml" + "github.com/bluenviron/mediamtx/internal/defs" + "github.com/stretchr/testify/require" +) + +func TestAPIDocs(t *testing.T) { + byts, err := os.ReadFile("../../apidocs/openapi.yaml") + require.NoError(t, err) + + var raw map[string]interface{} + err = yaml.Load(byts, &raw) + require.NoError(t, err) + + components := raw["components"].(map[string]interface{}) + schemas := components["schemas"].(map[string]interface{}) + + for _, ca := range []struct { + yamlKey string + goStruct interface{} + }{ + { + "AuthInternalUser", + conf.AuthInternalUser{}, + }, + { + "AuthInternalUserPermission", + conf.AuthInternalUserPermission{}, + }, + { + "GlobalConf", + conf.Conf{}, + }, + { + "PathConf", + conf.Path{}, + }, + { + "PathConfList", + defs.APIPathConfList{}, + }, + { + "Path", + defs.APIPath{}, + }, + { + "PathList", + defs.APIPathList{}, + }, + { + "PathSource", + defs.APIPathSourceOrReader{}, + }, + { + "PathReader", + defs.APIPathSourceOrReader{}, + }, + { + "HLSMuxer", + defs.APIHLSMuxer{}, + }, + { + "HLSMuxerList", + defs.APIHLSMuxerList{}, + }, + { + "Recording", + defs.APIRecording{}, + }, + { + "RecordingList", + defs.APIRecordingList{}, + }, + { + "RecordingSegment", + defs.APIRecordingSegment{}, + }, + { + "RTMPConn", + defs.APIRTMPConn{}, + }, + { + "RTMPConnList", + defs.APIRTMPConnList{}, + }, + { + "RTSPConn", + defs.APIRTSPConn{}, + }, + { + "RTSPConnList", + defs.APIRTSPConnsList{}, + }, + { + "RTSPSession", + defs.APIRTSPSession{}, + }, + { + "RTSPSessionList", + defs.APIRTSPSessionList{}, + }, + { + "SRTConn", + defs.APISRTConn{}, + }, + { + "SRTConnList", + defs.APISRTConnList{}, + }, + { + "WebRTCSession", + defs.APIWebRTCSession{}, + }, + { + "WebRTCSessionList", + defs.APIWebRTCSessionList{}, + }, + } { + t.Run(ca.yamlKey, func(t *testing.T) { + yamlContent := schemas[ca.yamlKey].(map[string]interface{}) + props := yamlContent["properties"].(map[string]interface{}) + key1 := make([]string, len(props)) + i := 0 + for key := range props { + key1[i] = key + i++ + } + + var key2 []string + ty := reflect.TypeOf(ca.goStruct) + for i := 0; i < ty.NumField(); i++ { + sf := ty.Field(i) + js := sf.Tag.Get("json") + if js != "-" && js != "paths" && js != "pathDefaults" && !strings.Contains(js, ",omitempty") { + key2 = append(key2, js) + } + } + + sort.Strings(key1) + sort.Strings(key2) + + require.Equal(t, key1, key2) + }) + } +} diff --git a/internal/highleveltests/build_images_test.go b/internal/testhighlevel/build_images_test.go similarity index 96% rename from internal/highleveltests/build_images_test.go rename to internal/testhighlevel/build_images_test.go index 1f164f8ae9a..7d24e4b1152 100644 --- a/internal/highleveltests/build_images_test.go +++ b/internal/testhighlevel/build_images_test.go @@ -1,7 +1,7 @@ //go:build enable_highlevel_tests // +build enable_highlevel_tests -package highleveltests +package testhighlevel import ( "os" diff --git a/internal/highleveltests/hls_manager_test.go b/internal/testhighlevel/hls_manager_test.go similarity index 98% rename from internal/highleveltests/hls_manager_test.go rename to internal/testhighlevel/hls_manager_test.go index 424c71cabfd..b0cc8c19291 100644 --- a/internal/highleveltests/hls_manager_test.go +++ b/internal/testhighlevel/hls_manager_test.go @@ -1,7 +1,7 @@ //go:build enable_highlevel_tests // +build enable_highlevel_tests -package highleveltests +package testhighlevel import ( "net/http" diff --git a/internal/highleveltests/images/ffmpeg/Dockerfile b/internal/testhighlevel/images/ffmpeg/Dockerfile similarity index 100% rename from internal/highleveltests/images/ffmpeg/Dockerfile rename to internal/testhighlevel/images/ffmpeg/Dockerfile diff --git a/internal/highleveltests/images/ffmpeg/emptyvideo.mkv b/internal/testhighlevel/images/ffmpeg/emptyvideo.mkv similarity index 100% rename from internal/highleveltests/images/ffmpeg/emptyvideo.mkv rename to internal/testhighlevel/images/ffmpeg/emptyvideo.mkv diff --git a/internal/highleveltests/images/ffmpeg/emptyvideoaudio.mkv b/internal/testhighlevel/images/ffmpeg/emptyvideoaudio.mkv similarity index 100% rename from internal/highleveltests/images/ffmpeg/emptyvideoaudio.mkv rename to internal/testhighlevel/images/ffmpeg/emptyvideoaudio.mkv diff --git a/internal/highleveltests/images/ffmpeg/start.sh b/internal/testhighlevel/images/ffmpeg/start.sh similarity index 100% rename from internal/highleveltests/images/ffmpeg/start.sh rename to internal/testhighlevel/images/ffmpeg/start.sh diff --git a/internal/highleveltests/images/gstreamer/Dockerfile b/internal/testhighlevel/images/gstreamer/Dockerfile similarity index 100% rename from internal/highleveltests/images/gstreamer/Dockerfile rename to internal/testhighlevel/images/gstreamer/Dockerfile diff --git a/internal/highleveltests/images/gstreamer/emptyvideo.mkv b/internal/testhighlevel/images/gstreamer/emptyvideo.mkv similarity index 100% rename from internal/highleveltests/images/gstreamer/emptyvideo.mkv rename to internal/testhighlevel/images/gstreamer/emptyvideo.mkv diff --git a/internal/highleveltests/images/gstreamer/exitafterframe.c b/internal/testhighlevel/images/gstreamer/exitafterframe.c similarity index 100% rename from internal/highleveltests/images/gstreamer/exitafterframe.c rename to internal/testhighlevel/images/gstreamer/exitafterframe.c diff --git a/internal/highleveltests/images/gstreamer/start.sh b/internal/testhighlevel/images/gstreamer/start.sh similarity index 100% rename from internal/highleveltests/images/gstreamer/start.sh rename to internal/testhighlevel/images/gstreamer/start.sh diff --git a/internal/highleveltests/images/vlc/Dockerfile b/internal/testhighlevel/images/vlc/Dockerfile similarity index 100% rename from internal/highleveltests/images/vlc/Dockerfile rename to internal/testhighlevel/images/vlc/Dockerfile diff --git a/internal/highleveltests/images/vlc/start.sh b/internal/testhighlevel/images/vlc/start.sh similarity index 100% rename from internal/highleveltests/images/vlc/start.sh rename to internal/testhighlevel/images/vlc/start.sh diff --git a/internal/highleveltests/rtsp_server_test.go b/internal/testhighlevel/rtsp_server_test.go similarity index 99% rename from internal/highleveltests/rtsp_server_test.go rename to internal/testhighlevel/rtsp_server_test.go index 4d7e75af9fa..48bd9f7b2b7 100644 --- a/internal/highleveltests/rtsp_server_test.go +++ b/internal/testhighlevel/rtsp_server_test.go @@ -1,7 +1,7 @@ //go:build enable_highlevel_tests // +build enable_highlevel_tests -package highleveltests +package testhighlevel import ( "os" diff --git a/internal/highleveltests/tests_test.go b/internal/testhighlevel/tests_test.go similarity index 98% rename from internal/highleveltests/tests_test.go rename to internal/testhighlevel/tests_test.go index e22ebc39e8e..b72d88d4ac8 100644 --- a/internal/highleveltests/tests_test.go +++ b/internal/testhighlevel/tests_test.go @@ -1,7 +1,7 @@ //go:build enable_highlevel_tests // +build enable_highlevel_tests -package highleveltests +package testhighlevel import ( "os" diff --git a/scripts/apidocs.mk b/scripts/apidocs.mk index 34c6510c20f..e06a006c514 100644 --- a/scripts/apidocs.mk +++ b/scripts/apidocs.mk @@ -1,21 +1,10 @@ -define DOCKERFILE_APIDOCS_LINT -FROM $(NODE_IMAGE) -RUN yarn global add @redocly/cli@1.0.0-beta.123 -endef -export DOCKERFILE_APIDOCS_LINT - -apidocs-lint: - echo "$$DOCKERFILE_APIDOCS_LINT" | docker build . -f - -t temp - docker run --rm -v $(PWD)/apidocs:/s -w /s temp \ - sh -c "openapi lint openapi.yaml" - define DOCKERFILE_APIDOCS_GEN FROM $(NODE_IMAGE) RUN yarn global add redoc-cli@0.13.7 endef export DOCKERFILE_APIDOCS_GEN -apidocs-gen: +apidocs: echo "$$DOCKERFILE_APIDOCS_GEN" | docker build . -f - -t temp docker run --rm -v $(PWD)/apidocs:/s -w /s temp \ sh -c "redoc-cli bundle openapi.yaml" diff --git a/scripts/lint.mk b/scripts/lint.mk index 5245ac8cbb4..f42ec326e5a 100644 --- a/scripts/lint.mk +++ b/scripts/lint.mk @@ -1,5 +1,22 @@ -lint: +define DOCKERFILE_APIDOCS_LINT +FROM $(NODE_IMAGE) +RUN yarn global add @redocly/cli@1.0.0-beta.123 +endef +export DOCKERFILE_APIDOCS_LINT + +lint-golangci: touch internal/servers/hls/hls.min.js docker run --rm -v $(PWD):/app -w /app \ $(LINT_IMAGE) \ golangci-lint run -v + +lint-mod-tidy: + go mod tidy + git diff --exit-code + +lint-apidocs: + echo "$$DOCKERFILE_APIDOCS_LINT" | docker build . -f - -t temp + docker run --rm -v $(PWD)/apidocs:/s -w /s temp \ + sh -c "openapi lint openapi.yaml" + +lint: lint-golangci lint-mod-tidy lint-apidocs diff --git a/scripts/test-highlevel.mk b/scripts/test-highlevel.mk index 60652213549..173092a083b 100644 --- a/scripts/test-highlevel.mk +++ b/scripts/test-highlevel.mk @@ -1,6 +1,6 @@ test-highlevel-nodocker: go generate ./... - go test -v -race -tags enable_highlevel_tests ./internal/highleveltests + go test -v -race -tags enable_highlevel_tests ./internal/testhighlevel define DOCKERFILE_HIGHLEVEL_TEST FROM $(BASE_IMAGE) From 1055be99c064427112215870af95c68e500bf5ef Mon Sep 17 00:00:00 2001 From: Dan Bason Date: Mon, 5 Aug 2024 00:12:08 +1200 Subject: [PATCH 13/88] automatically reload TLS certificates when they change (#3598) * Dynamically refresh tls certs for all servers * make sure that CertLoader is always closed --------- Co-authored-by: aler9 <46489434+aler9@users.noreply.github.com> --- internal/certloader/certloader.go | 108 +++++++++++++++++++++ internal/certloader/certloader_test.go | 52 ++++++++++ internal/protocols/httpp/wrapped_server.go | 15 ++- internal/servers/rtmp/server.go | 10 +- internal/servers/rtsp/server.go | 10 +- internal/test/tls_cert.go | 52 ++++++++++ 6 files changed, 239 insertions(+), 8 deletions(-) create mode 100644 internal/certloader/certloader.go create mode 100644 internal/certloader/certloader_test.go diff --git a/internal/certloader/certloader.go b/internal/certloader/certloader.go new file mode 100644 index 00000000000..8aade6e4f5b --- /dev/null +++ b/internal/certloader/certloader.go @@ -0,0 +1,108 @@ +// Package certloader contains a certicate loader. +package certloader + +import ( + "crypto/tls" + "sync" + + "github.com/bluenviron/mediamtx/internal/confwatcher" + "github.com/bluenviron/mediamtx/internal/logger" +) + +// CertLoader is a certificate loader. It watches for changes to the certificate and key files. +type CertLoader struct { + log logger.Writer + certWatcher, keyWatcher *confwatcher.ConfWatcher + certPath, keyPath string + done chan struct{} + + cert *tls.Certificate + certMu sync.RWMutex +} + +// New allocates a CertLoader. +func New(certPath, keyPath string, log logger.Writer) (*CertLoader, error) { + cl := &CertLoader{ + log: log, + certPath: certPath, + keyPath: keyPath, + done: make(chan struct{}), + } + + var err error + cl.certWatcher, err = confwatcher.New(certPath) + if err != nil { + return nil, err + } + + cl.keyWatcher, err = confwatcher.New(keyPath) + if err != nil { + cl.certWatcher.Close() //nolint:errcheck + return nil, err + } + + cert, err := tls.LoadX509KeyPair(certPath, keyPath) + if err != nil { + return nil, err + } + + cl.certMu.Lock() + cl.cert = &cert + cl.certMu.Unlock() + + go cl.watch() + + return cl, nil +} + +// Close closes a CertLoader and releases any underlying resources. +func (cl *CertLoader) Close() { + close(cl.done) + cl.certWatcher.Close() //nolint:errcheck + cl.keyWatcher.Close() //nolint:errcheck + cl.certMu.Lock() + defer cl.certMu.Unlock() + cl.cert = nil +} + +// GetCertificate returns a function that returns the certificate for use in a tls.Config. +func (cl *CertLoader) GetCertificate() func(*tls.ClientHelloInfo) (*tls.Certificate, error) { + return func(_ *tls.ClientHelloInfo) (*tls.Certificate, error) { + cl.certMu.RLock() + defer cl.certMu.RUnlock() + return cl.cert, nil + } +} + +func (cl *CertLoader) watch() { + for { + select { + case <-cl.certWatcher.Watch(): + cert, err := tls.LoadX509KeyPair(cl.certPath, cl.keyPath) + if err != nil { + cl.log.Log(logger.Error, "certloader failed to load after change to %s: %s", cl.certPath, err.Error()) + continue + } + + cl.certMu.Lock() + cl.cert = &cert + cl.certMu.Unlock() + + cl.log.Log(logger.Info, "certificate reloaded after change to %s", cl.certPath) + case <-cl.keyWatcher.Watch(): + cert, err := tls.LoadX509KeyPair(cl.certPath, cl.keyPath) + if err != nil { + cl.log.Log(logger.Error, "certloader failed to load after change to %s: %s", cl.keyPath, err.Error()) + continue + } + + cl.certMu.Lock() + cl.cert = &cert + cl.certMu.Unlock() + + cl.log.Log(logger.Info, "certificate reloaded after change to %s", cl.keyPath) + case <-cl.done: + return + } + } +} diff --git a/internal/certloader/certloader_test.go b/internal/certloader/certloader_test.go new file mode 100644 index 00000000000..57819a4aa6e --- /dev/null +++ b/internal/certloader/certloader_test.go @@ -0,0 +1,52 @@ +package certloader + +import ( + "crypto/tls" + "os" + "testing" + "time" + + "github.com/bluenviron/mediamtx/internal/test" + "github.com/stretchr/testify/require" +) + +func TestCertReload(t *testing.T) { + testData, err := tls.X509KeyPair(test.TLSCertPub, test.TLSCertKey) + require.NoError(t, err) + + serverCertPath, err := test.CreateTempFile(test.TLSCertPub) + require.NoError(t, err) + defer os.Remove(serverCertPath) + + serverKeyPath, err := test.CreateTempFile(test.TLSCertKey) + require.NoError(t, err) + defer os.Remove(serverKeyPath) + + loader, err := New(serverCertPath, serverKeyPath, test.NilLogger) + require.NoError(t, err) + defer loader.Close() + + getCert := loader.GetCertificate() + require.NotNil(t, getCert) + + cert, err := getCert(nil) + require.NoError(t, err) + require.NotNil(t, cert) + require.Equal(t, &testData, cert) + + testData, err = tls.X509KeyPair(test.TLSCertPubAlt, test.TLSCertKeyAlt) + require.NoError(t, err) + + err = os.WriteFile(serverCertPath, test.TLSCertPubAlt, 0o644) + require.NoError(t, err) + + err = os.WriteFile(serverKeyPath, test.TLSCertKeyAlt, 0o644) + require.NoError(t, err) + + time.Sleep(1 * time.Second) + + cert, err = getCert(nil) + require.NoError(t, err) + require.NotNil(t, cert) + require.Equal(t, &testData, cert) +} diff --git a/internal/protocols/httpp/wrapped_server.go b/internal/protocols/httpp/wrapped_server.go index 86579ee5abb..71a15f004c3 100644 --- a/internal/protocols/httpp/wrapped_server.go +++ b/internal/protocols/httpp/wrapped_server.go @@ -10,6 +10,7 @@ import ( "net/http" "time" + "github.com/bluenviron/mediamtx/internal/certloader" "github.com/bluenviron/mediamtx/internal/logger" ) @@ -36,8 +37,9 @@ type WrappedServer struct { Handler http.Handler Parent logger.Writer - ln net.Listener - inner *http.Server + ln net.Listener + inner *http.Server + loader *certloader.CertLoader } // Initialize initializes a WrappedServer. @@ -47,13 +49,15 @@ func (s *WrappedServer) Initialize() error { if s.ServerCert == "" { return fmt.Errorf("server cert is missing") } - crt, err := tls.LoadX509KeyPair(s.ServerCert, s.ServerKey) + + var err error + s.loader, err = certloader.New(s.ServerCert, s.ServerKey, s.Parent) if err != nil { return err } tlsConfig = &tls.Config{ - Certificates: []tls.Certificate{crt}, + GetCertificate: s.loader.GetCertificate(), } } @@ -92,4 +96,7 @@ func (s *WrappedServer) Close() { ctxCancel() s.inner.Shutdown(ctx) s.ln.Close() // in case Shutdown() is called before Serve() + if s.loader != nil { + s.loader.Close() + } } diff --git a/internal/servers/rtmp/server.go b/internal/servers/rtmp/server.go index 8f492805618..574f4ded61e 100644 --- a/internal/servers/rtmp/server.go +++ b/internal/servers/rtmp/server.go @@ -12,6 +12,7 @@ import ( "github.com/google/uuid" + "github.com/bluenviron/mediamtx/internal/certloader" "github.com/bluenviron/mediamtx/internal/conf" "github.com/bluenviron/mediamtx/internal/defs" "github.com/bluenviron/mediamtx/internal/externalcmd" @@ -82,6 +83,7 @@ type Server struct { wg sync.WaitGroup ln net.Listener conns map[*conn]struct{} + loader *certloader.CertLoader // in chNewConn chan net.Conn @@ -99,13 +101,14 @@ func (s *Server) Initialize() error { return net.Listen(restrictnetwork.Restrict("tcp", s.Address)) } - cert, err := tls.LoadX509KeyPair(s.ServerCert, s.ServerKey) + var err error + s.loader, err = certloader.New(s.ServerCert, s.ServerKey, s.Parent) if err != nil { return nil, err } network, address := restrictnetwork.Restrict("tcp", s.Address) - return tls.Listen(network, address, &tls.Config{Certificates: []tls.Certificate{cert}}) + return tls.Listen(network, address, &tls.Config{GetCertificate: s.loader.GetCertificate()}) }() if err != nil { return err @@ -153,6 +156,9 @@ func (s *Server) Close() { s.Log(logger.Info, "listener is closing") s.ctxCancel() s.wg.Wait() + if s.loader != nil { + s.loader.Close() + } } func (s *Server) run() { diff --git a/internal/servers/rtsp/server.go b/internal/servers/rtsp/server.go index db0f1b4612c..a36ce12b4aa 100644 --- a/internal/servers/rtsp/server.go +++ b/internal/servers/rtsp/server.go @@ -17,6 +17,7 @@ import ( "github.com/bluenviron/gortsplib/v4/pkg/liberrors" "github.com/google/uuid" + "github.com/bluenviron/mediamtx/internal/certloader" "github.com/bluenviron/mediamtx/internal/conf" "github.com/bluenviron/mediamtx/internal/defs" "github.com/bluenviron/mediamtx/internal/externalcmd" @@ -89,6 +90,7 @@ type Server struct { mutex sync.RWMutex conns map[*gortsplib.ServerConn]*conn sessions map[*gortsplib.ServerSession]*session + loader *certloader.CertLoader } // Initialize initializes the server. @@ -118,12 +120,13 @@ func (s *Server) Initialize() error { } if s.IsTLS { - cert, err := tls.LoadX509KeyPair(s.ServerCert, s.ServerKey) + var err error + s.loader, err = certloader.New(s.ServerCert, s.ServerKey, s.Parent) if err != nil { return err } - s.srv.TLSConfig = &tls.Config{Certificates: []tls.Certificate{cert}} + s.srv.TLSConfig = &tls.Config{GetCertificate: s.loader.GetCertificate()} } err := s.srv.Start() @@ -155,6 +158,9 @@ func (s *Server) Close() { s.Log(logger.Info, "listener is closing") s.ctxCancel() s.wg.Wait() + if s.loader != nil { + s.loader.Close() + } } func (s *Server) run() { diff --git a/internal/test/tls_cert.go b/internal/test/tls_cert.go index e110dc4d4ae..a1740dd3304 100644 --- a/internal/test/tls_cert.go +++ b/internal/test/tls_cert.go @@ -53,3 +53,55 @@ y++U32uuSFiXDcSLarfIsE992MEJLSAynbF1Rsgsr3gXbGiuToJRyxbIeVy7gwzD +3K6cnKEyg+0ekYmLertRFIY6SwWmY1fyKgTvxudMcsBY7dC4xs= -----END RSA PRIVATE KEY----- `) + +// TLSCertPubAlt is the public key of an alternative test certificate. +var TLSCertPubAlt = []byte(`-----BEGIN CERTIFICATE----- +MIIDSTCCAjECFEut6ZxIOnbxi3bhrPLfPQZCLReNMA0GCSqGSIb3DQEBCwUAMGEx +CzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRl +cm5ldCBXaWRnaXRzIFB0eSBMdGQxGjAYBgNVBAMMEW1lZGlhbXR4LnRlc3QuY29t +MB4XDTI0MDgwMTIzNDY0MloXDTM0MDczMDIzNDY0MlowYTELMAkGA1UEBhMCQVUx +EzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMg +UHR5IEx0ZDEaMBgGA1UEAwwRbWVkaWFtdHgudGVzdC5jb20wggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQCzfvG9eLXKSTDBoM+cgV/ThiNRI2JY6dpQV8rK +QFQ5bkkDUDP+2Ae/IWylgLLXmozsMwjz1Pu42awmGymBuo5HDbI4bxPJNQR9qRrR +2+MvfDgmZxyhw5NfZDlVl+enxhb3FRgbHsLBy4oSoHbRUdLApVdM0Kg6r3bXzkih +EEs63boFJOkPhs5H0NX7AzXyBp2WnvB71j+7avnMwAsjJHOiTs8wkp5wvRcIZpJl +MCandUkcZShMirug7QOcR9fAr5CVKxsO/DjqEjwkslJHFfizOl3yRx6nsxvW8JUd +dforpSRj84dkHTi7k37YTiji90GsOvh0qc0MfAmeE181HIb/AgMBAAEwDQYJKoZI +hvcNAQELBQADggEBAEWkLL/7nvt3iD7BVJNHLvAS6GwuTH99vCil6TFYwVl4goht +Dur7YfzN43vUq+lAwS3Ry4ka7tH72pAMkpNFRvHOikWGmWUSDo2DcLd8iu3ruLF7 +yUg2ASQuekK0sUv4YKpAqV8gS2R4Jh4vLU+8L5iJ1XWGELbQ+H5wm4l7l+r2X6cD +/opmdV8Slfi0FlNQtflLsGoSlfZF5jHxqi3zyt8QdEf9WZt8e6JPxcx2Fq7Op51u +Qx9nosr5fLwhkx46+B/cotsbI/xPDjLF6RQ1OUpcHwg1HI6czoW4hHn33S0zstCf +BWt5Q1Mb2tGInbmbUgw3wUu/4nWoY+Mq4DKPlKs= +-----END CERTIFICATE-----`) + +// TLSCertKeyAlt is the private key of an alternative test certificate. +var TLSCertKeyAlt = []byte(`-----BEGIN RSA PRIVATE KEY----- +MIIEoQIBAAKCAQEAs37xvXi1ykkwwaDPnIFf04YjUSNiWOnaUFfKykBUOW5JA1Az +/tgHvyFspYCy15qM7DMI89T7uNmsJhspgbqORw2yOG8TyTUEfaka0dvjL3w4Jmcc +ocOTX2Q5VZfnp8YW9xUYGx7CwcuKEqB20VHSwKVXTNCoOq92185IoRBLOt26BSTp +D4bOR9DV+wM18gadlp7we9Y/u2r5zMALIyRzok7PMJKecL0XCGaSZTAmp3VJHGUo +TIq7oO0DnEfXwK+QlSsbDvw46hI8JLJSRxX4szpd8kcep7Mb1vCVHXX6K6UkY/OH +ZB04u5N+2E4o4vdBrDr4dKnNDHwJnhNfNRyG/wIDAQABAoH/WmCqV6Lv5dEnofCj +ZUO/Fdv0hf/LBS0g2SAoFRSCIM8aJ3dUUH0PaXoeINDGCMlIxT7tKXJg5jJNYhWx +g7oegw6vLe5ZiA+p5miL/uue+Jas4kLVp9DrfQLgQevt0gw4g/00pgy9adbFlTUD +a2HhPB7RIvXs8gYA6nVAT9jK1ST2pbeUgQNO4Ji4EjpPUkR2O7ISOlu5EV8Cj0eV +1Vs5B92Z7ORh7P2fFV2YBu+igd04+uYvei6slQl+F9cETvJv2Z9r37Yashvnn1in +uy/u1U4B1t4oOz81nHz6kxTixPpBOdJ6x8jLDgNGSsauJQfXT9xmB/rAr/NFq+7I +tbTNAoGBAMOgm3XXHWokmJnX9pfNj6ixNlrMuuez/yXMVwuxa2WFwAFN16tjJhBi +XOjestcvu/SRhOAMmYac5QdopJpLjO/FxO165r73eZhW/SJefyOHtfD29kHagA1u +JjcznU6tiA0O1owy6nuuaTfyVbDQj32PhVBx9ZwSI4778GFbjWl7AoGBAOrj4WCC +gTMaExpwNo+L+3VkM79YD1Obl13FcgtVoxjcoWjQeMx9D0k7adTV3xlchHFAjiD5 +Gs/MZl8+seq+GDX3mODsmJkdRQbYId4g6IesiOnQ3Ug/Y282WZRnpB5h/BMnrcCZ +VoohnATA7f96c7XtPUgZyROmh24T7UIVwVdNAoGAbeeGT276TI6g2RWWqXRIOFrP +EbYhb1kViFPDt4MGtjOtSk5EUzpRwTSxw/aRfQmJS/6RKxqJCjKNDVuB1lmJpY9z +coPwrOr1+lssvalfPkPZOLZWZWrvNBxlBfBOeUxOuh9S89MLH08+N7tC3yJc6wq9 +uBM+DF+4cHUkeF3qFY8CgYBzS+IwBj82/0CLRLNzaKnIqKPB846qYoA9NhLRv3ps +VLgiA9qXvXdIYhKDt2toPoKAOMjLJJtljpZdgB/C8wZdTyjKlzgcSEK+pk6RgyPA +nQ8jfjNwKDU9vLbh4rGrfDtIh7yBAoN5ECBOMQlh0xCDJ21iO834iFCH1t4qBxW9 +LQKBgQC36adC2Gu+FJRvx4Mkm73fLmVdFbP6Do7qNwyVVyaG80PDVrFQrlWm4Dt7 +AO9IwzaS1Lx+qmU1Fj1WfCtXuQa5nc9AzZ36TmM6+pAn8AC7PdNqc0qSdefVrIjj +zRGhUPaJV3A+sfO+xedBsAFnqNuX9oODYVGbTjuc2OWC30MGaw== +-----END RSA PRIVATE KEY----- +`) From 53e9470201f5a026c3602421e9b90a41f27d4cc0 Mon Sep 17 00:00:00 2001 From: Alessandro Ros Date: Sun, 4 Aug 2024 14:21:12 +0200 Subject: [PATCH 14/88] update dependencies (#3612) --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 56c9f0842c1..27c5cd63386 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/abema/go-mp4 v1.2.0 github.com/alecthomas/kong v0.9.0 github.com/bluenviron/gohlslib v1.4.0 - github.com/bluenviron/gortsplib/v4 v4.10.3-0.20240801095652-e2d1e6dab418 + github.com/bluenviron/gortsplib/v4 v4.10.3 github.com/bluenviron/mediacommon v1.12.2 github.com/datarhei/gosrt v0.7.0 github.com/fsnotify/fsnotify v1.7.0 diff --git a/go.sum b/go.sum index 0c3c97a1a88..d2c6f5b4558 100644 --- a/go.sum +++ b/go.sum @@ -22,8 +22,8 @@ github.com/benburkert/openpgp v0.0.0-20160410205803-c2471f86866c h1:8XZeJrs4+ZYh github.com/benburkert/openpgp v0.0.0-20160410205803-c2471f86866c/go.mod h1:x1vxHcL/9AVzuk5HOloOEPrtJY0MaalYr78afXZ+pWI= github.com/bluenviron/gohlslib v1.4.0 h1:3a9W1x8eqlxJUKt1sJCunPGtti5ALIY2ik4GU0RVe7E= github.com/bluenviron/gohlslib v1.4.0/go.mod h1:q5ZElzNw5GRbV1VEI45qkcPbKBco6BP58QEY5HyFsmo= -github.com/bluenviron/gortsplib/v4 v4.10.3-0.20240801095652-e2d1e6dab418 h1:j+qo+yB1S1KiTOKSN4ABPbJ9rNdzoT6AoH9F/+k1QjE= -github.com/bluenviron/gortsplib/v4 v4.10.3-0.20240801095652-e2d1e6dab418/go.mod h1:FE/oUV476F/paP+D2YAV2LvXDgpZef4lN5uAKAuVAsQ= +github.com/bluenviron/gortsplib/v4 v4.10.3 h1:RNJQaWGNXoqaU2AFNfmPXS3Utapzx5Q17kDEvVAHS7E= +github.com/bluenviron/gortsplib/v4 v4.10.3/go.mod h1:2TY/SIjsg3I5b2uUUd0pEramhYRoKqRxYzBDntdKMrY= github.com/bluenviron/mediacommon v1.12.2 h1:H2TUvZmB5et5C7kORFrJ9XdOAtxYipnQBJlY4XAvY1M= github.com/bluenviron/mediacommon v1.12.2/go.mod h1:HDyW2CzjvhYJXtdxstdFPio3G0qSocPhqkhUt/qffec= github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= From 541f1c78fb978fb56ce188cec10ab55e3267cb46 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 7 Aug 2024 16:32:09 +0200 Subject: [PATCH 15/88] build(deps): bump golang.org/x/term from 0.22.0 to 0.23.0 (#3626) Bumps [golang.org/x/term](https://github.com/golang/term) from 0.22.0 to 0.23.0. - [Commits](https://github.com/golang/term/compare/v0.22.0...v0.23.0) --- updated-dependencies: - dependency-name: golang.org/x/term dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 27c5cd63386..0cf4358a190 100644 --- a/go.mod +++ b/go.mod @@ -29,8 +29,8 @@ require ( github.com/pion/webrtc/v3 v3.2.22 github.com/stretchr/testify v1.9.0 golang.org/x/crypto v0.25.0 - golang.org/x/sys v0.22.0 - golang.org/x/term v0.22.0 + golang.org/x/sys v0.23.0 + golang.org/x/term v0.23.0 gopkg.in/yaml.v2 v2.4.0 ) diff --git a/go.sum b/go.sum index d2c6f5b4558..a6a93dc1b58 100644 --- a/go.sum +++ b/go.sum @@ -262,8 +262,8 @@ golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= -golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= +golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -273,8 +273,8 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= -golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= -golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= +golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= +golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= From e7dc977ab99acfb8bcaa4f93e4ac3fcecf2bc449 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 7 Aug 2024 16:32:24 +0200 Subject: [PATCH 16/88] build(deps): bump github.com/pion/interceptor from 0.1.29 to 0.1.30 (#3627) Bumps [github.com/pion/interceptor](https://github.com/pion/interceptor) from 0.1.29 to 0.1.30. - [Release notes](https://github.com/pion/interceptor/releases) - [Changelog](https://github.com/pion/interceptor/blob/master/.goreleaser.yml) - [Commits](https://github.com/pion/interceptor/compare/v0.1.29...v0.1.30) --- updated-dependencies: - dependency-name: github.com/pion/interceptor dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 4 ++-- go.sum | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/go.mod b/go.mod index 0cf4358a190..83707d6caa3 100644 --- a/go.mod +++ b/go.mod @@ -21,10 +21,10 @@ require ( github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 github.com/matthewhartstonge/argon2 v1.0.0 github.com/pion/ice/v2 v2.3.24 - github.com/pion/interceptor v0.1.29 + github.com/pion/interceptor v0.1.30 github.com/pion/logging v0.2.2 github.com/pion/rtcp v1.2.14 - github.com/pion/rtp v1.8.7-0.20240429002300-bc5124c9d0d0 + github.com/pion/rtp v1.8.8 github.com/pion/sdp/v3 v3.0.9 github.com/pion/webrtc/v3 v3.2.22 github.com/stretchr/testify v1.9.0 diff --git a/go.sum b/go.sum index a6a93dc1b58..4f2d3c8f06c 100644 --- a/go.sum +++ b/go.sum @@ -137,8 +137,8 @@ github.com/pion/datachannel v1.5.5 h1:10ef4kwdjije+M9d7Xm9im2Y3O6A6ccQb0zcqZcJew github.com/pion/datachannel v1.5.5/go.mod h1:iMz+lECmfdCMqFRhXhcA/219B0SQlbpoR2V118yimL0= github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8= github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= -github.com/pion/interceptor v0.1.29 h1:39fsnlP1U8gw2JzOFWdfCU82vHvhW9o0rZnZF56wF+M= -github.com/pion/interceptor v0.1.29/go.mod h1:ri+LGNjRUc5xUNtDEPzfdkmSqISixVTBF/z/Zms/6T4= +github.com/pion/interceptor v0.1.30 h1:au5rlVHsgmxNi+v/mjOPazbW1SHzfx7/hYOEYQnUcxA= +github.com/pion/interceptor v0.1.30/go.mod h1:RQuKT5HTdkP2Fi0cuOS5G5WNymTjzXaGF75J4k7z2nc= github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= github.com/pion/mdns v0.0.12 h1:CiMYlY+O0azojWDmxdNr7ADGrnZ+V6Ilfner+6mSVK8= @@ -149,8 +149,8 @@ github.com/pion/rtcp v1.2.12/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9 github.com/pion/rtcp v1.2.14 h1:KCkGV3vJ+4DAJmvP0vaQShsb0xkRfWkO540Gy102KyE= github.com/pion/rtcp v1.2.14/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4= github.com/pion/rtp v1.8.3/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU= -github.com/pion/rtp v1.8.7-0.20240429002300-bc5124c9d0d0 h1:yPAphilskTN7U3URvBVxlVr0PzheMeWqo7PaOqh//Hg= -github.com/pion/rtp v1.8.7-0.20240429002300-bc5124c9d0d0/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU= +github.com/pion/rtp v1.8.8 h1:EtYFHI0rpUEjT/RMnGfb1vdJhbYmPG77szD72uUnSxs= +github.com/pion/rtp v1.8.8/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU= github.com/pion/sctp v1.8.5/go.mod h1:SUFFfDpViyKejTAdwD1d/HQsCu+V/40cCs2nZIvC3s0= github.com/pion/sctp v1.8.16 h1:PKrMs+o9EMLRvFfXq59WFsC+V8mN1wnKzqrv+3D/gYY= github.com/pion/sctp v1.8.16/go.mod h1:P6PbDVA++OJMrVNg2AL3XtYHV4uD6dvfyOovCgMs0PE= @@ -168,8 +168,8 @@ github.com/pion/transport/v2 v2.2.3/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLh github.com/pion/transport/v2 v2.2.4 h1:41JJK6DZQYSeVLxILA2+F4ZkKb4Xd/tFJZRFZQ9QAlo= github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0= github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0= -github.com/pion/transport/v3 v3.0.2 h1:r+40RJR25S9w3jbA6/5uEPTzcdn7ncyU44RWCbHkLg4= -github.com/pion/transport/v3 v3.0.2/go.mod h1:nIToODoOlb5If2jF9y2Igfx3PFYWfuXi37m0IlWa/D0= +github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0= +github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo= github.com/pion/turn/v2 v2.1.3 h1:pYxTVWG2gpC97opdRc5IGsQ1lJ9O/IlNhkzj7MMrGAA= github.com/pion/turn/v2 v2.1.3/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY= github.com/pkg/profile v1.4.0/go.mod h1:NWz/XGvpEW1FyYQ7fCx4dqYBLlfTcE+A9FLAkNKqjFE= From 01f1d276708840812518fea5d82329a8efeb89eb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 7 Aug 2024 16:33:50 +0200 Subject: [PATCH 17/88] bump hls.js to v1.5.14 (#3624) Co-authored-by: mediamtx-bot --- internal/servers/hls/hlsjsdownloader/HASH | 2 +- internal/servers/hls/hlsjsdownloader/VERSION | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/servers/hls/hlsjsdownloader/HASH b/internal/servers/hls/hlsjsdownloader/HASH index 894fba527b2..2877a954761 100644 --- a/internal/servers/hls/hlsjsdownloader/HASH +++ b/internal/servers/hls/hlsjsdownloader/HASH @@ -1 +1 @@ -c5ef2cf356b103bf7a19dd4d14257c9e00163551ed03bbf96bf22a12458a1250 +7a9def412c7d62f281f4c7e94921fa754644dc6a304f2a096bf3fdddff256470 diff --git a/internal/servers/hls/hlsjsdownloader/VERSION b/internal/servers/hls/hlsjsdownloader/VERSION index 77fb9c77172..0ecc386a036 100644 --- a/internal/servers/hls/hlsjsdownloader/VERSION +++ b/internal/servers/hls/hlsjsdownloader/VERSION @@ -1 +1 @@ -v1.5.13 +v1.5.14 From 3ff296d1005899fcf2e3958bc67ed241d41dc5ba Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 10 Aug 2024 11:01:41 +0200 Subject: [PATCH 18/88] build(deps): bump golang.org/x/sys from 0.23.0 to 0.24.0 (#3643) Bumps [golang.org/x/sys](https://github.com/golang/sys) from 0.23.0 to 0.24.0. - [Commits](https://github.com/golang/sys/compare/v0.23.0...v0.24.0) --- updated-dependencies: - dependency-name: golang.org/x/sys dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 83707d6caa3..8c09fc3db88 100644 --- a/go.mod +++ b/go.mod @@ -29,7 +29,7 @@ require ( github.com/pion/webrtc/v3 v3.2.22 github.com/stretchr/testify v1.9.0 golang.org/x/crypto v0.25.0 - golang.org/x/sys v0.23.0 + golang.org/x/sys v0.24.0 golang.org/x/term v0.23.0 gopkg.in/yaml.v2 v2.4.0 ) diff --git a/go.sum b/go.sum index 4f2d3c8f06c..08d9371f50c 100644 --- a/go.sum +++ b/go.sum @@ -262,8 +262,8 @@ golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= -golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= From 6225706c82b21ac70c43aa725098cdea69528d08 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 10 Aug 2024 11:05:00 +0200 Subject: [PATCH 19/88] build(deps): bump golang.org/x/crypto from 0.25.0 to 0.26.0 (#3628) Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.25.0 to 0.26.0. - [Commits](https://github.com/golang/crypto/compare/v0.25.0...v0.26.0) --- updated-dependencies: - dependency-name: golang.org/x/crypto dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 8c09fc3db88..037a9cda8c0 100644 --- a/go.mod +++ b/go.mod @@ -28,7 +28,7 @@ require ( github.com/pion/sdp/v3 v3.0.9 github.com/pion/webrtc/v3 v3.2.22 github.com/stretchr/testify v1.9.0 - golang.org/x/crypto v0.25.0 + golang.org/x/crypto v0.26.0 golang.org/x/sys v0.24.0 golang.org/x/term v0.23.0 gopkg.in/yaml.v2 v2.4.0 @@ -71,7 +71,7 @@ require ( github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect golang.org/x/arch v0.8.0 // indirect golang.org/x/net v0.27.0 // indirect - golang.org/x/text v0.16.0 // indirect + golang.org/x/text v0.17.0 // indirect golang.org/x/time v0.5.0 // indirect google.golang.org/protobuf v1.34.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 08d9371f50c..a0abd1f75d3 100644 --- a/go.sum +++ b/go.sum @@ -210,8 +210,8 @@ golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= -golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= -golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= +golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= +golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= @@ -285,8 +285,8 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= From c8f1fa93f576e4040267b4e956891ec0562a5cb8 Mon Sep 17 00:00:00 2001 From: Yang-Sang-Ho <106420760+San9H0@users.noreply.github.com> Date: Sat, 10 Aug 2024 18:15:17 +0900 Subject: [PATCH 20/88] remove whitespace from hashBuf (#3622) * remove whitespace from hashBuf * merge bytes.TrimSpace and strings.TrimSpace --------- Co-authored-by: aler9 <46489434+aler9@users.noreply.github.com> --- internal/servers/hls/hlsjsdownloader/main.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/internal/servers/hls/hlsjsdownloader/main.go b/internal/servers/hls/hlsjsdownloader/main.go index 7b9f3357208..2f1fb557324 100644 --- a/internal/servers/hls/hlsjsdownloader/main.go +++ b/internal/servers/hls/hlsjsdownloader/main.go @@ -44,9 +44,11 @@ func do() error { if err != nil { return err } - hash := make([]byte, hex.DecodedLen(len(hashBuf))) - if _, err = hex.Decode(hash, bytes.TrimSpace(hashBuf)); err != nil { + str := strings.TrimSpace(string(hashBuf)) + + hash, err := hex.DecodeString(str) + if err != nil { return err } From 1e7a7d16743cd435e1fa4bad197b7b4353705896 Mon Sep 17 00:00:00 2001 From: Alessandro Ros Date: Sun, 11 Aug 2024 23:34:12 +0200 Subject: [PATCH 21/88] rpi camera: update font and link it to IBM repository (#3647) --- .dockerignore | 2 +- .gitignore | 2 +- README.md | 2 +- internal/protocols/rpicamera/exe/Makefile | 8 ++++++++ internal/protocols/rpicamera/exe/text_font.ttf | Bin 129916 -> 0 bytes scripts/binaries.mk | 4 ++-- 6 files changed, 13 insertions(+), 5 deletions(-) delete mode 100644 internal/protocols/rpicamera/exe/text_font.ttf diff --git a/.dockerignore b/.dockerignore index f327c60aa53..516b04886cd 100644 --- a/.dockerignore +++ b/.dockerignore @@ -4,5 +4,5 @@ /coverage*.txt /apidocs/*.html /internal/servers/hls/hls.min.js -/internal/protocols/rpicamera/exe/text_font.h +/internal/protocols/rpicamera/exe/text_font.* /internal/protocols/rpicamera/exe/exe diff --git a/.gitignore b/.gitignore index 66f361215d0..cb096347a7f 100644 --- a/.gitignore +++ b/.gitignore @@ -3,5 +3,5 @@ /coverage*.txt /apidocs/*.html /internal/servers/hls/hls.min.js -/internal/protocols/rpicamera/exe/text_font.h +/internal/protocols/rpicamera/exe/text_font.* /internal/protocols/rpicamera/exe/exe diff --git a/README.md b/README.md index a931ce0f334..b62da770556 100644 --- a/README.md +++ b/README.md @@ -472,7 +472,7 @@ The resulting stream will be available in path `/cam`. _MediaMTX_ natively supports the Raspberry Pi Camera, enabling high-quality and low-latency video streaming from the camera to any user, for any purpose. There are a couple of requirements: -1. The server must run on a Raspberry Pi, with Raspberry Pi OS bullseye or newer as operative system. Both 32 bit and 64 bit operative systems are supported. +1. The server must run on a Raspberry Pi, with Raspberry Pi OS Bullseye as operative system. Both 32 bit and 64 bit architectures are supported. 2. Make sure that the legacy camera stack is disabled. Type `sudo raspi-config`, then go to `Interfacing options`, `enable/disable legacy camera support`, choose `no`. Reboot the system. diff --git a/internal/protocols/rpicamera/exe/Makefile b/internal/protocols/rpicamera/exe/Makefile index d2a19012ddc..65c5e796478 100644 --- a/internal/protocols/rpicamera/exe/Makefile +++ b/internal/protocols/rpicamera/exe/Makefile @@ -36,6 +36,14 @@ OBJS = \ all: exe +TEXT_FONT_URL = https://github.com/IBM/plex/raw/v6.4.2/IBM-Plex-Mono/fonts/complete/ttf/IBMPlexMono-Medium.ttf +TEXT_FONT_SHA256 = 0bede3debdea8488bbb927f8f0650d915073209734a67fe8cd5a3320b572511c + +text_font.ttf: + wget -O text_font.tmp $(TEXT_FONT_URL) + H=$$(sha256sum text_font.tmp | awk '{ print $$1 }'); [ "$$H" = "$(TEXT_FONT_SHA256)" ] || { echo "hash mismatch; got $$H, expected $(TEXT_FONT_SHA256)"; exit 1; } + mv text_font.tmp $@ + text_font.h: text_font.ttf xxd --include $< > text_font.h diff --git a/internal/protocols/rpicamera/exe/text_font.ttf b/internal/protocols/rpicamera/exe/text_font.ttf deleted file mode 100644 index 54927d50fdd603f47e231a56c9a1600ce919f27d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 129916 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 diff --git a/scripts/binaries.mk b/scripts/binaries.mk index c9693fc306d..b49d755cbd0 100644 --- a/scripts/binaries.mk +++ b/scripts/binaries.mk @@ -3,14 +3,14 @@ BINARY_NAME = mediamtx define DOCKERFILE_BINARIES FROM $(RPI32_IMAGE) AS rpicamera32 RUN ["cross-build-start"] -RUN apt update && apt install -y --no-install-recommends g++ pkg-config make libcamera-dev libfreetype-dev xxd +RUN apt update && apt install -y --no-install-recommends g++ pkg-config make libcamera-dev libfreetype-dev xxd wget WORKDIR /s/internal/protocols/rpicamera/exe COPY internal/protocols/rpicamera/exe . RUN make -j$$(nproc) FROM $(RPI64_IMAGE) AS rpicamera64 RUN ["cross-build-start"] -RUN apt update && apt install -y --no-install-recommends g++ pkg-config make libcamera-dev libfreetype-dev xxd +RUN apt update && apt install -y --no-install-recommends g++ pkg-config make libcamera-dev libfreetype-dev xxd wget WORKDIR /s/internal/protocols/rpicamera/exe COPY internal/protocols/rpicamera/exe . RUN make -j$$(nproc) From f9913b8a2ab40efb2f380e7110737de18296783b Mon Sep 17 00:00:00 2001 From: Alessandro Ros Date: Tue, 13 Aug 2024 11:43:22 +0200 Subject: [PATCH 22/88] simplify rpi / microphone instructions (#3650) --- README.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index b62da770556..567479ec439 100644 --- a/README.md +++ b/README.md @@ -544,19 +544,20 @@ default:CARD=U0x46d0x809 Default Audio Device ``` -Find the audio card of the microfone and take note of its name, for instance `default:CARD=U0x46d0x809`. Then use GStreamer inside `runOnReady` to read the video stream, add audio and publish the new stream to another path: +Find the audio card of the microfone and take note of its name, for instance `default:CARD=U0x46d0x809`. Then create a new path that takes the video stream from the camera and audio from the microphone: ```yml paths: cam: source: rpiCamera - runOnReady: > + + cam_with_audio: + runOnInit: > gst-launch-1.0 rtspclientsink name=s location=rtsp://localhost:$RTSP_PORT/cam_with_audio - rtspsrc location=rtsp://127.0.0.1:$RTSP_PORT/$MTX_PATH latency=0 ! rtph264depay ! s. + rtspsrc location=rtsp://127.0.0.1:$RTSP_PORT/cam latency=0 ! rtph264depay ! s. alsasrc device=default:CARD=U0x46d0x809 ! opusenc bitrate=16000 ! s. - runOnReadyRestart: yes - cam_with_audio: + runOnInitRestart: yes ``` The resulting stream will be available in path `/cam_with_audio`. From adf740098aabcb42128a68d0c1346a781ca2e5d2 Mon Sep 17 00:00:00 2001 From: Alessandro Ros Date: Tue, 13 Aug 2024 11:55:00 +0200 Subject: [PATCH 23/88] raise error in case of duplicate params in the configuration (#3593) (#3651) --- internal/conf/conf_test.go | 6 ++++++ internal/conf/yaml/load.go | 5 ++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/internal/conf/conf_test.go b/internal/conf/conf_test.go index 0c26086cfa6..f0d1529ad85 100644 --- a/internal/conf/conf_test.go +++ b/internal/conf/conf_test.go @@ -258,6 +258,12 @@ func TestConfErrors(t *testing.T) { conf string err string }{ + { + "duplicate parameter", + "paths:\n" + + "paths:\n", + "yaml: unmarshal errors:\n line 2: key \"paths\" already set in map", + }, { "non existent parameter 1", `invalid: param`, diff --git a/internal/conf/yaml/load.go b/internal/conf/yaml/load.go index 33abe950b4f..7c4c40853e7 100644 --- a/internal/conf/yaml/load.go +++ b/internal/conf/yaml/load.go @@ -44,8 +44,11 @@ func convertKeys(i interface{}) (interface{}, error) { // Load loads the configuration from Yaml. func Load(buf []byte, dest interface{}) error { // load YAML into a generic map + // from documentation: + // "UnmarshalStrict is like Unmarshal except that any fields that are found in the data + // that do not have corresponding struct members, or mapping keys that are duplicates, will result in an error." var temp interface{} - err := yaml.Unmarshal(buf, &temp) + err := yaml.UnmarshalStrict(buf, &temp) if err != nil { return err } From 7ceb5bafce4e45255b17e28f92c95fc4f95f8070 Mon Sep 17 00:00:00 2001 From: Roy Veshovda Date: Wed, 14 Aug 2024 09:49:27 +0200 Subject: [PATCH 24/88] Bump rpi camera base image (#3515) --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 5414070daaa..268bda76711 100644 --- a/Makefile +++ b/Makefile @@ -2,8 +2,8 @@ BASE_IMAGE = golang:1.22-alpine3.19 LINT_IMAGE = golangci/golangci-lint:v1.59.1 NODE_IMAGE = node:20-alpine3.19 ALPINE_IMAGE = alpine:3.19 -RPI32_IMAGE = balenalib/raspberry-pi:bullseye-run-20230712 -RPI64_IMAGE = balenalib/raspberrypi3-64:bullseye-run-20230530 +RPI32_IMAGE = balenalib/raspberry-pi:bullseye-run-20240508 +RPI64_IMAGE = balenalib/raspberrypi3-64:bullseye-run-20240429 .PHONY: $(shell ls) From 4f54ea8b7ed7639a3c0b08264d2e4d3f3b0867d9 Mon Sep 17 00:00:00 2001 From: Alessandro Ros Date: Wed, 14 Aug 2024 21:07:51 +0200 Subject: [PATCH 25/88] update actions/checkout (#3655) --- .github/workflows/bump_hls_js.yml | 2 +- .github/workflows/code_lint.yml | 6 +++--- .github/workflows/code_test.yml | 6 +++--- .github/workflows/issue_lint.yml | 2 +- .github/workflows/nightly_binaries.yml | 2 +- .github/workflows/release.yml | 8 ++++---- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/bump_hls_js.yml b/.github/workflows/bump_hls_js.yml index 10b14d1573b..3d45e961a24 100644 --- a/.github/workflows/bump_hls_js.yml +++ b/.github/workflows/bump_hls_js.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 diff --git a/.github/workflows/code_lint.yml b/.github/workflows/code_lint.yml index 0c77515467c..069d68b4965 100644 --- a/.github/workflows/code_lint.yml +++ b/.github/workflows/code_lint.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-go@v3 with: @@ -27,7 +27,7 @@ jobs: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-go@v2 with: @@ -39,6 +39,6 @@ jobs: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - run: make lint-apidocs diff --git a/.github/workflows/code_test.yml b/.github/workflows/code_test.yml index bb5a1409f45..723282b016c 100644 --- a/.github/workflows/code_test.yml +++ b/.github/workflows/code_test.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - run: make test @@ -23,7 +23,7 @@ jobs: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - run: make test32 @@ -31,7 +31,7 @@ jobs: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-go@v2 with: diff --git a/.github/workflows/issue_lint.yml b/.github/workflows/issue_lint.yml index 1f9cd95dee8..e4becd26e01 100644 --- a/.github/workflows/issue_lint.yml +++ b/.github/workflows/issue_lint.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/github-script@v6 with: diff --git a/.github/workflows/nightly_binaries.yml b/.github/workflows/nightly_binaries.yml index 419a8dc071c..8d15c96a04c 100644 --- a/.github/workflows/nightly_binaries.yml +++ b/.github/workflows/nightly_binaries.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - run: make binaries diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b375609c39f..467d5656f11 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - run: make binaries @@ -106,7 +106,7 @@ jobs: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/download-artifact@v3 with: @@ -123,7 +123,7 @@ jobs: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - run: make dockerhub-legacy env: @@ -135,7 +135,7 @@ jobs: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - run: make apidocs From c5059fa7a08bd6bb79a27fc60e6b36963a804682 Mon Sep 17 00:00:00 2001 From: Alessandro Ros Date: Wed, 14 Aug 2024 23:24:17 +0200 Subject: [PATCH 26/88] move RPI Camera component into dedicated repository (#3656) --- .dockerignore | 3 +- .gitignore | 3 +- README.md | 20 - internal/protocols/rpicamera/exe/Makefile | 57 -- internal/protocols/rpicamera/exe/base64.c | 87 --- internal/protocols/rpicamera/exe/base64.h | 6 - internal/protocols/rpicamera/exe/camera.cpp | 499 ------------------ internal/protocols/rpicamera/exe/camera.h | 31 -- internal/protocols/rpicamera/exe/encoder.c | 337 ------------ internal/protocols/rpicamera/exe/encoder.h | 15 - internal/protocols/rpicamera/exe/main.c | 116 ---- internal/protocols/rpicamera/exe/parameters.c | 210 -------- internal/protocols/rpicamera/exe/parameters.h | 64 --- internal/protocols/rpicamera/exe/pipe.c | 44 -- internal/protocols/rpicamera/exe/pipe.h | 12 - .../protocols/rpicamera/exe/sensor_mode.c | 27 - .../protocols/rpicamera/exe/sensor_mode.h | 15 - internal/protocols/rpicamera/exe/text.c | 171 ------ internal/protocols/rpicamera/exe/text.h | 15 - internal/protocols/rpicamera/exe/window.c | 30 -- internal/protocols/rpicamera/exe/window.h | 15 - .../protocols/rpicamera/rpicamera_disabled.go | 33 -- internal/servers/hls/hlsjsdownloader/main.go | 6 +- .../rpicamera/camera.go} | 28 +- .../rpicamera/camera_disabled.go | 24 + internal/staticsources/rpicamera/component.go | 3 + .../staticsources/rpicamera/component_32.go | 11 + .../staticsources/rpicamera/component_64.go | 11 + .../rpicamera/mtxrpicamdownloader/VERSION | 1 + .../rpicamera/mtxrpicamdownloader/main.go | 53 ++ .../rpicamera/params.go | 47 +- .../rpicamera/params_serialize.go | 48 ++ .../rpicamera/pipe.go | 4 +- internal/staticsources/rpicamera/source.go | 13 +- scripts/binaries.mk | 41 +- 35 files changed, 189 insertions(+), 1911 deletions(-) delete mode 100644 internal/protocols/rpicamera/exe/Makefile delete mode 100644 internal/protocols/rpicamera/exe/base64.c delete mode 100644 internal/protocols/rpicamera/exe/base64.h delete mode 100644 internal/protocols/rpicamera/exe/camera.cpp delete mode 100644 internal/protocols/rpicamera/exe/camera.h delete mode 100644 internal/protocols/rpicamera/exe/encoder.c delete mode 100644 internal/protocols/rpicamera/exe/encoder.h delete mode 100644 internal/protocols/rpicamera/exe/main.c delete mode 100644 internal/protocols/rpicamera/exe/parameters.c delete mode 100644 internal/protocols/rpicamera/exe/parameters.h delete mode 100644 internal/protocols/rpicamera/exe/pipe.c delete mode 100644 internal/protocols/rpicamera/exe/pipe.h delete mode 100644 internal/protocols/rpicamera/exe/sensor_mode.c delete mode 100644 internal/protocols/rpicamera/exe/sensor_mode.h delete mode 100644 internal/protocols/rpicamera/exe/text.c delete mode 100644 internal/protocols/rpicamera/exe/text.h delete mode 100644 internal/protocols/rpicamera/exe/window.c delete mode 100644 internal/protocols/rpicamera/exe/window.h delete mode 100644 internal/protocols/rpicamera/rpicamera_disabled.go rename internal/{protocols/rpicamera/rpicamera.go => staticsources/rpicamera/camera.go} (88%) create mode 100644 internal/staticsources/rpicamera/camera_disabled.go create mode 100644 internal/staticsources/rpicamera/component.go create mode 100644 internal/staticsources/rpicamera/component_32.go create mode 100644 internal/staticsources/rpicamera/component_64.go create mode 100644 internal/staticsources/rpicamera/mtxrpicamdownloader/VERSION create mode 100644 internal/staticsources/rpicamera/mtxrpicamdownloader/main.go rename internal/{protocols => staticsources}/rpicamera/params.go (53%) create mode 100644 internal/staticsources/rpicamera/params_serialize.go rename internal/{protocols => staticsources}/rpicamera/pipe.go (93%) diff --git a/.dockerignore b/.dockerignore index 516b04886cd..5be57d00abe 100644 --- a/.dockerignore +++ b/.dockerignore @@ -4,5 +4,4 @@ /coverage*.txt /apidocs/*.html /internal/servers/hls/hls.min.js -/internal/protocols/rpicamera/exe/text_font.* -/internal/protocols/rpicamera/exe/exe +/internal/staticsources/rpicamera/mtxrpicam_* diff --git a/.gitignore b/.gitignore index cb096347a7f..eba111852e5 100644 --- a/.gitignore +++ b/.gitignore @@ -3,5 +3,4 @@ /coverage*.txt /apidocs/*.html /internal/servers/hls/hls.min.js -/internal/protocols/rpicamera/exe/text_font.* -/internal/protocols/rpicamera/exe/exe +/internal/staticsources/rpicamera/mtxrpicam_* diff --git a/README.md b/README.md index 567479ec439..c5b86d619ee 100644 --- a/README.md +++ b/README.md @@ -144,7 +144,6 @@ _rtsp-simple-server_ has been rebranded as _MediaMTX_. The reason is pretty obvi * [Encryption](#encryption-1) * [Compile from source](#compile-from-source) * [Standard](#standard) - * [Raspberry Pi](#raspberry-pi) * [OpenWrt](#openwrt-1) * [Cross compile](#cross-compile) * [Compile for all supported platforms](#compile-for-all-supported-platforms) @@ -2047,25 +2046,6 @@ CGO_ENABLED=0 go build . The command will produce the `mediamtx` binary. -### Raspberry Pi - -The server can be compiled with native support for the Raspberry Pi Camera. Compilation must be performed on a Raspberry Pi, with the following dependencies: - -* Go ≥ 1.22 -* `libcamera-dev` -* `libfreetype-dev` -* `xxd` - -Download the repository, open a terminal in it and run: - -```sh -make -C internal/protocols/rpicamera/exe -j$(nproc) -go generate ./... -go build -tags rpicamera . -``` - -The command will produce the `mediamtx` binary. - ### OpenWrt The compilation procedure is the same as the standard one. On the OpenWrt device, install git and Go: diff --git a/internal/protocols/rpicamera/exe/Makefile b/internal/protocols/rpicamera/exe/Makefile deleted file mode 100644 index 65c5e796478..00000000000 --- a/internal/protocols/rpicamera/exe/Makefile +++ /dev/null @@ -1,57 +0,0 @@ -CFLAGS = \ - -Ofast \ - -Werror \ - -Wall \ - -Wextra \ - -Wno-unused-parameter \ - -Wno-unused-result \ - $$(pkg-config --cflags freetype2) - -CXXFLAGS = \ - -Ofast \ - -Werror \ - -Wall \ - -Wextra \ - -Wno-unused-parameter \ - -Wno-unused-result \ - -std=c++17 \ - $$(pkg-config --cflags libcamera) - -LDFLAGS = \ - -s \ - -pthread \ - $$(pkg-config --libs freetype2) \ - $$(pkg-config --libs libcamera) - -OBJS = \ - base64.o \ - camera.o \ - encoder.o \ - main.o \ - parameters.o \ - pipe.o \ - sensor_mode.o \ - text.o \ - window.o - -all: exe - -TEXT_FONT_URL = https://github.com/IBM/plex/raw/v6.4.2/IBM-Plex-Mono/fonts/complete/ttf/IBMPlexMono-Medium.ttf -TEXT_FONT_SHA256 = 0bede3debdea8488bbb927f8f0650d915073209734a67fe8cd5a3320b572511c - -text_font.ttf: - wget -O text_font.tmp $(TEXT_FONT_URL) - H=$$(sha256sum text_font.tmp | awk '{ print $$1 }'); [ "$$H" = "$(TEXT_FONT_SHA256)" ] || { echo "hash mismatch; got $$H, expected $(TEXT_FONT_SHA256)"; exit 1; } - mv text_font.tmp $@ - -text_font.h: text_font.ttf - xxd --include $< > text_font.h - -%.o: %.c text_font.h - $(CC) $(CFLAGS) -c $< -o $@ - -%.o: %.cpp - $(CXX) $(CXXFLAGS) -c $< -o $@ - -exe: $(OBJS) - $(CXX) $^ $(LDFLAGS) -o $@ diff --git a/internal/protocols/rpicamera/exe/base64.c b/internal/protocols/rpicamera/exe/base64.c deleted file mode 100644 index 5079bb21e81..00000000000 --- a/internal/protocols/rpicamera/exe/base64.c +++ /dev/null @@ -1,87 +0,0 @@ -#include -#include -#include -#include - -#include "base64.h" - -static const unsigned char decoding_table[256] = { - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, 0x3f, - 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, - 0x3c, 0x3d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, - 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, - 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, - 0x17, 0x18, 0x19, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, - 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, - 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30, - 0x31, 0x32, 0x33, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, -}; - -char* base64_decode(const char *data) { - size_t input_length = strlen(data); - if (input_length % 4 != 0) { - return NULL; - } - - size_t output_length = input_length / 4 * 3; - - if (data[input_length - 1] == '=') { - (output_length)--; - } - if (data[input_length - 2] == '=') { - (output_length)--; - } - - unsigned char* output = (unsigned char *)malloc(output_length + 1); - if (output == NULL) { - return NULL; - } - - for (int i = 0, j = 0; i < (int)input_length;) { - uint32_t sextet_a = (data[i] == '=') ? (0 & i++) : decoding_table[(unsigned char)(data[i++])]; - uint32_t sextet_b = (data[i] == '=') ? (0 & i++) : decoding_table[(unsigned char)(data[i++])]; - uint32_t sextet_c = (data[i] == '=') ? (0 & i++) : decoding_table[(unsigned char)(data[i++])]; - uint32_t sextet_d = (data[i] == '=') ? (0 & i++) : decoding_table[(unsigned char)(data[i++])]; - - uint32_t triple = (sextet_a << 3 * 6) - + (sextet_b << 2 * 6) - + (sextet_c << 1 * 6) - + (sextet_d << 0 * 6); - - if (j < (int)output_length) { - output[j++] = (triple >> 2 * 8) & 0xFF; - } - if (j < (int)output_length) { - output[j++] = (triple >> 1 * 8) & 0xFF; - } - if (j < (int)output_length) { - output[j++] = (triple >> 0 * 8) & 0xFF; - } - } - - output[output_length] = 0x00; - return (char *)output; -}; diff --git a/internal/protocols/rpicamera/exe/base64.h b/internal/protocols/rpicamera/exe/base64.h deleted file mode 100644 index 5a656aed732..00000000000 --- a/internal/protocols/rpicamera/exe/base64.h +++ /dev/null @@ -1,6 +0,0 @@ -#ifndef __BASE64_H__ -#define __BASE64_H__ - -char* base64_decode(const char *data); - -#endif diff --git a/internal/protocols/rpicamera/exe/camera.cpp b/internal/protocols/rpicamera/exe/camera.cpp deleted file mode 100644 index 73701a61dce..00000000000 --- a/internal/protocols/rpicamera/exe/camera.cpp +++ /dev/null @@ -1,499 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "camera.h" - -using libcamera::CameraManager; -using libcamera::CameraConfiguration; -using libcamera::Camera; -using libcamera::ColorSpace; -using libcamera::ControlList; -using libcamera::FrameBufferAllocator; -using libcamera::FrameBuffer; -using libcamera::PixelFormat; -using libcamera::Rectangle; -using libcamera::Request; -using libcamera::Size; -using libcamera::Span; -using libcamera::Stream; -using libcamera::StreamRole; -using libcamera::StreamConfiguration; -using libcamera::Transform; - -namespace controls = libcamera::controls; -namespace formats = libcamera::formats; -namespace properties = libcamera::properties; - -static char errbuf[256]; - -static void set_error(const char *format, ...) { - va_list args; - va_start(args, format); - vsnprintf(errbuf, 256, format, args); -} - -const char *camera_get_error() { - return errbuf; -} - -// https://github.com/raspberrypi/libcamera-apps/blob/dd97618a25523c2c4aa58f87af5f23e49aa6069c/core/libcamera_app.cpp#L42 -static PixelFormat mode_to_pixel_format(sensor_mode_t *mode) { - static std::vector, PixelFormat>> table = { - { {8, false}, formats::SBGGR8 }, - { {8, true}, formats::SBGGR8 }, - { {10, false}, formats::SBGGR10 }, - { {10, true}, formats::SBGGR10_CSI2P }, - { {12, false}, formats::SBGGR12 }, - { {12, true}, formats::SBGGR12_CSI2P }, - }; - - auto it = std::find_if(table.begin(), table.end(), [&mode] (auto &m) { - return mode->bit_depth == m.first.first && mode->packed == m.first.second; }); - if (it != table.end()) { - return it->second; - } - - return formats::SBGGR12_CSI2P; -} - -struct CameraPriv { - const parameters_t *params; - camera_frame_cb frame_cb; - std::unique_ptr camera_manager; - std::shared_ptr camera; - Stream *video_stream; - std::unique_ptr allocator; - std::vector> requests; - std::mutex ctrls_mutex; - std::unique_ptr ctrls; - std::map mapped_buffers; -}; - -static int get_v4l2_colorspace(std::optional const &cs) { - if (cs == ColorSpace::Rec709) { - return V4L2_COLORSPACE_REC709; - } - return V4L2_COLORSPACE_SMPTE170M; -} - -// https://github.com/raspberrypi/libcamera-apps/blob/a5b5506a132056ac48ba22bc581cc394456da339/core/libcamera_app.cpp#L824 -static uint8_t *map_buffer(FrameBuffer *buffer) { - size_t buffer_size = 0; - - for (unsigned i = 0; i < buffer->planes().size(); i++) { - const FrameBuffer::Plane &plane = buffer->planes()[i]; - buffer_size += plane.length; - - if (i == buffer->planes().size() - 1 || plane.fd.get() != buffer->planes()[i + 1].fd.get()) { - return (uint8_t *)mmap(NULL, buffer_size, PROT_READ | PROT_WRITE, MAP_SHARED, plane.fd.get(), 0); - } - } - - return NULL; -} - -// https://github.com/raspberrypi/libcamera-apps/blob/a6267d51949d0602eedf60f3ddf8c6685f652812/core/options.cpp#L101 -static void set_hdr(bool hdr) { - bool ok = false; - for (int i = 0; i < 4 && !ok; i++) - { - std::string dev("/dev/v4l-subdev"); - dev += (char)('0' + i); - int fd = open(dev.c_str(), O_RDWR, 0); - if (fd < 0) - continue; - - v4l2_control ctrl { V4L2_CID_WIDE_DYNAMIC_RANGE, hdr }; - ok = !ioctl(fd, VIDIOC_S_CTRL, &ctrl); - close(fd); - } -} - -bool camera_create(const parameters_t *params, camera_frame_cb frame_cb, camera_t **cam) { - std::unique_ptr camp = std::make_unique(); - - set_hdr(params->hdr); - - if (strcmp(params->log_level, "debug") == 0) { - setenv("LIBCAMERA_LOG_LEVELS", "*:DEBUG", 1); - } else if (strcmp(params->log_level, "info") == 0) { - setenv("LIBCAMERA_LOG_LEVELS", "*:INFO", 1); - } else if (strcmp(params->log_level, "warn") == 0) { - setenv("LIBCAMERA_LOG_LEVELS", "*:WARN", 1); - } else { // error - setenv("LIBCAMERA_LOG_LEVELS", "*:ERROR", 1); - } - - // We make sure to set the environment variable before libcamera init - setenv("LIBCAMERA_RPI_TUNING_FILE", params->tuning_file, 1); - - camp->camera_manager = std::make_unique(); - int ret = camp->camera_manager->start(); - if (ret != 0) { - set_error("CameraManager.start() failed"); - return false; - } - - std::vector> cameras = camp->camera_manager->cameras(); - auto rem = std::remove_if(cameras.begin(), cameras.end(), - [](auto &cam) { return cam->id().find("/usb") != std::string::npos; }); - cameras.erase(rem, cameras.end()); - if (params->camera_id >= cameras.size()){ - set_error("selected camera is not available"); - return false; - } - - camp->camera = camp->camera_manager->get(cameras[params->camera_id]->id()); - if (camp->camera == NULL) { - set_error("CameraManager.get() failed"); - return false; - } - - ret = camp->camera->acquire(); - if (ret != 0) { - set_error("Camera.acquire() failed"); - return false; - } - - std::vector stream_roles = { StreamRole::VideoRecording }; - if (params->mode != NULL) { - stream_roles.push_back(StreamRole::Raw); - } - - std::unique_ptr conf = camp->camera->generateConfiguration(stream_roles); - if (conf == NULL) { - set_error("Camera.generateConfiguration() failed"); - return false; - } - - StreamConfiguration &video_stream_conf = conf->at(0); - video_stream_conf.size = libcamera::Size(params->width, params->height); - video_stream_conf.pixelFormat = formats::YUV420; - video_stream_conf.bufferCount = params->buffer_count; - if (params->width >= 1280 || params->height >= 720) { - video_stream_conf.colorSpace = ColorSpace::Rec709; - } else { - video_stream_conf.colorSpace = ColorSpace::Smpte170m; - } - - if (params->mode != NULL) { - StreamConfiguration &raw_stream_conf = conf->at(1); - raw_stream_conf.size = Size(params->mode->width, params->mode->height); - raw_stream_conf.pixelFormat = mode_to_pixel_format(params->mode); - raw_stream_conf.bufferCount = video_stream_conf.bufferCount; - } - - conf->transform = Transform::Identity; - if (params->h_flip) { - conf->transform = Transform::HFlip * conf->transform; - } - if (params->v_flip) { - conf->transform = Transform::VFlip * conf->transform; - } - - CameraConfiguration::Status vstatus = conf->validate(); - if (vstatus == CameraConfiguration::Invalid) { - set_error("StreamConfiguration.validate() failed"); - return false; - } - - int res = camp->camera->configure(conf.get()); - if (res != 0) { - set_error("Camera.configure() failed"); - return false; - } - - camp->video_stream = video_stream_conf.stream(); - - for (unsigned int i = 0; i < params->buffer_count; i++) { - std::unique_ptr request = camp->camera->createRequest((uint64_t)camp.get()); - if (request == NULL) { - set_error("createRequest() failed"); - return false; - } - camp->requests.push_back(std::move(request)); - } - - camp->allocator = std::make_unique(camp->camera); - for (StreamConfiguration &stream_conf : *conf) { - Stream *stream = stream_conf.stream(); - - res = camp->allocator->allocate(stream); - if (res < 0) { - set_error("allocate() failed"); - return false; - } - - int i = 0; - for (const std::unique_ptr &buffer : camp->allocator->buffers(stream)) { - // map buffer of the video stream only - if (stream == video_stream_conf.stream()) { - camp->mapped_buffers[buffer.get()] = map_buffer(buffer.get()); - } - - res = camp->requests.at(i++)->addBuffer(stream, buffer.get()); - if (res != 0) { - set_error("addBuffer() failed"); - return false; - } - } - } - - camp->params = params; - camp->frame_cb = frame_cb; - *cam = camp.release(); - - return true; -} - -static int buffer_size(const std::vector &planes) { - int size = 0; - for (const FrameBuffer::Plane &plane : planes) { - size += plane.length; - } - return size; -} - -static void on_request_complete(Request *request) { - if (request->status() == Request::RequestCancelled) { - return; - } - - CameraPriv *camp = (CameraPriv *)request->cookie(); - - FrameBuffer *buffer = request->buffers().at(camp->video_stream); - - camp->frame_cb( - camp->mapped_buffers.at(buffer), - camp->video_stream->configuration().stride, - camp->video_stream->configuration().size.height, - buffer->planes()[0].fd.get(), - buffer_size(buffer->planes()), - buffer->metadata().timestamp / 1000); - - request->reuse(Request::ReuseFlag::ReuseBuffers); - - { - std::lock_guard lock(camp->ctrls_mutex); - request->controls() = *camp->ctrls; - camp->ctrls->clear(); - } - - camp->camera->queueRequest(request); -} - -int camera_get_mode_stride(camera_t *cam) { - CameraPriv *camp = (CameraPriv *)cam; - return camp->video_stream->configuration().stride; -} - -int camera_get_mode_colorspace(camera_t *cam) { - CameraPriv *camp = (CameraPriv *)cam; - return get_v4l2_colorspace(camp->video_stream->configuration().colorSpace); -} - -static void fill_dynamic_controls(ControlList *ctrls, const parameters_t *params) { - ctrls->set(controls::Brightness, params->brightness); - ctrls->set(controls::Contrast, params->contrast); - ctrls->set(controls::Saturation, params->saturation); - ctrls->set(controls::Sharpness, params->sharpness); - - int exposure_mode; - if (strcmp(params->exposure, "short") == 0) { - exposure_mode = controls::ExposureShort; - } else if (strcmp(params->exposure, "long") == 0) { - exposure_mode = controls::ExposureLong; - } else if (strcmp(params->exposure, "custom") == 0) { - exposure_mode = controls::ExposureCustom; - } else { - exposure_mode = controls::ExposureNormal; - } - ctrls->set(controls::AeExposureMode, exposure_mode); - - int awb_mode; - if (strcmp(params->awb, "incandescent") == 0) { - awb_mode = controls::AwbIncandescent; - } else if (strcmp(params->awb, "tungsten") == 0) { - awb_mode = controls::AwbTungsten; - } else if (strcmp(params->awb, "fluorescent") == 0) { - awb_mode = controls::AwbFluorescent; - } else if (strcmp(params->awb, "indoor") == 0) { - awb_mode = controls::AwbIndoor; - } else if (strcmp(params->awb, "daylight") == 0) { - awb_mode = controls::AwbDaylight; - } else if (strcmp(params->awb, "cloudy") == 0) { - awb_mode = controls::AwbCloudy; - } else if (strcmp(params->awb, "custom") == 0) { - awb_mode = controls::AwbCustom; - } else { - awb_mode = controls::AwbAuto; - } - ctrls->set(controls::AwbMode, awb_mode); - - if (params->awb_gain_red > 0 && params->awb_gain_blue > 0) { - ctrls->set(controls::ColourGains, - Span({params->awb_gain_red, params->awb_gain_blue})); - } - - int denoise_mode; - if (strcmp(params->denoise, "cdn_off") == 0) { - denoise_mode = controls::draft::NoiseReductionModeMinimal; - } else if (strcmp(params->denoise, "cdn_hq") == 0) { - denoise_mode = controls::draft::NoiseReductionModeHighQuality; - } else if (strcmp(params->denoise, "cdn_fast") == 0) { - denoise_mode = controls::draft::NoiseReductionModeFast; - } else { - denoise_mode = controls::draft::NoiseReductionModeOff; - } - ctrls->set(controls::draft::NoiseReductionMode, denoise_mode); - - ctrls->set(controls::ExposureTime, params->shutter); - - int metering_mode; - if (strcmp(params->metering, "spot") == 0) { - metering_mode = controls::MeteringSpot; - } else if (strcmp(params->metering, "matrix") == 0) { - metering_mode = controls::MeteringMatrix; - } else if (strcmp(params->metering, "custom") == 0) { - metering_mode = controls::MeteringCustom; - } else { - metering_mode = controls::MeteringCentreWeighted; - } - ctrls->set(controls::AeMeteringMode, metering_mode); - - ctrls->set(controls::AnalogueGain, params->gain); - - ctrls->set(controls::ExposureValue, params->ev); - - int64_t frame_time = (int64_t)(((float)1000000) / params->fps); - ctrls->set(controls::FrameDurationLimits, Span({ frame_time, frame_time })); -} - -bool camera_start(camera_t *cam) { - CameraPriv *camp = (CameraPriv *)cam; - - camp->ctrls = std::make_unique(controls::controls); - - fill_dynamic_controls(camp->ctrls.get(), camp->params); - - if (camp->camera->controls().count(&controls::AfMode) > 0) { - if (camp->params->af_window != NULL) { - std::optional opt = camp->camera->properties().get(properties::ScalerCropMaximum); - Rectangle sensor_area; - try { - sensor_area = opt.value(); - } catch(const std::bad_optional_access& exc) { - set_error("get(ScalerCropMaximum) failed"); - return false; - } - - Rectangle afwindows_rectangle[1]; - - afwindows_rectangle[0] = Rectangle( - camp->params->af_window->x * sensor_area.width, - camp->params->af_window->y * sensor_area.height, - camp->params->af_window->width * sensor_area.width, - camp->params->af_window->height * sensor_area.height); - - afwindows_rectangle[0].translateBy(sensor_area.topLeft()); - camp->ctrls->set(controls::AfMetering, controls::AfMeteringWindows); - camp->ctrls->set(controls::AfWindows, afwindows_rectangle); - } - - int af_mode; - if (strcmp(camp->params->af_mode, "manual") == 0) { - af_mode = controls::AfModeManual; - } else if (strcmp(camp->params->af_mode, "continuous") == 0) { - af_mode = controls::AfModeContinuous; - } else { - af_mode = controls::AfModeAuto; - } - camp->ctrls->set(controls::AfMode, af_mode); - - int af_range; - if (strcmp(camp->params->af_range, "macro") == 0) { - af_range = controls::AfRangeMacro; - } else if (strcmp(camp->params->af_range, "full") == 0) { - af_range = controls::AfRangeFull; - } else { - af_range = controls::AfRangeNormal; - } - camp->ctrls->set(controls::AfRange, af_range); - - int af_speed; - if (strcmp(camp->params->af_range, "fast") == 0) { - af_speed = controls::AfSpeedFast; - } else { - af_speed = controls::AfSpeedNormal; - } - camp->ctrls->set(controls::AfSpeed, af_speed); - - if (strcmp(camp->params->af_mode, "auto") == 0) { - camp->ctrls->set(controls::AfTrigger, controls::AfTriggerStart); - } else if (strcmp(camp->params->af_mode, "manual") == 0) { - camp->ctrls->set(controls::LensPosition, camp->params->lens_position); - } - } - - if (camp->params->roi != NULL) { - std::optional opt = camp->camera->properties().get(properties::ScalerCropMaximum); - Rectangle sensor_area; - try { - sensor_area = opt.value(); - } catch(const std::bad_optional_access& exc) { - set_error("get(ScalerCropMaximum) failed"); - return false; - } - - Rectangle crop( - camp->params->roi->x * sensor_area.width, - camp->params->roi->y * sensor_area.height, - camp->params->roi->width * sensor_area.width, - camp->params->roi->height * sensor_area.height); - crop.translateBy(sensor_area.topLeft()); - camp->ctrls->set(controls::ScalerCrop, crop); - } - - int res = camp->camera->start(camp->ctrls.get()); - if (res != 0) { - set_error("Camera.start() failed"); - return false; - } - - camp->ctrls->clear(); - - camp->camera->requestCompleted.connect(on_request_complete); - - for (std::unique_ptr &request : camp->requests) { - int res = camp->camera->queueRequest(request.get()); - if (res != 0) { - set_error("Camera.queueRequest() failed"); - return false; - } - } - - return true; -} - -void camera_reload_params(camera_t *cam, const parameters_t *params) { - CameraPriv *camp = (CameraPriv *)cam; - - std::lock_guard lock(camp->ctrls_mutex); - fill_dynamic_controls(camp->ctrls.get(), params); -} diff --git a/internal/protocols/rpicamera/exe/camera.h b/internal/protocols/rpicamera/exe/camera.h deleted file mode 100644 index 467a7cc7789..00000000000 --- a/internal/protocols/rpicamera/exe/camera.h +++ /dev/null @@ -1,31 +0,0 @@ -#ifndef __CAMERA_H__ -#define __CAMERA_H__ - -#include "parameters.h" - -typedef void camera_t; - -typedef void (*camera_frame_cb)( - uint8_t *mapped_buffer, - int stride, - int height, - int buffer_fd, - uint64_t size, - uint64_t timestamp); - -#ifdef __cplusplus -extern "C" { -#endif - -const char *camera_get_error(); -bool camera_create(const parameters_t *params, camera_frame_cb frame_cb, camera_t **cam); -int camera_get_mode_stride(camera_t *cam); -int camera_get_mode_colorspace(camera_t *cam); -bool camera_start(camera_t *cam); -void camera_reload_params(camera_t *cam, const parameters_t *params); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/internal/protocols/rpicamera/exe/encoder.c b/internal/protocols/rpicamera/exe/encoder.c deleted file mode 100644 index 0bfeb7b5b6f..00000000000 --- a/internal/protocols/rpicamera/exe/encoder.c +++ /dev/null @@ -1,337 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -#include "encoder.h" - -#define DEVICE "/dev/video11" -#define POLL_TIMEOUT_MS 200 - -static char errbuf[256]; - -static void set_error(const char *format, ...) { - va_list args; - va_start(args, format); - vsnprintf(errbuf, 256, format, args); -} - -const char *encoder_get_error() { - return errbuf; -} - -typedef struct { - const parameters_t *params; - int fd; - void **capture_buffers; - int cur_buffer; - encoder_output_cb output_cb; - pthread_t output_thread; - bool ts_initialized; - uint64_t start_ts; -} encoder_priv_t; - -static void *output_thread(void *userdata) { - encoder_priv_t *encp = (encoder_priv_t *)userdata; - - while (true) { - struct pollfd p = { encp->fd, POLLIN, 0 }; - int res = poll(&p, 1, POLL_TIMEOUT_MS); - if (res == -1) { - fprintf(stderr, "output_thread(): poll() failed\n"); - exit(1); - } - - if (p.revents & POLLIN) { - struct v4l2_buffer buf = {0}; - struct v4l2_plane planes[VIDEO_MAX_PLANES] = {0}; - buf.type = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE; - buf.memory = V4L2_MEMORY_DMABUF; - buf.length = 1; - buf.m.planes = planes; - int res = ioctl(encp->fd, VIDIOC_DQBUF, &buf); - if (res != 0) { - fprintf(stderr, "output_thread(): ioctl(VIDIOC_DQBUF) failed\n"); - exit(1); - } - - memset(&buf, 0, sizeof(buf)); - memset(planes, 0, sizeof(planes)); - buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE; - buf.memory = V4L2_MEMORY_MMAP; - buf.length = 1; - buf.m.planes = planes; - res = ioctl(encp->fd, VIDIOC_DQBUF, &buf); - if (res == 0) { - uint64_t ts = ((uint64_t)buf.timestamp.tv_sec * (uint64_t)1000000) + (uint64_t)buf.timestamp.tv_usec; - - if (!encp->ts_initialized) { - encp->ts_initialized = true; - encp->start_ts = ts; - } - - ts -= encp->start_ts; - - const uint8_t *bufmem = (const uint8_t *)encp->capture_buffers[buf.index]; - int bufsize = buf.m.planes[0].bytesused; - encp->output_cb(ts, bufmem, bufsize); - - int index = buf.index; - int length = buf.m.planes[0].length; - - struct v4l2_buffer buf = {0}; - struct v4l2_plane planes[VIDEO_MAX_PLANES] = {0}; - buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE; - buf.memory = V4L2_MEMORY_MMAP; - buf.index = index; - buf.length = 1; - buf.m.planes = planes; - buf.m.planes[0].bytesused = 0; - buf.m.planes[0].length = length; - int res = ioctl(encp->fd, VIDIOC_QBUF, &buf); - if (res < 0) { - fprintf(stderr, "output_thread(): ioctl(VIDIOC_QBUF) failed\n"); - exit(1); - } - } - } - } - - return NULL; -} - -static bool fill_dynamic_params(int fd, const parameters_t *params) { - struct v4l2_control ctrl = {0}; - ctrl.id = V4L2_CID_MPEG_VIDEO_H264_I_PERIOD; - ctrl.value = params->idr_period; - int res = ioctl(fd, VIDIOC_S_CTRL, &ctrl); - if (res != 0) { - set_error("unable to set IDR period"); - return false; - } - - ctrl.id = V4L2_CID_MPEG_VIDEO_BITRATE; - ctrl.value = params->bitrate; - res = ioctl(fd, VIDIOC_S_CTRL, &ctrl); - if (res != 0) { - set_error("unable to set bitrate"); - return false; - } - - return true; -} - -bool encoder_create(const parameters_t *params, int stride, int colorspace, encoder_output_cb output_cb, encoder_t **enc) { - *enc = malloc(sizeof(encoder_priv_t)); - encoder_priv_t *encp = (encoder_priv_t *)(*enc); - memset(encp, 0, sizeof(encoder_priv_t)); - - encp->fd = open(DEVICE, O_RDWR, 0); - if (encp->fd < 0) { - set_error("unable to open device"); - goto failed; - } - - bool res2 = fill_dynamic_params(encp->fd, params); - if (!res2) { - goto failed; - } - - struct v4l2_control ctrl = {0}; - ctrl.id = V4L2_CID_MPEG_VIDEO_H264_PROFILE; - ctrl.value = params->profile; - int res = ioctl(encp->fd, VIDIOC_S_CTRL, &ctrl); - if (res != 0) { - set_error("unable to set profile"); - goto failed; - } - - ctrl.id = V4L2_CID_MPEG_VIDEO_H264_LEVEL; - ctrl.value = params->level; - res = ioctl(encp->fd, VIDIOC_S_CTRL, &ctrl); - if (res != 0) { - set_error("unable to set level"); - goto failed; - } - - ctrl.id = V4L2_CID_MPEG_VIDEO_REPEAT_SEQ_HEADER; - ctrl.value = 0; - res = ioctl(encp->fd, VIDIOC_S_CTRL, &ctrl); - if (res != 0) { - set_error("unable to set REPEAT_SEQ_HEADER"); - goto failed; - } - - struct v4l2_format fmt = {0}; - fmt.type = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE; - fmt.fmt.pix_mp.width = params->width; - fmt.fmt.pix_mp.height = params->height; - fmt.fmt.pix_mp.pixelformat = V4L2_PIX_FMT_YUV420; - fmt.fmt.pix_mp.plane_fmt[0].bytesperline = stride; - fmt.fmt.pix_mp.field = V4L2_FIELD_ANY; - fmt.fmt.pix_mp.colorspace = colorspace; - fmt.fmt.pix_mp.num_planes = 1; - res = ioctl(encp->fd, VIDIOC_S_FMT, &fmt); - if (res != 0) { - set_error("unable to set output format"); - goto failed; - } - - memset(&fmt, 0, sizeof(fmt)); - fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE; - fmt.fmt.pix_mp.width = params->width; - fmt.fmt.pix_mp.height = params->height; - fmt.fmt.pix_mp.pixelformat = V4L2_PIX_FMT_H264; - fmt.fmt.pix_mp.field = V4L2_FIELD_ANY; - fmt.fmt.pix_mp.colorspace = V4L2_COLORSPACE_DEFAULT; - fmt.fmt.pix_mp.num_planes = 1; - fmt.fmt.pix_mp.plane_fmt[0].bytesperline = 0; - fmt.fmt.pix_mp.plane_fmt[0].sizeimage = 512 << 10; - res = ioctl(encp->fd, VIDIOC_S_FMT, &fmt); - if (res != 0) { - set_error("unable to set capture format"); - goto failed; - } - - struct v4l2_streamparm parm = {0}; - parm.type = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE; - parm.parm.output.timeperframe.numerator = 1; - parm.parm.output.timeperframe.denominator = params->fps; - res = ioctl(encp->fd, VIDIOC_S_PARM, &parm); - if (res != 0) { - set_error("unable to set fps"); - goto failed; - } - - struct v4l2_requestbuffers reqbufs = {0}; - reqbufs.count = params->buffer_count; - reqbufs.type = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE; - reqbufs.memory = V4L2_MEMORY_DMABUF; - res = ioctl(encp->fd, VIDIOC_REQBUFS, &reqbufs); - if (res != 0) { - set_error("unable to set output buffers"); - goto failed; - } - - memset(&reqbufs, 0, sizeof(reqbufs)); - reqbufs.count = params->capture_buffer_count; - reqbufs.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE; - reqbufs.memory = V4L2_MEMORY_MMAP; - res = ioctl(encp->fd, VIDIOC_REQBUFS, &reqbufs); - if (res != 0) { - set_error("unable to set capture buffers"); - goto failed; - } - - encp->capture_buffers = malloc(sizeof(void *) * reqbufs.count); - - for (unsigned int i = 0; i < reqbufs.count; i++) { - struct v4l2_plane planes[VIDEO_MAX_PLANES]; - - struct v4l2_buffer buffer = {0}; - buffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE; - buffer.memory = V4L2_MEMORY_MMAP; - buffer.index = i; - buffer.length = 1; - buffer.m.planes = planes; - int res = ioctl(encp->fd, VIDIOC_QUERYBUF, &buffer); - if (res != 0) { - set_error("unable to query buffer"); - goto failed; - } - - encp->capture_buffers[i] = mmap( - 0, - buffer.m.planes[0].length, - PROT_READ | PROT_WRITE, MAP_SHARED, - encp->fd, - buffer.m.planes[0].m.mem_offset); - if (encp->capture_buffers[i] == MAP_FAILED) { - set_error("mmap() failed"); - goto failed; - } - - res = ioctl(encp->fd, VIDIOC_QBUF, &buffer); - if (res != 0) { - set_error("ioctl(VIDIOC_QBUF) failed"); - goto failed; - } - } - - enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE; - res = ioctl(encp->fd, VIDIOC_STREAMON, &type); - if (res != 0) { - set_error("unable to activate output stream"); - goto failed; - } - - type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE; - res = ioctl(encp->fd, VIDIOC_STREAMON, &type); - if (res != 0) { - set_error("unable to activate capture stream"); - } - - encp->params = params; - encp->cur_buffer = 0; - encp->output_cb = output_cb; - encp->ts_initialized = false; - - pthread_create(&encp->output_thread, NULL, output_thread, encp); - - return true; - -failed: - if (encp->capture_buffers != NULL) { - free(encp->capture_buffers); - } - if (encp->fd >= 0) { - close(encp->fd); - } - - free(encp); - - return false; -} - -void encoder_encode(encoder_t *enc, int buffer_fd, size_t size, int64_t timestamp_us) { - encoder_priv_t *encp = (encoder_priv_t *)enc; - - int index = encp->cur_buffer++; - encp->cur_buffer %= encp->params->buffer_count; - - struct v4l2_buffer buf = {0}; - struct v4l2_plane planes[VIDEO_MAX_PLANES] = {0}; - buf.type = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE; - buf.index = index; - buf.field = V4L2_FIELD_NONE; - buf.memory = V4L2_MEMORY_DMABUF; - buf.length = 1; - buf.timestamp.tv_sec = timestamp_us / 1000000; - buf.timestamp.tv_usec = timestamp_us % 1000000; - buf.m.planes = planes; - buf.m.planes[0].m.fd = buffer_fd; - buf.m.planes[0].bytesused = size; - buf.m.planes[0].length = size; - int res = ioctl(encp->fd, VIDIOC_QBUF, &buf); - if (res != 0) { - fprintf(stderr, "encoder_encode(): ioctl(VIDIOC_QBUF) failed\n"); - // it happens when the raspberry is under pressure. do not exit. - } -} - -void encoder_reload_params(encoder_t *enc, const parameters_t *params) { - encoder_priv_t *encp = (encoder_priv_t *)enc; - - fill_dynamic_params(encp->fd, params); -} diff --git a/internal/protocols/rpicamera/exe/encoder.h b/internal/protocols/rpicamera/exe/encoder.h deleted file mode 100644 index eb1e7928e9e..00000000000 --- a/internal/protocols/rpicamera/exe/encoder.h +++ /dev/null @@ -1,15 +0,0 @@ -#ifndef __ENCODER_H__ -#define __ENCODER_H__ - -#include "parameters.h" - -typedef void encoder_t; - -typedef void (*encoder_output_cb)(uint64_t ts, const uint8_t *buf, uint64_t size); - -const char *encoder_get_error(); -bool encoder_create(const parameters_t *params, int stride, int colorspace, encoder_output_cb output_cb, encoder_t **enc); -void encoder_encode(encoder_t *enc, int buffer_fd, size_t size, int64_t timestamp_us); -void encoder_reload_params(encoder_t *enc, const parameters_t *params); - -#endif diff --git a/internal/protocols/rpicamera/exe/main.c b/internal/protocols/rpicamera/exe/main.c deleted file mode 100644 index bb379215f2a..00000000000 --- a/internal/protocols/rpicamera/exe/main.c +++ /dev/null @@ -1,116 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include - -#include "parameters.h" -#include "pipe.h" -#include "camera.h" -#include "text.h" -#include "encoder.h" - -static int pipe_video_fd; -static pthread_mutex_t pipe_video_mutex; -static text_t *text; -static encoder_t *enc; - -static void on_frame( - uint8_t *mapped_buffer, - int stride, - int height, - int buffer_fd, - uint64_t size, - uint64_t timestamp) { - text_draw(text, mapped_buffer, stride, height); - encoder_encode(enc, buffer_fd, size, timestamp); -} - -static void on_encoder_output(uint64_t ts, const uint8_t *buf, uint64_t size) { - pthread_mutex_lock(&pipe_video_mutex); - pipe_write_buf(pipe_video_fd, ts, buf, size); - pthread_mutex_unlock(&pipe_video_mutex); -} - -int main() { - int pipe_conf_fd = atoi(getenv("PIPE_CONF_FD")); - pipe_video_fd = atoi(getenv("PIPE_VIDEO_FD")); - - uint8_t *buf; - uint32_t n = pipe_read(pipe_conf_fd, &buf); - - parameters_t params; - bool ok = parameters_unserialize(¶ms, &buf[1], n-1); - free(buf); - if (!ok) { - pipe_write_error(pipe_video_fd, "parameters_unserialize(): %s", parameters_get_error()); - return 5; - } - - pthread_mutex_init(&pipe_video_mutex, NULL); - pthread_mutex_lock(&pipe_video_mutex); - - camera_t *cam; - ok = camera_create( - ¶ms, - on_frame, - &cam); - if (!ok) { - pipe_write_error(pipe_video_fd, "camera_create(): %s", camera_get_error()); - return 5; - } - - ok = text_create(¶ms, &text); - if (!ok) { - pipe_write_error(pipe_video_fd, "text_create(): %s", text_get_error()); - return 5; - } - - ok = encoder_create( - ¶ms, - camera_get_mode_stride(cam), - camera_get_mode_colorspace(cam), - on_encoder_output, - &enc); - if (!ok) { - pipe_write_error(pipe_video_fd, "encoder_create(): %s", encoder_get_error()); - return 5; - } - - ok = camera_start(cam); - if (!ok) { - pipe_write_error(pipe_video_fd, "camera_start(): %s", camera_get_error()); - return 5; - } - - pipe_write_ready(pipe_video_fd); - pthread_mutex_unlock(&pipe_video_mutex); - - while (true) { - uint8_t *buf; - uint32_t n = pipe_read(pipe_conf_fd, &buf); - - switch (buf[0]) { - case 'e': - return 0; - - case 'c': - { - parameters_t params; - bool ok = parameters_unserialize(¶ms, &buf[1], n-1); - free(buf); - if (!ok) { - printf("skipping reloading parameters since they are invalid: %s\n", parameters_get_error()); - continue; - } - camera_reload_params(cam, ¶ms); - encoder_reload_params(enc, ¶ms); - parameters_destroy(¶ms); - } - } - } - - return 0; -} diff --git a/internal/protocols/rpicamera/exe/parameters.c b/internal/protocols/rpicamera/exe/parameters.c deleted file mode 100644 index 9805d2f3e0e..00000000000 --- a/internal/protocols/rpicamera/exe/parameters.c +++ /dev/null @@ -1,210 +0,0 @@ -#include -#include -#include -#include -#include - -#include - -#include "base64.h" -#include "parameters.h" - -static char errbuf[256]; - -static void set_error(const char *format, ...) { - va_list args; - va_start(args, format); - vsnprintf(errbuf, 256, format, args); -} - -const char *parameters_get_error() { - return errbuf; -} - -bool parameters_unserialize(parameters_t *params, const uint8_t *buf, size_t buf_size) { - memset(params, 0, sizeof(parameters_t)); - - char *tmp = malloc(buf_size + 1); - memcpy(tmp, buf, buf_size); - tmp[buf_size] = 0x00; - - while (true) { - char *entry = strsep(&tmp, " "); - if (entry == NULL) { - break; - } - - char *key = strsep(&entry, ":"); - char *val = strsep(&entry, ":"); - - if (strcmp(key, "LogLevel") == 0) { - params->log_level = base64_decode(val); - } else if (strcmp(key, "CameraID") == 0) { - params->camera_id = atoi(val); - } else if (strcmp(key, "Width") == 0) { - params->width = atoi(val); - } else if (strcmp(key, "Height") == 0) { - params->height = atoi(val); - } else if (strcmp(key, "HFlip") == 0) { - params->h_flip = (strcmp(val, "1") == 0); - } else if (strcmp(key, "VFlip") == 0) { - params->v_flip = (strcmp(val, "1") == 0); - } else if (strcmp(key, "Brightness") == 0) { - params->brightness = atof(val); - } else if (strcmp(key, "Contrast") == 0) { - params->contrast = atof(val); - } else if (strcmp(key, "Saturation") == 0) { - params->saturation = atof(val); - } else if (strcmp(key, "Sharpness") == 0) { - params->sharpness = atof(val); - } else if (strcmp(key, "Exposure") == 0) { - params->exposure = base64_decode(val); - } else if (strcmp(key, "AWB") == 0) { - params->awb = base64_decode(val); - } else if (strcmp(key, "AWBGainRed") == 0) { - params->awb_gain_red = atof(val); - } else if (strcmp(key, "AWBGainBlue") == 0) { - params->awb_gain_blue = atof(val); - } else if (strcmp(key, "Denoise") == 0) { - params->denoise = base64_decode(val); - } else if (strcmp(key, "Shutter") == 0) { - params->shutter = atoi(val); - } else if (strcmp(key, "Metering") == 0) { - params->metering = base64_decode(val); - } else if (strcmp(key, "Gain") == 0) { - params->gain = atof(val); - } else if (strcmp(key, "EV") == 0) { - params->ev = atof(val); - } else if (strcmp(key, "ROI") == 0) { - char *decoded_val = base64_decode(val); - if (strlen(decoded_val) != 0) { - params->roi = malloc(sizeof(window_t)); - bool ok = window_load(decoded_val, params->roi); - if (!ok) { - set_error("invalid ROI"); - free(decoded_val); - goto failed; - } - } - free(decoded_val); - } else if (strcmp(key, "HDR") == 0) { - params->hdr = (strcmp(val, "1") == 0); - } else if (strcmp(key, "TuningFile") == 0) { - params->tuning_file = base64_decode(val); - } else if (strcmp(key, "Mode") == 0) { - char *decoded_val = base64_decode(val); - if (strlen(decoded_val) != 0) { - params->mode = malloc(sizeof(sensor_mode_t)); - bool ok = sensor_mode_load(decoded_val, params->mode); - if (!ok) { - set_error("invalid sensor mode"); - free(decoded_val); - goto failed; - } - } - free(decoded_val); - } else if (strcmp(key, "FPS") == 0) { - params->fps = atof(val); - } else if (strcmp(key, "IDRPeriod") == 0) { - params->idr_period = atoi(val); - } else if (strcmp(key, "Bitrate") == 0) { - params->bitrate = atoi(val); - } else if (strcmp(key, "Profile") == 0) { - char *decoded_val = base64_decode(val); - if (strcmp(decoded_val, "baseline") == 0) { - params->profile = V4L2_MPEG_VIDEO_H264_PROFILE_BASELINE; - } else if (strcmp(decoded_val, "main") == 0) { - params->profile = V4L2_MPEG_VIDEO_H264_PROFILE_MAIN; - } else { - params->profile = V4L2_MPEG_VIDEO_H264_PROFILE_HIGH; - } - free(decoded_val); - } else if (strcmp(key, "Level") == 0) { - char *decoded_val = base64_decode(val); - if (strcmp(decoded_val, "4.0") == 0) { - params->level = V4L2_MPEG_VIDEO_H264_LEVEL_4_0; - } else if (strcmp(decoded_val, "4.1") == 0) { - params->level = V4L2_MPEG_VIDEO_H264_LEVEL_4_1; - } else { - params->level = V4L2_MPEG_VIDEO_H264_LEVEL_4_2; - } - free(decoded_val); - } else if (strcmp(key, "AfMode") == 0) { - params->af_mode = base64_decode(val); - } else if (strcmp(key, "AfRange") == 0) { - params->af_range = base64_decode(val); - } else if (strcmp(key, "AfSpeed") == 0) { - params->af_speed = base64_decode(val); - } else if (strcmp(key, "LensPosition") == 0) { - params->lens_position = atof(val); - } else if (strcmp(key, "AfWindow") == 0) { - char *decoded_val = base64_decode(val); - if (strlen(decoded_val) != 0) { - params->af_window = malloc(sizeof(window_t)); - bool ok = window_load(decoded_val, params->af_window); - if (!ok) { - set_error("invalid AfWindow"); - free(decoded_val); - goto failed; - } - } - free(decoded_val); - } else if (strcmp(key, "TextOverlayEnable") == 0) { - params->text_overlay_enable = (strcmp(val, "1") == 0); - } else if (strcmp(key, "TextOverlay") == 0) { - params->text_overlay = base64_decode(val); - } - } - - free(tmp); - - params->buffer_count = 6; - params->capture_buffer_count = params->buffer_count * 2; - - return true; - -failed: - free(tmp); - parameters_destroy(params); - - return false; -} - -void parameters_destroy(parameters_t *params) { - if (params->exposure != NULL) { - free(params->exposure); - } - if (params->awb != NULL) { - free(params->awb); - } - if (params->denoise != NULL) { - free(params->denoise); - } - if (params->metering != NULL) { - free(params->metering); - } - if (params->roi != NULL) { - free(params->roi); - } - if (params->tuning_file != NULL) { - free(params->tuning_file); - } - if (params->mode != NULL) { - free(params->mode); - } - if (params->af_mode != NULL) { - free(params->af_mode); - } - if (params->af_range != NULL) { - free(params->af_range); - } - if (params->af_speed != NULL) { - free(params->af_speed); - } - if (params->af_window != NULL) { - free(params->af_window); - } - if (params->text_overlay != NULL) { - free(params->text_overlay); - } -} diff --git a/internal/protocols/rpicamera/exe/parameters.h b/internal/protocols/rpicamera/exe/parameters.h deleted file mode 100644 index 36ced18b0da..00000000000 --- a/internal/protocols/rpicamera/exe/parameters.h +++ /dev/null @@ -1,64 +0,0 @@ -#ifndef __PARAMETERS_H__ -#define __PARAMETERS_H__ - -#include -#include - -#include "window.h" -#include "sensor_mode.h" - -typedef struct { - char *log_level; - unsigned int camera_id; - unsigned int width; - unsigned int height; - bool h_flip; - bool v_flip; - float brightness; - float contrast; - float saturation; - float sharpness; - char *exposure; - char *awb; - float awb_gain_red; - float awb_gain_blue; - char *denoise; - unsigned int shutter; - char *metering; - float gain; - float ev; - window_t *roi; - bool hdr; - char *tuning_file; - sensor_mode_t *mode; - float fps; - unsigned int idr_period; - unsigned int bitrate; - unsigned int profile; - unsigned int level; - char *af_mode; - char *af_range; - char *af_speed; - float lens_position; - window_t *af_window; - bool text_overlay_enable; - char *text_overlay; - - // private - unsigned int buffer_count; - unsigned int capture_buffer_count; -} parameters_t; - -#ifdef __cplusplus -extern "C" { -#endif - -const char *parameters_get_error(); -bool parameters_unserialize(parameters_t *params, const uint8_t *buf, size_t buf_size); -void parameters_destroy(parameters_t *params); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/internal/protocols/rpicamera/exe/pipe.c b/internal/protocols/rpicamera/exe/pipe.c deleted file mode 100644 index 38d2437f3ae..00000000000 --- a/internal/protocols/rpicamera/exe/pipe.c +++ /dev/null @@ -1,44 +0,0 @@ -#include -#include -#include -#include -#include -#include - -#include "pipe.h" - -void pipe_write_error(int fd, const char *format, ...) { - char buf[256]; - buf[0] = 'e'; - va_list args; - va_start(args, format); - vsnprintf(&buf[1], 255, format, args); - uint32_t n = strlen(buf); - write(fd, &n, 4); - write(fd, buf, n); -} - -void pipe_write_ready(int fd) { - char buf[] = {'r'}; - uint32_t n = 1; - write(fd, &n, 4); - write(fd, buf, n); -} - -void pipe_write_buf(int fd, uint64_t ts, const uint8_t *buf, uint32_t n) { - char head[] = {'b'}; - n += 1 + sizeof(uint64_t); - write(fd, &n, 4); - write(fd, head, 1); - write(fd, &ts, sizeof(uint64_t)); - write(fd, buf, n - 1 - sizeof(uint64_t)); -} - -uint32_t pipe_read(int fd, uint8_t **pbuf) { - uint32_t n; - read(fd, &n, 4); - - *pbuf = malloc(n); - read(fd, *pbuf, n); - return n; -} diff --git a/internal/protocols/rpicamera/exe/pipe.h b/internal/protocols/rpicamera/exe/pipe.h deleted file mode 100644 index 266304522f8..00000000000 --- a/internal/protocols/rpicamera/exe/pipe.h +++ /dev/null @@ -1,12 +0,0 @@ -#ifndef __PIPE_H__ -#define __PIPE_H__ - -#include -#include - -void pipe_write_error(int fd, const char *format, ...); -void pipe_write_ready(int fd); -void pipe_write_buf(int fd, uint64_t ts, const uint8_t *buf, uint32_t n); -uint32_t pipe_read(int fd, uint8_t **pbuf); - -#endif diff --git a/internal/protocols/rpicamera/exe/sensor_mode.c b/internal/protocols/rpicamera/exe/sensor_mode.c deleted file mode 100644 index 55932e21a28..00000000000 --- a/internal/protocols/rpicamera/exe/sensor_mode.c +++ /dev/null @@ -1,27 +0,0 @@ -#include -#include -#include - -#include "sensor_mode.h" - -bool sensor_mode_load(const char *encoded, sensor_mode_t *mode) { - char p; - int n = sscanf(encoded, "%u:%u:%u:%c", &(mode->width), &(mode->height), &(mode->bit_depth), &p); - if (n < 2) { - return false; - } - - if (n < 4) { - mode->packed = true; - } else if (toupper(p) == 'P') { - mode->packed = true; - } else if (toupper(p) == 'U') { - mode->packed = false; - } - - if (n < 3) { - mode->bit_depth = 12; - } - - return true; -} diff --git a/internal/protocols/rpicamera/exe/sensor_mode.h b/internal/protocols/rpicamera/exe/sensor_mode.h deleted file mode 100644 index 6306ee8775e..00000000000 --- a/internal/protocols/rpicamera/exe/sensor_mode.h +++ /dev/null @@ -1,15 +0,0 @@ -#ifndef __SENSOR_MODE_H__ -#define __SENSOR_MODE_H__ - -#include - -typedef struct { - int width; - int height; - int bit_depth; - bool packed; -} sensor_mode_t; - -bool sensor_mode_load(const char *encoded, sensor_mode_t *mode); - -#endif diff --git a/internal/protocols/rpicamera/exe/text.c b/internal/protocols/rpicamera/exe/text.c deleted file mode 100644 index 4ec8593d227..00000000000 --- a/internal/protocols/rpicamera/exe/text.c +++ /dev/null @@ -1,171 +0,0 @@ -#include - -#include -#include FT_FREETYPE_H - -#include "text_font.h" -#include "text.h" - -static char errbuf[256]; - -static void set_error(const char *format, ...) { - va_list args; - va_start(args, format); - vsnprintf(errbuf, 256, format, args); -} - -const char *text_get_error() { - return errbuf; -} - -typedef struct { - bool enabled; - char *text_overlay; - FT_Library library; - FT_Face face; -} text_priv_t; - -bool text_create(const parameters_t *params, text_t **text) { - *text = malloc(sizeof(text_priv_t)); - text_priv_t *textp = (text_priv_t *)(*text); - memset(textp, 0, sizeof(text_priv_t)); - - textp->enabled = params->text_overlay_enable; - textp->text_overlay = strdup(params->text_overlay); - - if (textp->enabled) { - int error = FT_Init_FreeType(&textp->library); - if (error) { - set_error("FT_Init_FreeType() failed"); - goto failed; - } - - error = FT_New_Memory_Face( - textp->library, - text_font_ttf, - sizeof(text_font_ttf), - 0, - &textp->face); - if (error) { - set_error("FT_New_Memory_Face() failed"); - goto failed; - } - - error = FT_Set_Pixel_Sizes( - textp->face, - 25, - 25); - if (error) { - set_error("FT_Set_Pixel_Sizes() failed"); - goto failed; - } - } - - return true; - -failed: - free(textp); - - return false; -} - -static void draw_rect(uint8_t *buf, int stride, int height, int x, int y, unsigned int rect_width, unsigned int rect_height) { - uint8_t *Y = buf; - uint8_t *U = Y + stride * height; - uint8_t *V = U + (stride / 2) * (height / 2); - const uint8_t color[3] = {0, 128, 128}; - uint32_t opacity = 45; - - for (unsigned int src_y = 0; src_y < rect_height; src_y++) { - for (unsigned int src_x = 0; src_x < rect_width; src_x++) { - unsigned int dest_x = x + src_x; - unsigned int dest_y = y + src_y; - int i1 = dest_y*stride + dest_x; - int i2 = dest_y/2*stride/2 + dest_x/2; - - Y[i1] = ((color[0] * opacity) + (uint32_t)Y[i1] * (255 - opacity)) / 255; - U[i2] = ((color[1] * opacity) + (uint32_t)U[i2] * (255 - opacity)) / 255; - V[i2] = ((color[2] * opacity) + (uint32_t)V[i2] * (255 - opacity)) / 255; - } - } -} - -static void draw_bitmap(uint8_t *buf, int stride, int height, const FT_Bitmap *bitmap, int x, int y) { - uint8_t *Y = buf; - uint8_t *U = Y + stride * height; - uint8_t *V = U + (stride / 2) * (height / 2); - - for (unsigned int src_y = 0; src_y < bitmap->rows; src_y++) { - for (unsigned int src_x = 0; src_x < bitmap->width; src_x++) { - uint8_t v = bitmap->buffer[src_y*bitmap->pitch + src_x]; - - if (v != 0) { - unsigned int dest_x = x + src_x; - unsigned int dest_y = y + src_y; - int i1 = dest_y*stride + dest_x; - int i2 = dest_y/2*stride/2 + dest_x/2; - uint32_t opacity = (uint32_t)v; - - Y[i1] = (uint8_t)(((uint32_t)v * opacity + (uint32_t)Y[i1] * (255 - opacity)) / 255); - U[i2] = (uint8_t)((128 * opacity + (uint32_t)U[i2] * (255 - opacity)) / 255); - V[i2] = (uint8_t)((128 * opacity + (uint32_t)V[i2] * (255 - opacity)) / 255); - } - } - } -} - -static int get_text_width(FT_Face face, const char *text) { - int ret = 0; - - for (const char *ptr = text; *ptr != 0x00; ptr++) { - int error = FT_Load_Char(face, *ptr, FT_LOAD_RENDER); - if (error) { - continue; - } - - ret += face->glyph->advance.x >> 6; - } - - return ret; -} - -void text_draw(text_t *text, uint8_t *buf, int stride, int height) { - text_priv_t *textp = (text_priv_t *)text; - - if (textp->enabled) { - time_t timer = time(NULL); - struct tm *tm_info = localtime(&timer); - char buffer[255]; - memset(buffer, 0, sizeof(buffer)); - strftime(buffer, 255, textp->text_overlay, tm_info); - - draw_rect( - buf, - stride, - height, - 7, - 7, - get_text_width(textp->face, buffer) + 10, - 34); - - int x = 12; - int y = 33; - - for (const char *ptr = buffer; *ptr != 0x00; ptr++) { - int error = FT_Load_Char(textp->face, *ptr, FT_LOAD_RENDER); - if (error) { - continue; - } - - draw_bitmap( - buf, - stride, - height, - &textp->face->glyph->bitmap, - x + textp->face->glyph->bitmap_left, - y - textp->face->glyph->bitmap_top); - - x += textp->face->glyph->advance.x >> 6; - } - } -} diff --git a/internal/protocols/rpicamera/exe/text.h b/internal/protocols/rpicamera/exe/text.h deleted file mode 100644 index 235bf7a03f3..00000000000 --- a/internal/protocols/rpicamera/exe/text.h +++ /dev/null @@ -1,15 +0,0 @@ -#ifndef __TEXT_H__ -#define __TEXT_H__ - -#include -#include - -#include "parameters.h" - -typedef void text_t; - -const char *text_get_error(); -bool text_create(const parameters_t *params, text_t **text); -void text_draw(text_t *text, uint8_t *buf, int stride, int height); - -#endif diff --git a/internal/protocols/rpicamera/exe/window.c b/internal/protocols/rpicamera/exe/window.c deleted file mode 100644 index 83e653583ea..00000000000 --- a/internal/protocols/rpicamera/exe/window.c +++ /dev/null @@ -1,30 +0,0 @@ -#include -#include - -#include "window.h" - -bool window_load(const char *encoded, window_t *window) { - float vals[4]; - int i = 0; - char *token = strtok((char *)encoded, ","); - while (token != NULL) { - vals[i] = atof(token); - if (vals[i] < 0 || vals[i] > 1) { - return false; - } - - i++; - token = strtok(NULL, ","); - } - - if (i != 4) { - return false; - } - - window->x = vals[0]; - window->y = vals[1]; - window->width = vals[2]; - window->height = vals[3]; - - return true; -} diff --git a/internal/protocols/rpicamera/exe/window.h b/internal/protocols/rpicamera/exe/window.h deleted file mode 100644 index f27e470a227..00000000000 --- a/internal/protocols/rpicamera/exe/window.h +++ /dev/null @@ -1,15 +0,0 @@ -#ifndef __WINDOW_H__ -#define __WINDOW_H__ - -#include - -typedef struct { - float x; - float y; - float width; - float height; -} window_t; - -bool window_load(const char *encoded, window_t *window); - -#endif diff --git a/internal/protocols/rpicamera/rpicamera_disabled.go b/internal/protocols/rpicamera/rpicamera_disabled.go deleted file mode 100644 index 3bf10ce551f..00000000000 --- a/internal/protocols/rpicamera/rpicamera_disabled.go +++ /dev/null @@ -1,33 +0,0 @@ -//go:build !rpicamera -// +build !rpicamera - -// Package rpicamera allows to interact with a Raspberry Pi Camera. -package rpicamera - -import ( - "fmt" - "time" -) - -// Cleanup cleanups files created by the camera implementation. -func Cleanup() { -} - -// RPICamera is a RPI Camera reader. -type RPICamera struct { - Params Params - OnData func(time.Duration, [][]byte) -} - -// Initialize initializes a RPICamera. -func (c *RPICamera) Initialize() error { - return fmt.Errorf("server was compiled without support for the Raspberry Pi Camera") -} - -// Close closes a RPICamera. -func (c *RPICamera) Close() { -} - -// ReloadParams reloads the camera parameters. -func (c *RPICamera) ReloadParams(_ Params) { -} diff --git a/internal/servers/hls/hlsjsdownloader/main.go b/internal/servers/hls/hlsjsdownloader/main.go index 2f1fb557324..3dcf3df5c79 100644 --- a/internal/servers/hls/hlsjsdownloader/main.go +++ b/internal/servers/hls/hlsjsdownloader/main.go @@ -20,7 +20,6 @@ func do() error { if err != nil { return err } - version := strings.TrimSpace(string(buf)) log.Printf("downloading hls.js version %s...", version) @@ -40,12 +39,11 @@ func do() error { return err } - hashBuf, err := os.ReadFile("./hlsjsdownloader/HASH") + buf, err = os.ReadFile("./hlsjsdownloader/HASH") if err != nil { return err } - - str := strings.TrimSpace(string(hashBuf)) + str := strings.TrimSpace(string(buf)) hash, err := hex.DecodeString(str) if err != nil { diff --git a/internal/protocols/rpicamera/rpicamera.go b/internal/staticsources/rpicamera/camera.go similarity index 88% rename from internal/protocols/rpicamera/rpicamera.go rename to internal/staticsources/rpicamera/camera.go index d99e305b451..46bddb861ac 100644 --- a/internal/protocols/rpicamera/rpicamera.go +++ b/internal/staticsources/rpicamera/camera.go @@ -1,11 +1,10 @@ -//go:build rpicamera -// +build rpicamera +//go:build (linux && arm) || (linux && arm64) +// +build linux,arm linux,arm64 package rpicamera import ( "debug/elf" - _ "embed" "fmt" "os" "os/exec" @@ -19,12 +18,9 @@ import ( ) const ( - tempPathPrefix = "/dev/shm/rtspss-embeddedexe-" + tempPathPrefix = "/dev/shm/mediamtx-rpicamera-" ) -//go:embed exe/exe -var exeContent []byte - func startEmbeddedExe(content []byte, env []string) (*exec.Cmd, error) { tempPath := tempPathPrefix + strconv.FormatInt(time.Now().UnixNano(), 10) @@ -111,9 +107,8 @@ func checkLibraries64Bit() error { return nil } -// RPICamera is a RPI Camera reader. -type RPICamera struct { - Params Params +type camera struct { + Params params OnData func(time.Duration, [][]byte) cmd *exec.Cmd @@ -124,8 +119,7 @@ type RPICamera struct { readerDone chan error } -// Initialize initializes a RPICamera. -func (c *RPICamera) Initialize() error { +func (c *camera) initialize() error { if runtime.GOARCH == "arm" { err := checkLibraries64Bit() if err != nil { @@ -150,7 +144,7 @@ func (c *RPICamera) Initialize() error { "PIPE_VIDEO_FD=" + strconv.FormatInt(int64(c.pipeVideo.writeFD), 10), } - c.cmd, err = startEmbeddedExe(exeContent, env) + c.cmd, err = startEmbeddedExe(component, env) if err != nil { c.pipeConf.close() c.pipeVideo.close() @@ -194,7 +188,7 @@ func (c *RPICamera) Initialize() error { return nil } -func (c *RPICamera) Close() { +func (c *camera) close() { c.pipeConf.write([]byte{'e'}) <-c.waitDone c.pipeConf.close() @@ -202,11 +196,11 @@ func (c *RPICamera) Close() { <-c.readerDone } -func (c *RPICamera) ReloadParams(params Params) { +func (c *camera) reloadParams(params params) { c.pipeConf.write(append([]byte{'c'}, params.serialize()...)) } -func (c *RPICamera) readReady() error { +func (c *camera) readReady() error { buf, err := c.pipeVideo.read() if err != nil { return err @@ -224,7 +218,7 @@ func (c *RPICamera) readReady() error { } } -func (c *RPICamera) readData() error { +func (c *camera) readData() error { for { buf, err := c.pipeVideo.read() if err != nil { diff --git a/internal/staticsources/rpicamera/camera_disabled.go b/internal/staticsources/rpicamera/camera_disabled.go new file mode 100644 index 00000000000..60a726e25a0 --- /dev/null +++ b/internal/staticsources/rpicamera/camera_disabled.go @@ -0,0 +1,24 @@ +//go:build !linux || (!arm && !arm64) +// +build !linux !arm,!arm64 + +package rpicamera + +import ( + "fmt" + "time" +) + +type camera struct { + Params params + OnData func(time.Duration, [][]byte) +} + +func (c *camera) initialize() error { + return fmt.Errorf("server was compiled without support for the Raspberry Pi Camera") +} + +func (c *camera) close() { +} + +func (c *camera) reloadParams(_ params) { +} diff --git a/internal/staticsources/rpicamera/component.go b/internal/staticsources/rpicamera/component.go new file mode 100644 index 00000000000..918e554ec2a --- /dev/null +++ b/internal/staticsources/rpicamera/component.go @@ -0,0 +1,3 @@ +package rpicamera + +//go:generate go run ./mtxrpicamdownloader diff --git a/internal/staticsources/rpicamera/component_32.go b/internal/staticsources/rpicamera/component_32.go new file mode 100644 index 00000000000..0cdeffd9f69 --- /dev/null +++ b/internal/staticsources/rpicamera/component_32.go @@ -0,0 +1,11 @@ +//go:build linux && arm +// +build linux,arm + +package rpicamera + +import ( + _ "embed" +) + +//go:embed mtxrpicam_32 +var component []byte diff --git a/internal/staticsources/rpicamera/component_64.go b/internal/staticsources/rpicamera/component_64.go new file mode 100644 index 00000000000..d1c562a027b --- /dev/null +++ b/internal/staticsources/rpicamera/component_64.go @@ -0,0 +1,11 @@ +//go:build linux && arm64 +// +build linux,arm64 + +package rpicamera + +import ( + _ "embed" +) + +//go:embed mtxrpicam_64 +var component []byte diff --git a/internal/staticsources/rpicamera/mtxrpicamdownloader/VERSION b/internal/staticsources/rpicamera/mtxrpicamdownloader/VERSION new file mode 100644 index 00000000000..0ec25f7505c --- /dev/null +++ b/internal/staticsources/rpicamera/mtxrpicamdownloader/VERSION @@ -0,0 +1 @@ +v1.0.0 diff --git a/internal/staticsources/rpicamera/mtxrpicamdownloader/main.go b/internal/staticsources/rpicamera/mtxrpicamdownloader/main.go new file mode 100644 index 00000000000..6f8ee9bce2a --- /dev/null +++ b/internal/staticsources/rpicamera/mtxrpicamdownloader/main.go @@ -0,0 +1,53 @@ +// Package main contains an utility to download hls.js +package main + +import ( + "fmt" + "io" + "log" + "net/http" + "os" + "strings" +) + +func do() error { + buf, err := os.ReadFile("./mtxrpicamdownloader/VERSION") + if err != nil { + return err + } + version := strings.TrimSpace(string(buf)) + + log.Printf("downloading mediamtx-rpicamera version %s...", version) + + for _, f := range []string{"mtxrpicam_32", "mtxrpicam_64"} { + res, err := http.Get("https://github.com/bluenviron/mediamtx-rpicamera/releases/download/" + version + "/" + f) + if err != nil { + return err + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + return fmt.Errorf("bad status code: %v", res.StatusCode) + } + + buf, err := io.ReadAll(res.Body) + if err != nil { + return err + } + + if err = os.WriteFile(f, buf, 0o644); err != nil { + return err + } + } + + log.Println("ok") + return nil +} + +func main() { + err := do() + if err != nil { + log.Printf("ERR: %v", err) + os.Exit(1) + } +} diff --git a/internal/protocols/rpicamera/params.go b/internal/staticsources/rpicamera/params.go similarity index 53% rename from internal/protocols/rpicamera/params.go rename to internal/staticsources/rpicamera/params.go index 5262d36f1ff..c847a388545 100644 --- a/internal/protocols/rpicamera/params.go +++ b/internal/staticsources/rpicamera/params.go @@ -1,14 +1,6 @@ package rpicamera -import ( - "encoding/base64" - "reflect" - "strconv" - "strings" -) - -// Params is a set of camera parameters. -type Params struct { +type params struct { LogLevel string CameraID int Width int @@ -45,40 +37,3 @@ type Params struct { TextOverlayEnable bool TextOverlay string } - -func (p Params) serialize() []byte { //nolint:unused - rv := reflect.ValueOf(p) - rt := rv.Type() - nf := rv.NumField() - ret := make([]string, nf) - - for i := 0; i < nf; i++ { - entry := rt.Field(i).Name + ":" - f := rv.Field(i) - - switch f.Kind() { - case reflect.Int: - entry += strconv.FormatInt(f.Int(), 10) - - case reflect.Float64: - entry += strconv.FormatFloat(f.Float(), 'f', -1, 64) - - case reflect.String: - entry += base64.StdEncoding.EncodeToString([]byte(f.String())) - - case reflect.Bool: - if f.Bool() { - entry += "1" - } else { - entry += "0" - } - - default: - panic("unhandled type") - } - - ret[i] = entry - } - - return []byte(strings.Join(ret, " ")) -} diff --git a/internal/staticsources/rpicamera/params_serialize.go b/internal/staticsources/rpicamera/params_serialize.go new file mode 100644 index 00000000000..362f6f8f1ee --- /dev/null +++ b/internal/staticsources/rpicamera/params_serialize.go @@ -0,0 +1,48 @@ +//go:build (linux && arm) || (linux && arm64) +// +build linux,arm linux,arm64 + +package rpicamera + +import ( + "encoding/base64" + "reflect" + "strconv" + "strings" +) + +func (p params) serialize() []byte { + rv := reflect.ValueOf(p) + rt := rv.Type() + nf := rv.NumField() + ret := make([]string, nf) + + for i := 0; i < nf; i++ { + entry := rt.Field(i).Name + ":" + f := rv.Field(i) + + switch f.Kind() { + case reflect.Int: + entry += strconv.FormatInt(f.Int(), 10) + + case reflect.Float64: + entry += strconv.FormatFloat(f.Float(), 'f', -1, 64) + + case reflect.String: + entry += base64.StdEncoding.EncodeToString([]byte(f.String())) + + case reflect.Bool: + if f.Bool() { + entry += "1" + } else { + entry += "0" + } + + default: + panic("unhandled type") + } + + ret[i] = entry + } + + return []byte(strings.Join(ret, " ")) +} diff --git a/internal/protocols/rpicamera/pipe.go b/internal/staticsources/rpicamera/pipe.go similarity index 93% rename from internal/protocols/rpicamera/pipe.go rename to internal/staticsources/rpicamera/pipe.go index 4b037e8c996..926ad77c72e 100644 --- a/internal/protocols/rpicamera/pipe.go +++ b/internal/staticsources/rpicamera/pipe.go @@ -1,5 +1,5 @@ -//go:build rpicamera -// +build rpicamera +//go:build (linux && arm) || (linux && arm64) +// +build linux,arm linux,arm64 package rpicamera diff --git a/internal/staticsources/rpicamera/source.go b/internal/staticsources/rpicamera/source.go index 924a9ab340a..1a4d49c6b2f 100644 --- a/internal/staticsources/rpicamera/source.go +++ b/internal/staticsources/rpicamera/source.go @@ -10,13 +10,12 @@ import ( "github.com/bluenviron/mediamtx/internal/conf" "github.com/bluenviron/mediamtx/internal/defs" "github.com/bluenviron/mediamtx/internal/logger" - "github.com/bluenviron/mediamtx/internal/protocols/rpicamera" "github.com/bluenviron/mediamtx/internal/stream" "github.com/bluenviron/mediamtx/internal/unit" ) -func paramsFromConf(logLevel conf.LogLevel, cnf *conf.Path) rpicamera.Params { - return rpicamera.Params{ +func paramsFromConf(logLevel conf.LogLevel, cnf *conf.Path) params { + return params{ LogLevel: func() string { switch logLevel { case conf.LogLevel(logger.Debug): @@ -110,15 +109,15 @@ func (s *Source) Run(params defs.StaticSourceRunParams) error { }) } - cam := &rpicamera.RPICamera{ + cam := &camera{ Params: paramsFromConf(s.LogLevel, params.Conf), OnData: onData, } - err := cam.Initialize() + err := cam.initialize() if err != nil { return err } - defer cam.Close() + defer cam.close() defer func() { if stream != nil { @@ -129,7 +128,7 @@ func (s *Source) Run(params defs.StaticSourceRunParams) error { for { select { case cnf := <-params.ReloadConf: - cam.ReloadParams(paramsFromConf(s.LogLevel, cnf)) + cam.reloadParams(paramsFromConf(s.LogLevel, cnf)) case <-params.Context.Done(): return nil diff --git a/scripts/binaries.mk b/scripts/binaries.mk index b49d755cbd0..aaed26c0538 100644 --- a/scripts/binaries.mk +++ b/scripts/binaries.mk @@ -1,20 +1,6 @@ BINARY_NAME = mediamtx define DOCKERFILE_BINARIES -FROM $(RPI32_IMAGE) AS rpicamera32 -RUN ["cross-build-start"] -RUN apt update && apt install -y --no-install-recommends g++ pkg-config make libcamera-dev libfreetype-dev xxd wget -WORKDIR /s/internal/protocols/rpicamera/exe -COPY internal/protocols/rpicamera/exe . -RUN make -j$$(nproc) - -FROM $(RPI64_IMAGE) AS rpicamera64 -RUN ["cross-build-start"] -RUN apt update && apt install -y --no-install-recommends g++ pkg-config make libcamera-dev libfreetype-dev xxd wget -WORKDIR /s/internal/protocols/rpicamera/exe -COPY internal/protocols/rpicamera/exe . -RUN make -j$$(nproc) - FROM $(BASE_IMAGE) AS build-base RUN apk add --no-cache zip make git tar WORKDIR /s @@ -29,38 +15,39 @@ RUN cp mediamtx.yml LICENSE tmp/ RUN go generate ./... FROM build-base AS build-windows-amd64 -RUN GOOS=windows GOARCH=amd64 go build -ldflags "-X github.com/bluenviron/mediamtx/internal/core.version=$$VERSION" -o tmp/$(BINARY_NAME).exe +ENV GOOS=windows GOARCH=amd64 +RUN go build -ldflags "-X github.com/bluenviron/mediamtx/internal/core.version=$$VERSION" -o tmp/$(BINARY_NAME).exe RUN cd tmp && zip -q ../binaries/$(BINARY_NAME)_$${VERSION}_windows_amd64.zip $(BINARY_NAME).exe mediamtx.yml LICENSE FROM build-base AS build-linux-amd64 -RUN GOOS=linux GOARCH=amd64 go build -ldflags "-X github.com/bluenviron/mediamtx/internal/core.version=$$VERSION" -o tmp/$(BINARY_NAME) +ENV GOOS=linux GOARCH=amd64 +RUN go build -ldflags "-X github.com/bluenviron/mediamtx/internal/core.version=$$VERSION" -o tmp/$(BINARY_NAME) RUN tar -C tmp -czf binaries/$(BINARY_NAME)_$${VERSION}_linux_amd64.tar.gz --owner=0 --group=0 $(BINARY_NAME) mediamtx.yml LICENSE FROM build-base AS build-darwin-amd64 -RUN GOOS=darwin GOARCH=amd64 go build -ldflags "-X github.com/bluenviron/mediamtx/internal/core.version=$$VERSION" -o tmp/$(BINARY_NAME) +ENV GOOS=darwin GOARCH=amd64 +RUN go build -ldflags "-X github.com/bluenviron/mediamtx/internal/core.version=$$VERSION" -o tmp/$(BINARY_NAME) RUN tar -C tmp -czf binaries/$(BINARY_NAME)_$${VERSION}_darwin_amd64.tar.gz --owner=0 --group=0 $(BINARY_NAME) mediamtx.yml LICENSE FROM build-base AS build-darwin-arm64 -RUN GOOS=darwin GOARCH=arm64 go build -ldflags "-X github.com/bluenviron/mediamtx/internal/core.version=$$VERSION" -o tmp/$(BINARY_NAME) +ENV GOOS=darwin GOARCH=arm64 +RUN go build -ldflags "-X github.com/bluenviron/mediamtx/internal/core.version=$$VERSION" -o tmp/$(BINARY_NAME) RUN tar -C tmp -czf binaries/$(BINARY_NAME)_$${VERSION}_darwin_arm64.tar.gz --owner=0 --group=0 $(BINARY_NAME) mediamtx.yml LICENSE FROM build-base AS build-linux-armv6 -COPY --from=rpicamera32 /s/internal/protocols/rpicamera/exe/exe internal/protocols/rpicamera/exe/ -RUN GOOS=linux GOARCH=arm GOARM=6 go build -ldflags "-X github.com/bluenviron/mediamtx/internal/core.version=$$VERSION" -o tmp/$(BINARY_NAME) -tags rpicamera +ENV GOOS=linux GOARCH=arm GOARM=6 +RUN go build -ldflags "-X github.com/bluenviron/mediamtx/internal/core.version=$$VERSION" -o tmp/$(BINARY_NAME) RUN tar -C tmp -czf binaries/$(BINARY_NAME)_$${VERSION}_linux_armv6.tar.gz --owner=0 --group=0 $(BINARY_NAME) mediamtx.yml LICENSE -RUN rm internal/protocols/rpicamera/exe/exe FROM build-base AS build-linux-armv7 -COPY --from=rpicamera32 /s/internal/protocols/rpicamera/exe/exe internal/protocols/rpicamera/exe/ -RUN GOOS=linux GOARCH=arm GOARM=7 go build -ldflags "-X github.com/bluenviron/mediamtx/internal/core.version=$$VERSION" -o tmp/$(BINARY_NAME) -tags rpicamera +ENV GOOS=linux GOARCH=arm GOARM=7 +RUN go build -ldflags "-X github.com/bluenviron/mediamtx/internal/core.version=$$VERSION" -o tmp/$(BINARY_NAME) RUN tar -C tmp -czf binaries/$(BINARY_NAME)_$${VERSION}_linux_armv7.tar.gz --owner=0 --group=0 $(BINARY_NAME) mediamtx.yml LICENSE -RUN rm internal/protocols/rpicamera/exe/exe FROM build-base AS build-linux-arm64 -COPY --from=rpicamera64 /s/internal/protocols/rpicamera/exe/exe internal/protocols/rpicamera/exe/ -RUN GOOS=linux GOARCH=arm64 go build -ldflags "-X github.com/bluenviron/mediamtx/internal/core.version=$$VERSION" -o tmp/$(BINARY_NAME) -tags rpicamera +ENV GOOS=linux GOARCH=arm64 +RUN go build -ldflags "-X github.com/bluenviron/mediamtx/internal/core.version=$$VERSION" -o tmp/$(BINARY_NAME) RUN tar -C tmp -czf binaries/$(BINARY_NAME)_$${VERSION}_linux_arm64v8.tar.gz --owner=0 --group=0 $(BINARY_NAME) mediamtx.yml LICENSE -RUN rm internal/protocols/rpicamera/exe/exe FROM $(BASE_IMAGE) COPY --from=build-windows-amd64 /s/binaries /s/binaries From 6256d0b893813a0953f95a8f2e05b2b52a8a79c2 Mon Sep 17 00:00:00 2001 From: Alessandro Ros Date: Sun, 18 Aug 2024 19:12:26 +0200 Subject: [PATCH 27/88] rpi: embed libcamera and libfreetype into the server (#2581) (#3665) --- README.md | 20 +-- internal/staticsources/rpicamera/camera.go | 123 +++--------------- internal/staticsources/rpicamera/component.go | 104 +++++++++++++++ .../staticsources/rpicamera/component_32.go | 6 +- .../staticsources/rpicamera/component_64.go | 6 +- .../staticsources/rpicamera/component_dl.go | 3 + .../rpicamera/mtxrpicamdownloader/VERSION | 2 +- .../rpicamera/mtxrpicamdownloader/main.go | 57 +++++++- scripts/dockerhub.mk | 2 - 9 files changed, 195 insertions(+), 128 deletions(-) create mode 100644 internal/staticsources/rpicamera/component_dl.go diff --git a/README.md b/README.md index c5b86d619ee..bc3d6f6b9c5 100644 --- a/README.md +++ b/README.md @@ -471,20 +471,20 @@ The resulting stream will be available in path `/cam`. _MediaMTX_ natively supports the Raspberry Pi Camera, enabling high-quality and low-latency video streaming from the camera to any user, for any purpose. There are a couple of requirements: -1. The server must run on a Raspberry Pi, with Raspberry Pi OS Bullseye as operative system. Both 32 bit and 64 bit architectures are supported. +1. The server must run on a Raspberry Pi, with one of the following operating systems: -2. Make sure that the legacy camera stack is disabled. Type `sudo raspi-config`, then go to `Interfacing options`, `enable/disable legacy camera support`, choose `no`. Reboot the system. + * Raspberry Pi OS Bookworm + * Raspberry Pi OS Bullseye -If you want to run the standard (non-Docker) version of the server: + Both 32 bit and 64 bit architectures are supported. -1. Make sure that the following packages are installed: +2. If you are using Raspberry Pi OS Bullseye, make sure that the legacy camera stack is disabled. Type `sudo raspi-config`, then go to `Interfacing options`, `enable/disable legacy camera support`, choose `no`. Reboot the system. - * `libcamera0` (≥ 0.0.5) - * `libfreetype6` +If you want to run the standard (non-Docker) version of the server: -2. download the server executable. If you're using 64-bit version of the operative system, make sure to pick the `arm64` variant. +1. Download the server executable. If you're using 64-bit version of the operative system, make sure to pick the `arm64` variant. -3. edit `mediamtx.yml` and replace everything inside section `paths` with the following content: +2. Edit `mediamtx.yml` and replace everything inside section `paths` with the following content: ```yml paths: @@ -494,7 +494,7 @@ If you want to run the standard (non-Docker) version of the server: The resulting stream will be available in path `/cam`. -If you want to run the server inside Docker, you need to use the `latest-rpi` image (that already contains required libraries) and launch the container with some additional flags: +If you want to run the server inside Docker, you need to use the `latest-rpi` image and launch the container with some additional flags: ```sh docker run --rm -it \ @@ -506,7 +506,7 @@ docker run --rm -it \ bluenviron/mediamtx:latest-rpi ``` -Be aware that the Docker image is not compatible with cameras that requires a custom `libcamera` (like some ArduCam products), since it comes with a standard `libcamera` included. +Be aware that the server is not compatible with cameras that requires a custom `libcamera` (like some ArduCam products), since it comes with a bundled `libcamera`. If you want to use a custom one, you can [compile from source](#compile-from-source). Camera settings can be changed by using the `rpiCamera*` parameters: diff --git a/internal/staticsources/rpicamera/camera.go b/internal/staticsources/rpicamera/camera.go index 46bddb861ac..b4f31479fcc 100644 --- a/internal/staticsources/rpicamera/camera.go +++ b/internal/staticsources/rpicamera/camera.go @@ -4,109 +4,16 @@ package rpicamera import ( - "debug/elf" "fmt" "os" "os/exec" - "runtime" + "path/filepath" "strconv" - "strings" - "sync" "time" "github.com/bluenviron/mediacommon/pkg/codecs/h264" ) -const ( - tempPathPrefix = "/dev/shm/mediamtx-rpicamera-" -) - -func startEmbeddedExe(content []byte, env []string) (*exec.Cmd, error) { - tempPath := tempPathPrefix + strconv.FormatInt(time.Now().UnixNano(), 10) - - err := os.WriteFile(tempPath, content, 0o755) - if err != nil { - return nil, err - } - - cmd := exec.Command(tempPath) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - cmd.Env = env - - err = cmd.Start() - os.Remove(tempPath) - - if err != nil { - return nil, err - } - - return cmd, nil -} - -func findLibrary(name string) (string, error) { - byts, err := exec.Command("ldconfig", "-p").Output() - if err == nil { - for _, line := range strings.Split(string(byts), "\n") { - f := strings.Split(line, " => ") - if len(f) == 2 && strings.Contains(f[1], name+".so") { - return f[1], nil - } - } - } - - return "", fmt.Errorf("library '%s' not found", name) -} - -func check64bit(fpath string) error { - f, err := os.Open(fpath) - if err != nil { - return err - } - defer f.Close() - - ef, err := elf.NewFile(f) - if err != nil { - return err - } - defer ef.Close() - - if ef.FileHeader.Class == elf.ELFCLASS64 { - return fmt.Errorf("libcamera is 64-bit, you need the 64-bit server version") - } - - return nil -} - -var ( - mutex sync.Mutex - checked bool -) - -func checkLibraries64Bit() error { - mutex.Lock() - defer mutex.Unlock() - - if checked { - return nil - } - - for _, name := range []string{"libcamera", "libcamera-base"} { - lib, err := findLibrary(name) - if err != nil { - return err - } - - err = check64bit(lib) - if err != nil { - return err - } - } - - checked = true - return nil -} - type camera struct { Params params OnData func(time.Duration, [][]byte) @@ -120,34 +27,41 @@ type camera struct { } func (c *camera) initialize() error { - if runtime.GOARCH == "arm" { - err := checkLibraries64Bit() - if err != nil { - return err - } + err := dumpComponent() + if err != nil { + return err } - var err error c.pipeConf, err = newPipe() if err != nil { + freeComponent() return err } c.pipeVideo, err = newPipe() if err != nil { c.pipeConf.close() + freeComponent() return err } env := []string{ "PIPE_CONF_FD=" + strconv.FormatInt(int64(c.pipeConf.readFD), 10), "PIPE_VIDEO_FD=" + strconv.FormatInt(int64(c.pipeVideo.writeFD), 10), + "LD_LIBRARY_PATH=" + dumpPath, } - c.cmd, err = startEmbeddedExe(component, env) + c.cmd = exec.Command(filepath.Join(dumpPath, "exe")) + c.cmd.Stdout = os.Stdout + c.cmd.Stderr = os.Stderr + c.cmd.Env = env + c.cmd.Dir = dumpPath + + err = c.cmd.Start() if err != nil { c.pipeConf.close() c.pipeVideo.close() + freeComponent() return err } @@ -164,11 +78,12 @@ func (c *camera) initialize() error { }() select { - case <-c.waitDone: + case err := <-c.waitDone: c.pipeConf.close() c.pipeVideo.close() <-c.readerDone - return fmt.Errorf("process exited unexpectedly") + freeComponent() + return fmt.Errorf("process exited unexpectedly: %v", err) case err := <-c.readerDone: if err != nil { @@ -176,6 +91,7 @@ func (c *camera) initialize() error { <-c.waitDone c.pipeConf.close() c.pipeVideo.close() + freeComponent() return err } } @@ -194,6 +110,7 @@ func (c *camera) close() { c.pipeConf.close() c.pipeVideo.close() <-c.readerDone + freeComponent() } func (c *camera) reloadParams(params params) { diff --git a/internal/staticsources/rpicamera/component.go b/internal/staticsources/rpicamera/component.go index 918e554ec2a..35d2ec652c4 100644 --- a/internal/staticsources/rpicamera/component.go +++ b/internal/staticsources/rpicamera/component.go @@ -1,3 +1,107 @@ +//go:build (linux && arm) || (linux && arm64) +// +build linux,arm linux,arm64 + package rpicamera +import ( + "os" + "path/filepath" + "strconv" + "sync" + "time" +) + //go:generate go run ./mtxrpicamdownloader + +const ( + dumpPrefix = "/dev/shm/mediamtx-rpicamera-" +) + +var ( + dumpMutex sync.Mutex + dumpCount = 0 + dumpPath = "" +) + +func dumpEmbedFSRecursive(src string, dest string) error { + files, err := component.ReadDir(src) + if err != nil { + return err + } + + for _, f := range files { + if f.IsDir() { + err = os.Mkdir(filepath.Join(dest, f.Name()), 0o755) + if err != nil { + return err + } + + err = dumpEmbedFSRecursive(filepath.Join(src, f.Name()), filepath.Join(dest, f.Name())) + if err != nil { + return err + } + } else { + buf, err := component.ReadFile(filepath.Join(src, f.Name())) + if err != nil { + return err + } + + err = os.WriteFile(filepath.Join(dest, f.Name()), buf, 0o644) + if err != nil { + return err + } + } + } + + return nil +} + +func dumpComponent() error { + dumpMutex.Lock() + defer dumpMutex.Unlock() + + if dumpCount > 0 { + dumpCount++ + return nil + } + + dumpPath = dumpPrefix + strconv.FormatInt(time.Now().UnixNano(), 10) + + err := os.Mkdir(dumpPath, 0o755) + if err != nil { + return err + } + + files, err := component.ReadDir(".") + if err != nil { + os.RemoveAll(dumpPath) + return err + } + + err = dumpEmbedFSRecursive(files[0].Name(), dumpPath) + if err != nil { + os.RemoveAll(dumpPath) + return err + } + + err = os.Chmod(filepath.Join(dumpPath, "exe"), 0o755) + if err != nil { + os.RemoveAll(dumpPath) + return err + } + + dumpCount++ + + return nil +} + +func freeComponent() { + dumpMutex.Lock() + defer dumpMutex.Unlock() + + dumpCount-- + + if dumpCount == 0 { + os.RemoveAll(dumpPath) + } +} diff --git a/internal/staticsources/rpicamera/component_32.go b/internal/staticsources/rpicamera/component_32.go index 0cdeffd9f69..d91cce1c966 100644 --- a/internal/staticsources/rpicamera/component_32.go +++ b/internal/staticsources/rpicamera/component_32.go @@ -4,8 +4,8 @@ package rpicamera import ( - _ "embed" + "embed" ) -//go:embed mtxrpicam_32 -var component []byte +//go:embed mtxrpicam_32/* +var component embed.FS diff --git a/internal/staticsources/rpicamera/component_64.go b/internal/staticsources/rpicamera/component_64.go index d1c562a027b..cdc924dd04c 100644 --- a/internal/staticsources/rpicamera/component_64.go +++ b/internal/staticsources/rpicamera/component_64.go @@ -4,8 +4,8 @@ package rpicamera import ( - _ "embed" + "embed" ) -//go:embed mtxrpicam_64 -var component []byte +//go:embed mtxrpicam_64/* +var component embed.FS diff --git a/internal/staticsources/rpicamera/component_dl.go b/internal/staticsources/rpicamera/component_dl.go new file mode 100644 index 00000000000..918e554ec2a --- /dev/null +++ b/internal/staticsources/rpicamera/component_dl.go @@ -0,0 +1,3 @@ +package rpicamera + +//go:generate go run ./mtxrpicamdownloader diff --git a/internal/staticsources/rpicamera/mtxrpicamdownloader/VERSION b/internal/staticsources/rpicamera/mtxrpicamdownloader/VERSION index 0ec25f7505c..46b105a30dc 100644 --- a/internal/staticsources/rpicamera/mtxrpicamdownloader/VERSION +++ b/internal/staticsources/rpicamera/mtxrpicamdownloader/VERSION @@ -1 +1 @@ -v1.0.0 +v2.0.0 diff --git a/internal/staticsources/rpicamera/mtxrpicamdownloader/main.go b/internal/staticsources/rpicamera/mtxrpicamdownloader/main.go index 6f8ee9bce2a..30d382927e3 100644 --- a/internal/staticsources/rpicamera/mtxrpicamdownloader/main.go +++ b/internal/staticsources/rpicamera/mtxrpicamdownloader/main.go @@ -2,6 +2,9 @@ package main import ( + "archive/tar" + "compress/gzip" + "errors" "fmt" "io" "log" @@ -10,6 +13,47 @@ import ( "strings" ) +func dumpTar(src io.Reader) error { + uncompressed, err := gzip.NewReader(src) + if err != nil { + return err + } + + tr := tar.NewReader(uncompressed) + + for { + header, err := tr.Next() + if err != nil { + if errors.Is(err, io.EOF) { + break + } + return err + } + + switch header.Typeflag { + case tar.TypeDir: + err = os.Mkdir(header.Name, header.FileInfo().Mode()) + if err != nil { + return err + } + + case tar.TypeReg: + f, err := os.OpenFile(header.Name, os.O_WRONLY|os.O_CREATE, header.FileInfo().Mode()) + if err != nil { + return err + } + defer f.Close() + + _, err = io.Copy(f, tr) + if err != nil { + return err + } + } + } + + return nil +} + func do() error { buf, err := os.ReadFile("./mtxrpicamdownloader/VERSION") if err != nil { @@ -19,7 +63,12 @@ func do() error { log.Printf("downloading mediamtx-rpicamera version %s...", version) - for _, f := range []string{"mtxrpicam_32", "mtxrpicam_64"} { + for _, f := range []string{"mtxrpicam_32.tar.gz", "mtxrpicam_64.tar.gz"} { + err = os.RemoveAll(strings.TrimSuffix(f, ".tar.gz")) + if err != nil { + return err + } + res, err := http.Get("https://github.com/bluenviron/mediamtx-rpicamera/releases/download/" + version + "/" + f) if err != nil { return err @@ -30,14 +79,10 @@ func do() error { return fmt.Errorf("bad status code: %v", res.StatusCode) } - buf, err := io.ReadAll(res.Body) + err = dumpTar(res.Body) if err != nil { return err } - - if err = os.WriteFile(f, buf, 0o644); err != nil { - return err - } } log.Println("ok") diff --git a/scripts/dockerhub.mk b/scripts/dockerhub.mk index 4d26a79ebbc..d7905245840 100644 --- a/scripts/dockerhub.mk +++ b/scripts/dockerhub.mk @@ -19,13 +19,11 @@ export DOCKERFILE_DOCKERHUB_FFMPEG define DOCKERFILE_DOCKERHUB_RPI_BASE_32 FROM $(RPI32_IMAGE) -RUN apt update && apt install -y --no-install-recommends libcamera0 libfreetype6 && rm -rf /var/lib/apt/lists/* endef export DOCKERFILE_DOCKERHUB_RPI_BASE_32 define DOCKERFILE_DOCKERHUB_RPI_BASE_64 FROM $(RPI64_IMAGE) -RUN apt update && apt install -y --no-install-recommends libcamera0 libfreetype6 && rm -rf /var/lib/apt/lists/* endef export DOCKERFILE_DOCKERHUB_RPI_BASE_64 From f4051eb63d56298fb752aebcffd90f5f9cfb2a72 Mon Sep 17 00:00:00 2001 From: Alessandro Ros Date: Mon, 19 Aug 2024 11:22:54 +0200 Subject: [PATCH 28/88] rpi: add rpiCameraFlickerPeriod (#3463) (#3667) --- apidocs/openapi.yaml | 2 ++ internal/conf/path.go | 1 + internal/core/path_manager.go | 1 + internal/staticsources/rpicamera/mtxrpicamdownloader/VERSION | 2 +- internal/staticsources/rpicamera/params.go | 1 + internal/staticsources/rpicamera/source.go | 1 + mediamtx.yml | 2 ++ 7 files changed, 9 insertions(+), 1 deletion(-) diff --git a/apidocs/openapi.yaml b/apidocs/openapi.yaml index a13c5795b06..4e0ce1b5cf9 100644 --- a/apidocs/openapi.yaml +++ b/apidocs/openapi.yaml @@ -420,6 +420,8 @@ components: type: number rpiCameraAfWindow: type: string + rpiCameraFlickerPeriod: + type: integer rpiCameraTextOverlayEnable: type: boolean rpiCameraTextOverlay: diff --git a/internal/conf/path.go b/internal/conf/path.go index 925f050f439..242d1309181 100644 --- a/internal/conf/path.go +++ b/internal/conf/path.go @@ -161,6 +161,7 @@ type Path struct { RPICameraAfSpeed string `json:"rpiCameraAfSpeed"` RPICameraLensPosition float64 `json:"rpiCameraLensPosition"` RPICameraAfWindow string `json:"rpiCameraAfWindow"` + RPICameraFlickerPeriod int `json:"rpiCameraFlickerPeriod"` RPICameraTextOverlayEnable bool `json:"rpiCameraTextOverlayEnable"` RPICameraTextOverlay string `json:"rpiCameraTextOverlay"` diff --git a/internal/core/path_manager.go b/internal/core/path_manager.go index ae78ba1b1e2..623ac6a7c65 100644 --- a/internal/core/path_manager.go +++ b/internal/core/path_manager.go @@ -24,6 +24,7 @@ func pathConfCanBeUpdated(oldPathConf *conf.Path, newPathConf *conf.Path) bool { clone.RPICameraSaturation = newPathConf.RPICameraSaturation clone.RPICameraSharpness = newPathConf.RPICameraSharpness clone.RPICameraExposure = newPathConf.RPICameraExposure + clone.RPICameraFlickerPeriod = newPathConf.RPICameraFlickerPeriod clone.RPICameraAWB = newPathConf.RPICameraAWB clone.RPICameraAWBGains = newPathConf.RPICameraAWBGains clone.RPICameraDenoise = newPathConf.RPICameraDenoise diff --git a/internal/staticsources/rpicamera/mtxrpicamdownloader/VERSION b/internal/staticsources/rpicamera/mtxrpicamdownloader/VERSION index 46b105a30dc..1defe531bfa 100644 --- a/internal/staticsources/rpicamera/mtxrpicamdownloader/VERSION +++ b/internal/staticsources/rpicamera/mtxrpicamdownloader/VERSION @@ -1 +1 @@ -v2.0.0 +v2.1.0 diff --git a/internal/staticsources/rpicamera/params.go b/internal/staticsources/rpicamera/params.go index c847a388545..b5460d3ec6d 100644 --- a/internal/staticsources/rpicamera/params.go +++ b/internal/staticsources/rpicamera/params.go @@ -34,6 +34,7 @@ type params struct { AfSpeed string LensPosition float64 AfWindow string + FlickerPeriod int TextOverlayEnable bool TextOverlay string } diff --git a/internal/staticsources/rpicamera/source.go b/internal/staticsources/rpicamera/source.go index 1a4d49c6b2f..6f2c72b6ae4 100644 --- a/internal/staticsources/rpicamera/source.go +++ b/internal/staticsources/rpicamera/source.go @@ -59,6 +59,7 @@ func paramsFromConf(logLevel conf.LogLevel, cnf *conf.Path) params { AfSpeed: cnf.RPICameraAfSpeed, LensPosition: cnf.RPICameraLensPosition, AfWindow: cnf.RPICameraAfWindow, + FlickerPeriod: cnf.RPICameraFlickerPeriod, TextOverlayEnable: cnf.RPICameraTextOverlayEnable, TextOverlay: cnf.RPICameraTextOverlay, } diff --git a/mediamtx.yml b/mediamtx.yml index f936a305d3e..9bc18f1293f 100644 --- a/mediamtx.yml +++ b/mediamtx.yml @@ -582,6 +582,8 @@ pathDefaults: # Specifies the autofocus window, in the form x,y,width,height where the coordinates # are given as a proportion of the entire image. rpiCameraAfWindow: + # Manual flicker correction period, in microseconds. + rpiCameraFlickerPeriod: 0 # enables printing text on each frame. rpiCameraTextOverlayEnable: false # text that is printed on each frame. From 41a3fd503d1d88e0879b2b42eab4f948ef43ea25 Mon Sep 17 00:00:00 2001 From: Alessandro Ros Date: Wed, 21 Aug 2024 00:05:40 +0200 Subject: [PATCH 29/88] rpi: add H264 software encoder (#2581) (#3670) This allows to use the RPI camera on the Raspberry Pi 5 too. --- apidocs/openapi.yaml | 18 +++--- internal/conf/conf_test.go | 9 +-- internal/conf/path.go | 23 +++++--- .../rpicamera/mtxrpicamdownloader/VERSION | 2 +- internal/staticsources/rpicamera/params.go | 9 +-- internal/staticsources/rpicamera/source.go | 9 +-- mediamtx.yml | 58 ++++++++++--------- 7 files changed, 71 insertions(+), 57 deletions(-) diff --git a/apidocs/openapi.yaml b/apidocs/openapi.yaml index 4e0ce1b5cf9..94fd864a909 100644 --- a/apidocs/openapi.yaml +++ b/apidocs/openapi.yaml @@ -402,14 +402,6 @@ components: type: string rpiCameraFPS: type: number - rpiCameraIDRPeriod: - type: integer - rpiCameraBitrate: - type: integer - rpiCameraProfile: - type: string - rpiCameraLevel: - type: string rpiCameraAfMode: type: string rpiCameraAfRange: @@ -426,6 +418,16 @@ components: type: boolean rpiCameraTextOverlay: type: string + rpiCameraCodec: + type: string + rpiCameraIDRPeriod: + type: integer + rpiCameraBitrate: + type: integer + rpiCameraProfile: + type: string + rpiCameraLevel: + type: string # Hooks runOnInit: diff --git a/internal/conf/conf_test.go b/internal/conf/conf_test.go index f0d1529ad85..88982317d57 100644 --- a/internal/conf/conf_test.go +++ b/internal/conf/conf_test.go @@ -69,14 +69,15 @@ func TestConfFromFile(t *testing.T) { RPICameraDenoise: "off", RPICameraMetering: "centre", RPICameraFPS: 30, - RPICameraIDRPeriod: 60, - RPICameraBitrate: 1000000, - RPICameraProfile: "main", - RPICameraLevel: "4.1", RPICameraAfMode: "continuous", RPICameraAfRange: "normal", RPICameraAfSpeed: "normal", RPICameraTextOverlay: "%Y-%m-%d %H:%M:%S - MediaMTX", + RPICameraCodec: "auto", + RPICameraIDRPeriod: 60, + RPICameraBitrate: 1000000, + RPICameraProfile: "main", + RPICameraLevel: "4.1", RunOnDemandStartTimeout: 5 * StringDuration(time.Second), RunOnDemandCloseAfter: 10 * StringDuration(time.Second), }, pa) diff --git a/internal/conf/path.go b/internal/conf/path.go index 242d1309181..2cff904e749 100644 --- a/internal/conf/path.go +++ b/internal/conf/path.go @@ -152,10 +152,6 @@ type Path struct { RPICameraTuningFile string `json:"rpiCameraTuningFile"` RPICameraMode string `json:"rpiCameraMode"` RPICameraFPS float64 `json:"rpiCameraFPS"` - RPICameraIDRPeriod int `json:"rpiCameraIDRPeriod"` - RPICameraBitrate int `json:"rpiCameraBitrate"` - RPICameraProfile string `json:"rpiCameraProfile"` - RPICameraLevel string `json:"rpiCameraLevel"` RPICameraAfMode string `json:"rpiCameraAfMode"` RPICameraAfRange string `json:"rpiCameraAfRange"` RPICameraAfSpeed string `json:"rpiCameraAfSpeed"` @@ -164,6 +160,11 @@ type Path struct { RPICameraFlickerPeriod int `json:"rpiCameraFlickerPeriod"` RPICameraTextOverlayEnable bool `json:"rpiCameraTextOverlayEnable"` RPICameraTextOverlay string `json:"rpiCameraTextOverlay"` + RPICameraCodec string `json:"rpiCameraCodec"` + RPICameraIDRPeriod int `json:"rpiCameraIDRPeriod"` + RPICameraBitrate int `json:"rpiCameraBitrate"` + RPICameraProfile string `json:"rpiCameraProfile"` + RPICameraLevel string `json:"rpiCameraLevel"` // Hooks RunOnInit string `json:"runOnInit"` @@ -211,14 +212,15 @@ func (pconf *Path) setDefaults() { pconf.RPICameraDenoise = "off" pconf.RPICameraMetering = "centre" pconf.RPICameraFPS = 30 - pconf.RPICameraIDRPeriod = 60 - pconf.RPICameraBitrate = 1000000 - pconf.RPICameraProfile = "main" - pconf.RPICameraLevel = "4.1" pconf.RPICameraAfMode = "continuous" pconf.RPICameraAfRange = "normal" pconf.RPICameraAfSpeed = "normal" pconf.RPICameraTextOverlay = "%Y-%m-%d %H:%M:%S - MediaMTX" + pconf.RPICameraCodec = "auto" + pconf.RPICameraIDRPeriod = 60 + pconf.RPICameraBitrate = 1000000 + pconf.RPICameraProfile = "main" + pconf.RPICameraLevel = "4.1" // Hooks pconf.RunOnDemandStartTimeout = 10 * StringDuration(time.Second) @@ -550,6 +552,11 @@ func (pconf *Path) validate( default: return fmt.Errorf("invalid 'rpiCameraAfSpeed' value") } + switch pconf.RPICameraCodec { + case "auto", "hardwareH264", "softwareH264": + default: + return fmt.Errorf("invalid 'rpiCameraCodec' value") + } // Hooks diff --git a/internal/staticsources/rpicamera/mtxrpicamdownloader/VERSION b/internal/staticsources/rpicamera/mtxrpicamdownloader/VERSION index 1defe531bfa..a4b6ac3ded6 100644 --- a/internal/staticsources/rpicamera/mtxrpicamdownloader/VERSION +++ b/internal/staticsources/rpicamera/mtxrpicamdownloader/VERSION @@ -1 +1 @@ -v2.1.0 +v2.2.0 diff --git a/internal/staticsources/rpicamera/params.go b/internal/staticsources/rpicamera/params.go index b5460d3ec6d..ec8641104fd 100644 --- a/internal/staticsources/rpicamera/params.go +++ b/internal/staticsources/rpicamera/params.go @@ -25,10 +25,6 @@ type params struct { TuningFile string Mode string FPS float64 - IDRPeriod int - Bitrate int - Profile string - Level string AfMode string AfRange string AfSpeed string @@ -37,4 +33,9 @@ type params struct { FlickerPeriod int TextOverlayEnable bool TextOverlay string + Codec string + IDRPeriod int + Bitrate int + Profile string + Level string } diff --git a/internal/staticsources/rpicamera/source.go b/internal/staticsources/rpicamera/source.go index 6f2c72b6ae4..75e702888c8 100644 --- a/internal/staticsources/rpicamera/source.go +++ b/internal/staticsources/rpicamera/source.go @@ -50,10 +50,6 @@ func paramsFromConf(logLevel conf.LogLevel, cnf *conf.Path) params { TuningFile: cnf.RPICameraTuningFile, Mode: cnf.RPICameraMode, FPS: cnf.RPICameraFPS, - IDRPeriod: cnf.RPICameraIDRPeriod, - Bitrate: cnf.RPICameraBitrate, - Profile: cnf.RPICameraProfile, - Level: cnf.RPICameraLevel, AfMode: cnf.RPICameraAfMode, AfRange: cnf.RPICameraAfRange, AfSpeed: cnf.RPICameraAfSpeed, @@ -62,6 +58,11 @@ func paramsFromConf(logLevel conf.LogLevel, cnf *conf.Path) params { FlickerPeriod: cnf.RPICameraFlickerPeriod, TextOverlayEnable: cnf.RPICameraTextOverlayEnable, TextOverlay: cnf.RPICameraTextOverlay, + Codec: cnf.RPICameraCodec, + IDRPeriod: cnf.RPICameraIDRPeriod, + Bitrate: cnf.RPICameraBitrate, + Profile: cnf.RPICameraProfile, + Level: cnf.RPICameraLevel, } } diff --git a/mediamtx.yml b/mediamtx.yml index 9bc18f1293f..ba713a9d08c 100644 --- a/mediamtx.yml +++ b/mediamtx.yml @@ -508,62 +508,54 @@ pathDefaults: # ID of the camera rpiCameraCamID: 0 - # width of frames + # Width of frames rpiCameraWidth: 1920 - # height of frames + # Height of frames rpiCameraHeight: 1080 - # flip horizontally + # Flip horizontally rpiCameraHFlip: false - # flip vertically + # Flip vertically rpiCameraVFlip: false - # brightness [-1, 1] + # Brightness [-1, 1] rpiCameraBrightness: 0 - # contrast [0, 16] + # Contrast [0, 16] rpiCameraContrast: 1 - # saturation [0, 16] + # Saturation [0, 16] rpiCameraSaturation: 1 - # sharpness [0, 16] + # Sharpness [0, 16] rpiCameraSharpness: 1 - # exposure mode. + # Exposure mode. # values: normal, short, long, custom rpiCameraExposure: normal - # auto-white-balance mode. + # Auto-white-balance mode. # values: auto, incandescent, tungsten, fluorescent, indoor, daylight, cloudy, custom rpiCameraAWB: auto - # auto-white-balance fixed gains. This can be used in place of rpiCameraAWB. + # Auto-white-balance fixed gains. This can be used in place of rpiCameraAWB. # format: [red,blue] rpiCameraAWBGains: [0, 0] - # denoise operating mode. + # Denoise operating mode. # values: off, cdn_off, cdn_fast, cdn_hq rpiCameraDenoise: "off" - # fixed shutter speed, in microseconds. + # Fixed shutter speed, in microseconds. rpiCameraShutter: 0 - # metering mode of the AEC/AGC algorithm. + # Metering mode of the AEC/AGC algorithm. # values: centre, spot, matrix, custom rpiCameraMetering: centre - # fixed gain + # Fixed gain rpiCameraGain: 0 # EV compensation of the image [-10, 10] rpiCameraEV: 0 # Region of interest, in format x,y,width,height rpiCameraROI: - # whether to enable HDR on Raspberry Camera 3. + # Whether to enable HDR on Raspberry Camera 3. rpiCameraHDR: false - # tuning file + # Tuning file rpiCameraTuningFile: - # sensor mode, in format [width]:[height]:[bit-depth]:[packing] + # Sensor mode, in format [width]:[height]:[bit-depth]:[packing] # bit-depth and packing are optional. rpiCameraMode: # frames per second rpiCameraFPS: 30 - # period between IDR frames - rpiCameraIDRPeriod: 60 - # bitrate - rpiCameraBitrate: 1000000 - # H264 profile - rpiCameraProfile: main - # H264 level - rpiCameraLevel: '4.1' # Autofocus mode # values: auto, manual, continuous rpiCameraAfMode: continuous @@ -584,11 +576,21 @@ pathDefaults: rpiCameraAfWindow: # Manual flicker correction period, in microseconds. rpiCameraFlickerPeriod: 0 - # enables printing text on each frame. + # Enables printing text on each frame. rpiCameraTextOverlayEnable: false - # text that is printed on each frame. + # Text that is printed on each frame. # format is the one of the strftime() function. rpiCameraTextOverlay: '%Y-%m-%d %H:%M:%S - MediaMTX' + # Codec. Available values: auto, hardwareH264, softwareH264 + rpiCameraCodec: auto + # Period between IDR frames + rpiCameraIDRPeriod: 60 + # Bitrate + rpiCameraBitrate: 1000000 + # H264 profile + rpiCameraProfile: main + # H264 level + rpiCameraLevel: '4.1' ############################################### # Default path settings -> Hooks From 3700d5e5b9804e4b85ef9afcb309a42fe07388f6 Mon Sep 17 00:00:00 2001 From: Alessandro Ros Date: Wed, 21 Aug 2024 00:08:54 +0200 Subject: [PATCH 30/88] rpi: fix passing unsigned integers to component (#3672) --- internal/conf/env/env.go | 2 +- internal/conf/env/env_test.go | 8 ++++---- internal/conf/path.go | 14 +++++++------- internal/staticsources/rpicamera/params.go | 14 +++++++------- .../staticsources/rpicamera/params_serialize.go | 4 ++-- 5 files changed, 21 insertions(+), 21 deletions(-) diff --git a/internal/conf/env/env.go b/internal/conf/env/env.go index bb7352247c6..13151b18b9d 100644 --- a/internal/conf/env/env.go +++ b/internal/conf/env/env.go @@ -72,7 +72,7 @@ func loadEnvInternal(env map[string]string, prefix string, prv reflect.Value) er } return nil - case reflect.TypeOf(uint64(0)): + case reflect.TypeOf(uint(0)): if ev, ok := env[prefix]; ok { if prv.IsNil() { prv.Set(reflect.New(rt)) diff --git a/internal/conf/env/env_test.go b/internal/conf/env/env_test.go index 47ff7e3978f..76f5e808cce 100644 --- a/internal/conf/env/env_test.go +++ b/internal/conf/env/env_test.go @@ -16,7 +16,7 @@ func intPtr(v int) *int { return &v } -func uint64Ptr(v uint64) *uint64 { +func uintPtr(v uint) *uint { return &v } @@ -75,8 +75,8 @@ type testStruct struct { MyStringOpt *string `json:"myStringOpt"` MyInt int `json:"myInt"` MyIntOpt *int `json:"myIntOpt"` - MyUint uint64 `json:"myUint"` - MyUintOpt *uint64 `json:"myUintOpt"` + MyUint uint `json:"myUint"` + MyUintOpt *uint `json:"myUintOpt"` MyFloat float64 `json:"myFloat"` MyFloatOpt *float64 `json:"myFloatOpt"` MyBool bool `json:"myBool"` @@ -141,7 +141,7 @@ func TestLoad(t *testing.T) { MyInt: 123, MyIntOpt: intPtr(456), MyUint: 8910, - MyUintOpt: uint64Ptr(112313), + MyUintOpt: uintPtr(112313), MyFloat: 15.2, MyFloatOpt: float64Ptr(16.2), MyBool: true, diff --git a/internal/conf/path.go b/internal/conf/path.go index 2cff904e749..d6735dab021 100644 --- a/internal/conf/path.go +++ b/internal/conf/path.go @@ -130,9 +130,9 @@ type Path struct { SourceRedirect string `json:"sourceRedirect"` // Raspberry Pi Camera source - RPICameraCamID int `json:"rpiCameraCamID"` - RPICameraWidth int `json:"rpiCameraWidth"` - RPICameraHeight int `json:"rpiCameraHeight"` + RPICameraCamID uint `json:"rpiCameraCamID"` + RPICameraWidth uint `json:"rpiCameraWidth"` + RPICameraHeight uint `json:"rpiCameraHeight"` RPICameraHFlip bool `json:"rpiCameraHFlip"` RPICameraVFlip bool `json:"rpiCameraVFlip"` RPICameraBrightness float64 `json:"rpiCameraBrightness"` @@ -143,7 +143,7 @@ type Path struct { RPICameraAWB string `json:"rpiCameraAWB"` RPICameraAWBGains []float64 `json:"rpiCameraAWBGains"` RPICameraDenoise string `json:"rpiCameraDenoise"` - RPICameraShutter int `json:"rpiCameraShutter"` + RPICameraShutter uint `json:"rpiCameraShutter"` RPICameraMetering string `json:"rpiCameraMetering"` RPICameraGain float64 `json:"rpiCameraGain"` RPICameraEV float64 `json:"rpiCameraEV"` @@ -157,12 +157,12 @@ type Path struct { RPICameraAfSpeed string `json:"rpiCameraAfSpeed"` RPICameraLensPosition float64 `json:"rpiCameraLensPosition"` RPICameraAfWindow string `json:"rpiCameraAfWindow"` - RPICameraFlickerPeriod int `json:"rpiCameraFlickerPeriod"` + RPICameraFlickerPeriod uint `json:"rpiCameraFlickerPeriod"` RPICameraTextOverlayEnable bool `json:"rpiCameraTextOverlayEnable"` RPICameraTextOverlay string `json:"rpiCameraTextOverlay"` RPICameraCodec string `json:"rpiCameraCodec"` - RPICameraIDRPeriod int `json:"rpiCameraIDRPeriod"` - RPICameraBitrate int `json:"rpiCameraBitrate"` + RPICameraIDRPeriod uint `json:"rpiCameraIDRPeriod"` + RPICameraBitrate uint `json:"rpiCameraBitrate"` RPICameraProfile string `json:"rpiCameraProfile"` RPICameraLevel string `json:"rpiCameraLevel"` diff --git a/internal/staticsources/rpicamera/params.go b/internal/staticsources/rpicamera/params.go index ec8641104fd..3ca84194b89 100644 --- a/internal/staticsources/rpicamera/params.go +++ b/internal/staticsources/rpicamera/params.go @@ -2,9 +2,9 @@ package rpicamera type params struct { LogLevel string - CameraID int - Width int - Height int + CameraID uint + Width uint + Height uint HFlip bool VFlip bool Brightness float64 @@ -16,7 +16,7 @@ type params struct { AWBGainRed float64 AWBGainBlue float64 Denoise string - Shutter int + Shutter uint Metering string Gain float64 EV float64 @@ -30,12 +30,12 @@ type params struct { AfSpeed string LensPosition float64 AfWindow string - FlickerPeriod int + FlickerPeriod uint TextOverlayEnable bool TextOverlay string Codec string - IDRPeriod int - Bitrate int + IDRPeriod uint + Bitrate uint Profile string Level string } diff --git a/internal/staticsources/rpicamera/params_serialize.go b/internal/staticsources/rpicamera/params_serialize.go index 362f6f8f1ee..1234fdbb9ff 100644 --- a/internal/staticsources/rpicamera/params_serialize.go +++ b/internal/staticsources/rpicamera/params_serialize.go @@ -21,8 +21,8 @@ func (p params) serialize() []byte { f := rv.Field(i) switch f.Kind() { - case reflect.Int: - entry += strconv.FormatInt(f.Int(), 10) + case reflect.Uint: + entry += strconv.FormatUint(f.Uint(), 10) case reflect.Float64: entry += strconv.FormatFloat(f.Float(), 'f', -1, 64) From 7da91a77d9f5940c9cab8237e48524ad58ee036f Mon Sep 17 00:00:00 2001 From: Alessandro Ros Date: Wed, 21 Aug 2024 12:53:08 +0200 Subject: [PATCH 31/88] rpi: fix compatibility with latest mediamtx-rpicamera (#3674) --- internal/staticsources/rpicamera/camera.go | 2 +- internal/staticsources/rpicamera/component.go | 2 +- internal/staticsources/rpicamera/mtxrpicamdownloader/VERSION | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/staticsources/rpicamera/camera.go b/internal/staticsources/rpicamera/camera.go index b4f31479fcc..4177b3ef84d 100644 --- a/internal/staticsources/rpicamera/camera.go +++ b/internal/staticsources/rpicamera/camera.go @@ -51,7 +51,7 @@ func (c *camera) initialize() error { "LD_LIBRARY_PATH=" + dumpPath, } - c.cmd = exec.Command(filepath.Join(dumpPath, "exe")) + c.cmd = exec.Command(filepath.Join(dumpPath, "mtxrpicam")) c.cmd.Stdout = os.Stdout c.cmd.Stderr = os.Stderr c.cmd.Env = env diff --git a/internal/staticsources/rpicamera/component.go b/internal/staticsources/rpicamera/component.go index 35d2ec652c4..25ce4b37c29 100644 --- a/internal/staticsources/rpicamera/component.go +++ b/internal/staticsources/rpicamera/component.go @@ -84,7 +84,7 @@ func dumpComponent() error { return err } - err = os.Chmod(filepath.Join(dumpPath, "exe"), 0o755) + err = os.Chmod(filepath.Join(dumpPath, "mtxrpicam"), 0o755) if err != nil { os.RemoveAll(dumpPath) return err diff --git a/internal/staticsources/rpicamera/mtxrpicamdownloader/VERSION b/internal/staticsources/rpicamera/mtxrpicamdownloader/VERSION index a4b6ac3ded6..b1d18bc43f0 100644 --- a/internal/staticsources/rpicamera/mtxrpicamdownloader/VERSION +++ b/internal/staticsources/rpicamera/mtxrpicamdownloader/VERSION @@ -1 +1 @@ -v2.2.0 +v2.3.0 From aa1822dd62d15080245869ccb80d643f7bdf8f15 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 23 Aug 2024 12:48:46 +0200 Subject: [PATCH 32/88] bump hls.js to v1.5.15 (#3681) Co-authored-by: mediamtx-bot --- internal/servers/hls/hlsjsdownloader/HASH | 2 +- internal/servers/hls/hlsjsdownloader/VERSION | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/servers/hls/hlsjsdownloader/HASH b/internal/servers/hls/hlsjsdownloader/HASH index 2877a954761..a9bd8fcebf2 100644 --- a/internal/servers/hls/hlsjsdownloader/HASH +++ b/internal/servers/hls/hlsjsdownloader/HASH @@ -1 +1 @@ -7a9def412c7d62f281f4c7e94921fa754644dc6a304f2a096bf3fdddff256470 +7eff587d9b5f6ce78cafd3b2863fc7bfacb6dcfb0696b85714746666b2af2e54 diff --git a/internal/servers/hls/hlsjsdownloader/VERSION b/internal/servers/hls/hlsjsdownloader/VERSION index 0ecc386a036..f830b2f549f 100644 --- a/internal/servers/hls/hlsjsdownloader/VERSION +++ b/internal/servers/hls/hlsjsdownloader/VERSION @@ -1 +1 @@ -v1.5.14 +v1.5.15 From dc3b5f4e57f45cbb05e44394f61198c1cb90b403 Mon Sep 17 00:00:00 2001 From: Alessandro Ros Date: Sun, 25 Aug 2024 22:27:51 +0200 Subject: [PATCH 33/88] webrtc: fix 'duplicate payload type' error in read page (#3543) (#3679) --- internal/servers/webrtc/read_index.html | 109 ++++++++++++++---------- 1 file changed, 65 insertions(+), 44 deletions(-) diff --git a/internal/servers/webrtc/read_index.html b/internal/servers/webrtc/read_index.html index 427073f9a17..e2c340292d7 100644 --- a/internal/servers/webrtc/read_index.html +++ b/internal/servers/webrtc/read_index.html @@ -108,16 +108,28 @@ return ret; }; +const findFreePayloadType = (firstLine) => { + const payloadTypes = firstLine.split(' ').slice(3); + for (let i = 96; i <= 127; i++) { + if (!payloadTypes.includes(i.toString())) { + return i.toString(); + } + } + throw Error('unable to find a free payload type'); +}; + const enableStereoPcmau = (section) => { let lines = section.split('\r\n'); - lines[0] += ' 118'; - lines.splice(lines.length - 1, 0, 'a=rtpmap:118 PCMU/8000/2'); - lines.splice(lines.length - 1, 0, 'a=rtcp-fb:118 transport-cc'); + let payloadType = findFreePayloadType(lines[0]); + lines[0] += ` ${payloadType}`; + lines.splice(lines.length - 1, 0, `a=rtpmap:${payloadType} PCMU/8000/2`); + lines.splice(lines.length - 1, 0, `a=rtcp-fb:${payloadType} transport-cc`); - lines[0] += ' 119'; - lines.splice(lines.length - 1, 0, 'a=rtpmap:119 PCMA/8000/2'); - lines.splice(lines.length - 1, 0, 'a=rtcp-fb:119 transport-cc'); + payloadType = findFreePayloadType(lines[0]); + lines[0] += ` ${payloadType}`; + lines.splice(lines.length - 1, 0, `a=rtpmap:${payloadType} PCMA/8000/2`); + lines.splice(lines.length - 1, 0, `a=rtcp-fb:${payloadType} transport-cc`); return lines.join('\r\n'); }; @@ -125,35 +137,41 @@ const enableMultichannelOpus = (section) => { let lines = section.split('\r\n'); - lines[0] += " 112"; - lines.splice(lines.length - 1, 0, "a=rtpmap:112 multiopus/48000/3"); - lines.splice(lines.length - 1, 0, "a=fmtp:112 channel_mapping=0,2,1;num_streams=2;coupled_streams=1"); - lines.splice(lines.length - 1, 0, "a=rtcp-fb:112 transport-cc"); - - lines[0] += " 113"; - lines.splice(lines.length - 1, 0, "a=rtpmap:113 multiopus/48000/4"); - lines.splice(lines.length - 1, 0, "a=fmtp:113 channel_mapping=0,1,2,3;num_streams=2;coupled_streams=2"); - lines.splice(lines.length - 1, 0, "a=rtcp-fb:113 transport-cc"); - - lines[0] += " 114"; - lines.splice(lines.length - 1, 0, "a=rtpmap:114 multiopus/48000/5"); - lines.splice(lines.length - 1, 0, "a=fmtp:114 channel_mapping=0,4,1,2,3;num_streams=3;coupled_streams=2"); - lines.splice(lines.length - 1, 0, "a=rtcp-fb:114 transport-cc"); - - lines[0] += " 115"; - lines.splice(lines.length - 1, 0, "a=rtpmap:115 multiopus/48000/6"); - lines.splice(lines.length - 1, 0, "a=fmtp:115 channel_mapping=0,4,1,2,3,5;num_streams=4;coupled_streams=2"); - lines.splice(lines.length - 1, 0, "a=rtcp-fb:115 transport-cc"); - - lines[0] += " 116"; - lines.splice(lines.length - 1, 0, "a=rtpmap:116 multiopus/48000/7"); - lines.splice(lines.length - 1, 0, "a=fmtp:116 channel_mapping=0,4,1,2,3,5,6;num_streams=4;coupled_streams=4"); - lines.splice(lines.length - 1, 0, "a=rtcp-fb:116 transport-cc"); - - lines[0] += " 117"; - lines.splice(lines.length - 1, 0, "a=rtpmap:117 multiopus/48000/8"); - lines.splice(lines.length - 1, 0, "a=fmtp:117 channel_mapping=0,6,1,4,5,2,3,7;num_streams=5;coupled_streams=4"); - lines.splice(lines.length - 1, 0, "a=rtcp-fb:117 transport-cc"); + let payloadType = findFreePayloadType(lines[0]); + lines[0] += ` ${payloadType}`; + lines.splice(lines.length - 1, 0, `a=rtpmap:${payloadType} multiopus/48000/3`); + lines.splice(lines.length - 1, 0, `a=fmtp:${payloadType} channel_mapping=0,2,1;num_streams=2;coupled_streams=1`); + lines.splice(lines.length - 1, 0, `a=rtcp-fb:${payloadType} transport-cc`); + + payloadType = findFreePayloadType(lines[0]); + lines[0] += ` ${payloadType}`; + lines.splice(lines.length - 1, 0, `a=rtpmap:${payloadType} multiopus/48000/4`); + lines.splice(lines.length - 1, 0, `a=fmtp:${payloadType} channel_mapping=0,1,2,3;num_streams=2;coupled_streams=2`); + lines.splice(lines.length - 1, 0, `a=rtcp-fb:${payloadType} transport-cc`); + + payloadType = findFreePayloadType(lines[0]); + lines[0] += ` ${payloadType}`; + lines.splice(lines.length - 1, 0, `a=rtpmap:${payloadType} multiopus/48000/5`); + lines.splice(lines.length - 1, 0, `a=fmtp:${payloadType} channel_mapping=0,4,1,2,3;num_streams=3;coupled_streams=2`); + lines.splice(lines.length - 1, 0, `a=rtcp-fb:${payloadType} transport-cc`); + + payloadType = findFreePayloadType(lines[0]); + lines[0] += ` ${payloadType}`; + lines.splice(lines.length - 1, 0, `a=rtpmap:${payloadType} multiopus/48000/6`); + lines.splice(lines.length - 1, 0, `a=fmtp:${payloadType} channel_mapping=0,4,1,2,3,5;num_streams=4;coupled_streams=2`); + lines.splice(lines.length - 1, 0, `a=rtcp-fb:${payloadType} transport-cc`); + + payloadType = findFreePayloadType(lines[0]); + lines[0] += ` ${payloadType}`; + lines.splice(lines.length - 1, 0, `a=rtpmap:${payloadType} multiopus/48000/7`); + lines.splice(lines.length - 1, 0, `a=fmtp:${payloadType} channel_mapping=0,4,1,2,3,5,6;num_streams=4;coupled_streams=4`); + lines.splice(lines.length - 1, 0, `a=rtcp-fb:${payloadType} transport-cc`); + + payloadType = findFreePayloadType(lines[0]); + lines[0] += ` ${payloadType}`; + lines.splice(lines.length - 1, 0, `a=rtpmap:${payloadType} multiopus/48000/8`); + lines.splice(lines.length - 1, 0, `a=fmtp:${payloadType} channel_mapping=0,6,1,4,5,2,3,7;num_streams=5;coupled_streams=4`); + lines.splice(lines.length - 1, 0, `a=rtcp-fb:${payloadType} transport-cc`); return lines.join('\r\n'); }; @@ -161,17 +179,20 @@ const enableL16 = (section) => { let lines = section.split('\r\n'); - lines[0] += " 120"; - lines.splice(lines.length - 1, 0, "a=rtpmap:120 L16/8000/2"); - lines.splice(lines.length - 1, 0, "a=rtcp-fb:120 transport-cc"); + let payloadType = findFreePayloadType(lines[0]); + lines[0] += ` ${payloadType}`; + lines.splice(lines.length - 1, 0, `a=rtpmap:${payloadType} L16/8000/2`); + lines.splice(lines.length - 1, 0, `a=rtcp-fb:${payloadType} transport-cc`); - lines[0] += " 121"; - lines.splice(lines.length - 1, 0, "a=rtpmap:121 L16/16000/2"); - lines.splice(lines.length - 1, 0, "a=rtcp-fb:121 transport-cc"); + payloadType = findFreePayloadType(lines[0]); + lines[0] += ` ${payloadType}`; + lines.splice(lines.length - 1, 0, `a=rtpmap:${payloadType} L16/16000/2`); + lines.splice(lines.length - 1, 0, `a=rtcp-fb:${payloadType} transport-cc`); - lines[0] += " 122"; - lines.splice(lines.length - 1, 0, "a=rtpmap:122 L16/48000/2"); - lines.splice(lines.length - 1, 0, "a=rtcp-fb:122 transport-cc"); + payloadType = findFreePayloadType(lines[0]); + lines[0] += ` ${payloadType}`; + lines.splice(lines.length - 1, 0, `a=rtpmap:${payloadType} L16/48000/2`); + lines.splice(lines.length - 1, 0, `a=rtcp-fb:${payloadType} transport-cc`); return lines.join('\r\n'); }; From 6da35c8041ecba1ed9fe49a7c320ab48d36beaa4 Mon Sep 17 00:00:00 2001 From: Alessandro Ros Date: Sun, 25 Aug 2024 23:04:36 +0200 Subject: [PATCH 34/88] playback: add "url" field to recording timespans in /list (#3619) --- README.md | 12 +++++---- internal/playback/on_get.go | 24 +++++++++--------- internal/playback/on_list.go | 42 ++++++++++++++++++++++++------- internal/playback/on_list_test.go | 8 ++++++ 4 files changed, 60 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index bc3d6f6b9c5..793e1ec7e20 100644 --- a/README.md +++ b/README.md @@ -1186,7 +1186,7 @@ The JWT is expected to contain the `mediamtx_permissions` scope, with a list of } ``` -Clients are expected to pass the JWT in the Authorization header (in case of HLS and WebRTC) or in query parameters (in case of any other protocol), for instance (RTSP): +Clients are expected to pass the JWT in the Authorization header (in case of HLS and WebRTC) or in query parameters (in case of all other protocols), for instance: ``` ffmpeg -re -stream_loop -1 -i file.ts -c copy -f rtsp rtsp://localhost:8554/mystream?jwt=MY_JWT @@ -1350,16 +1350,18 @@ Where [mypath] is the name of a path. The server will return a list of timespans [ { "start": "2006-01-02T15:04:05Z07:00", - "duration": "60.0" + "duration": "60.0", + "url": "http://localhost:9996/get?path=[mypath]&start=2006-01-02T15%3A04%3A05Z07%3A00&duration=60.0" }, { "start": "2006-01-02T15:07:05Z07:00", - "duration": "32.33" + "duration": "32.33", + "url": "http://localhost:9996/get?path=[mypath]&start=2006-01-02T15%3A07%3A05Z07%3A00&duration=32.33" } ] ``` -The server provides an endpoint for downloading recordings: +The server provides an endpoint to download recordings: ``` http://localhost:9996/get?path=[mypath]&start=[start_date]&duration=[duration]&format=[format] @@ -1375,7 +1377,7 @@ Where: All parameters must be [url-encoded](https://www.urlencoder.org/). For instance: ``` -http://localhost:9996/get?path=stream2&start=2024-01-14T16%3A33%3A17%2B00%3A00&duration=200.5 +http://localhost:9996/get?path=mypath&start=2024-01-14T16%3A33%3A17%2B00%3A00&duration=200.5 ``` The resulting stream uses the fMP4 format, that is natively compatible with any browser, therefore its URL can be directly inserted into a \