From bf098c7eb5cd5eb467f6d7eb6487d233241ed017 Mon Sep 17 00:00:00 2001 From: Alessandro Ros Date: Sun, 28 Jun 2026 11:19:23 +0200 Subject: [PATCH] support encoding secondary stream with H264 (2/2) (#4485) (#5898) --- internal/api/api_config_paths_test.go | 37 ++++++++- internal/conf/path.go | 28 ++++--- .../rpicamera/camera_params_arm_.go | 27 +++++++ .../HASH_MTXRPICAM_32_TAR_GZ | 2 +- .../HASH_MTXRPICAM_64_TAR_GZ | 2 +- .../rpicamera/mtxrpicamdownloader/VERSION | 2 +- .../staticsources/rpicamera/source_arm_.go | 79 ++++++++++++++----- 7 files changed, 139 insertions(+), 38 deletions(-) diff --git a/internal/api/api_config_paths_test.go b/internal/api/api_config_paths_test.go index 881f11d9..c2046c6e 100644 --- a/internal/api/api_config_paths_test.go +++ b/internal/api/api_config_paths_test.go @@ -112,6 +112,11 @@ func TestConfigPathsAdd(t *testing.T) { "sourceOnDemand": true, "disablePublisherOverride": true, // test setting a deprecated parameter "rpiCameraVFlip": true, + "rpiCameraCodec": "hardwareH264", + "rpiCameraIDRPeriod": 120, + "rpiCameraBitrate": 3000000, + "rpiCameraH264Profile": "main", + "rpiCameraH264Level": "4.2", }, nil) var out map[string]any @@ -120,6 +125,11 @@ func TestConfigPathsAdd(t *testing.T) { require.Equal(t, true, out["sourceOnDemand"]) require.Equal(t, true, out["disablePublisherOverride"]) require.Equal(t, true, out["rpiCameraVFlip"]) + require.Equal(t, "hardwareH264", out["rpiCameraCodec"]) + require.Equal(t, float64(120), out["rpiCameraIDRPeriod"]) + require.Equal(t, float64(3000000), out["rpiCameraBitrate"]) + require.Equal(t, "main", out["rpiCameraH264Profile"]) + require.Equal(t, "4.2", out["rpiCameraH264Level"]) } func TestConfigPathsAddUnknownField(t *testing.T) { //nolint:dupl @@ -185,12 +195,20 @@ func TestConfigPathsPatch(t *testing.T) { //nolint:dupl "sourceOnDemand": true, "disablePublisherOverride": true, // test setting a deprecated parameter "rpiCameraVFlip": true, + "rpiCameraCodec": "hardwareH264", + "rpiCameraIDRPeriod": 120, + "rpiCameraBitrate": 3000000, + "rpiCameraH264Profile": "main", + "rpiCameraH264Level": "4.2", }, nil) httpRequest(t, hc, http.MethodPatch, "http://localhost:9997/v3/config/paths/patch/my/path", map[string]any{ - "source": "rtsp://127.0.0.1:9998/mypath", - "sourceOnDemand": true, + "source": "rtsp://127.0.0.1:9998/mypath", + "sourceOnDemand": true, + "rpiCameraCodec": "softwareH264", + "rpiCameraBitrate": 4000000, + "rpiCameraH264Level": "4.0", }, nil) var out map[string]any @@ -199,6 +217,11 @@ func TestConfigPathsPatch(t *testing.T) { //nolint:dupl require.Equal(t, true, out["sourceOnDemand"]) require.Equal(t, true, out["disablePublisherOverride"]) require.Equal(t, true, out["rpiCameraVFlip"]) + require.Equal(t, "softwareH264", out["rpiCameraCodec"]) + require.Equal(t, float64(120), out["rpiCameraIDRPeriod"]) + require.Equal(t, float64(4000000), out["rpiCameraBitrate"]) + require.Equal(t, "main", out["rpiCameraH264Profile"]) + require.Equal(t, "4.0", out["rpiCameraH264Level"]) } func TestConfigPathsReplace(t *testing.T) { //nolint:dupl @@ -226,6 +249,11 @@ func TestConfigPathsReplace(t *testing.T) { //nolint:dupl "sourceOnDemand": true, "disablePublisherOverride": true, // test setting a deprecated parameter "rpiCameraVFlip": true, + "rpiCameraCodec": "hardwareH264", + "rpiCameraIDRPeriod": 120, + "rpiCameraBitrate": 3000000, + "rpiCameraH264Profile": "main", + "rpiCameraH264Level": "4.2", }, nil) httpRequest(t, hc, http.MethodPost, "http://localhost:9997/v3/config/paths/replace/my/path", @@ -240,6 +268,11 @@ func TestConfigPathsReplace(t *testing.T) { //nolint:dupl require.Equal(t, true, out["sourceOnDemand"]) require.Equal(t, nil, out["disablePublisherOverride"]) require.Equal(t, false, out["rpiCameraVFlip"]) + require.Equal(t, "auto", out["rpiCameraCodec"]) + require.Equal(t, float64(60), out["rpiCameraIDRPeriod"]) + require.Equal(t, float64(5000000), out["rpiCameraBitrate"]) + require.Equal(t, "auto", out["rpiCameraH264Profile"]) + require.Equal(t, "4.1", out["rpiCameraH264Level"]) } func TestConfigPathsReplaceNonExisting(t *testing.T) { //nolint:dupl diff --git a/internal/conf/path.go b/internal/conf/path.go index 7128f6fd..967083e2 100644 --- a/internal/conf/path.go +++ b/internal/conf/path.go @@ -323,9 +323,14 @@ type Path struct { RPICameraJPEGQuality *uint `json:"rpiCameraJPEGQuality,omitempty" deprecated:"true"` RPICameraMJPEGQuality uint `json:"rpiCameraMJPEGQuality"` RPICameraPrimaryName string `json:"-"` // filled by Validate() + RPICameraSecondaryCodec string `json:"-"` // filled by Validate() RPICameraSecondaryWidth uint `json:"-"` // filled by Validate() RPICameraSecondaryHeight uint `json:"-"` // filled by Validate() RPICameraSecondaryFPS float64 `json:"-"` // filled by Validate() + RPICameraSecondaryIDRPeriod uint `json:"-"` // filled by Validate() + RPICameraSecondaryBitrate uint `json:"-"` // filled by Validate() + RPICameraSecondaryH264Profile string `json:"-"` // filled by Validate() + RPICameraSecondaryH264Level string `json:"-"` // filled by Validate() RPICameraSecondaryMJPEGQuality uint `json:"-"` // filled by Validate() // Hooks @@ -699,13 +704,13 @@ func (pconf *Path) validate( pconf.RPICameraMJPEGQuality = *pconf.RPICameraJPEGQuality } - if !pconf.RPICameraSecondary { - switch pconf.RPICameraCodec { - case "auto", "hardwareH264", "softwareH264", "mjpeg": - default: - return fmt.Errorf("supported codecs for a primary RPI Camera stream are auto, hardwareH264, softwareH264, mjpeg") - } + switch pconf.RPICameraCodec { + case "auto", "hardwareH264", "softwareH264", "mjpeg": + default: + return fmt.Errorf("supported codecs for a RPI Camera stream are auto, hardwareH264, softwareH264, mjpeg") + } + if !pconf.RPICameraSecondary { for otherName, otherPath := range conf.Paths { if otherPath != pconf && otherPath != nil && @@ -717,12 +722,6 @@ func (pconf *Path) validate( } } } else { - switch pconf.RPICameraCodec { - case "auto", "mjpeg": - default: - return fmt.Errorf("supported codecs for a secondary RPI Camera stream are auto, mjpeg") - } - var primaryName string var primary *Path @@ -751,6 +750,11 @@ func (pconf *Path) validate( primary.RPICameraSecondaryHeight = pconf.RPICameraHeight primary.RPICameraSecondaryFPS = pconf.RPICameraFPS primary.RPICameraSecondaryMJPEGQuality = pconf.RPICameraMJPEGQuality + primary.RPICameraSecondaryCodec = pconf.RPICameraCodec + primary.RPICameraSecondaryIDRPeriod = pconf.RPICameraIDRPeriod + primary.RPICameraSecondaryBitrate = pconf.RPICameraBitrate + primary.RPICameraSecondaryH264Profile = pconf.RPICameraH264Profile + primary.RPICameraSecondaryH264Level = pconf.RPICameraH264Level } default: diff --git a/internal/staticsources/rpicamera/camera_params_arm_.go b/internal/staticsources/rpicamera/camera_params_arm_.go index 83bc34d1..0f7e2e56 100644 --- a/internal/staticsources/rpicamera/camera_params_arm_.go +++ b/internal/staticsources/rpicamera/camera_params_arm_.go @@ -52,9 +52,14 @@ type cameraParams struct { H264Level string MJPEGQuality uint32 + SecondaryCodec string SecondaryWidth uint32 SecondaryHeight uint32 SecondaryFPS float32 + SecondaryIDRPeriod uint32 + SecondaryBitrate uint32 + SecondaryH264Profile string + SecondaryH264Level string SecondaryMJPEGQuality uint32 } @@ -149,9 +154,31 @@ func (p *cameraParams) fromConf(logLevel conf.LogLevel, cnf *conf.Path) { p.MJPEGQuality = uint32(cnf.RPICameraMJPEGQuality) + p.SecondaryCodec = func() string { + if cnf.RPICameraSecondaryCodec == "auto" { + return "mjpeg" + } + return cnf.RPICameraSecondaryCodec + }() + p.SecondaryWidth = uint32(cnf.RPICameraSecondaryWidth) p.SecondaryHeight = uint32(cnf.RPICameraSecondaryHeight) p.SecondaryFPS = float32(cnf.RPICameraSecondaryFPS) + p.SecondaryIDRPeriod = uint32(cnf.RPICameraSecondaryIDRPeriod) + p.SecondaryBitrate = uint32(cnf.RPICameraSecondaryBitrate) + + p.SecondaryH264Profile = func() string { + if cnf.RPICameraSecondaryH264Profile == "auto" { + if p.SecondaryCodec == "hardwareH264" { + return "main" + } + return "baseline" + } + + return cnf.RPICameraSecondaryH264Profile + }() + + p.SecondaryH264Level = cnf.RPICameraSecondaryH264Level p.SecondaryMJPEGQuality = uint32(cnf.RPICameraSecondaryMJPEGQuality) } diff --git a/internal/staticsources/rpicamera/mtxrpicamdownloader/HASH_MTXRPICAM_32_TAR_GZ b/internal/staticsources/rpicamera/mtxrpicamdownloader/HASH_MTXRPICAM_32_TAR_GZ index 7bd669a7..9f59ce52 100644 --- a/internal/staticsources/rpicamera/mtxrpicamdownloader/HASH_MTXRPICAM_32_TAR_GZ +++ b/internal/staticsources/rpicamera/mtxrpicamdownloader/HASH_MTXRPICAM_32_TAR_GZ @@ -1 +1 @@ -22aa13d2506cbfab4369114aca23643a2b5f0b2df681ef4407d4cd358973a2c7 +1ef561be0f00c745871b936bf86f6c6ca973a752ee335a2ec33f3ae05187adb3 diff --git a/internal/staticsources/rpicamera/mtxrpicamdownloader/HASH_MTXRPICAM_64_TAR_GZ b/internal/staticsources/rpicamera/mtxrpicamdownloader/HASH_MTXRPICAM_64_TAR_GZ index e914dbfb..b8b76973 100644 --- a/internal/staticsources/rpicamera/mtxrpicamdownloader/HASH_MTXRPICAM_64_TAR_GZ +++ b/internal/staticsources/rpicamera/mtxrpicamdownloader/HASH_MTXRPICAM_64_TAR_GZ @@ -1 +1 @@ -6c58145ec21aae6a362e233e17e2fd93aa778848b5462848861128310270b3aa +890f0eee1889f279e904ea669b67d2c54d9872f0cb68a17f1c4fa574a5b61a82 diff --git a/internal/staticsources/rpicamera/mtxrpicamdownloader/VERSION b/internal/staticsources/rpicamera/mtxrpicamdownloader/VERSION index 873ca0fa..b77e3cc5 100644 --- a/internal/staticsources/rpicamera/mtxrpicamdownloader/VERSION +++ b/internal/staticsources/rpicamera/mtxrpicamdownloader/VERSION @@ -1 +1 @@ -v2.7.0 +v2.8.0 diff --git a/internal/staticsources/rpicamera/source_arm_.go b/internal/staticsources/rpicamera/source_arm_.go index d980273a..25b5c82e 100644 --- a/internal/staticsources/rpicamera/source_arm_.go +++ b/internal/staticsources/rpicamera/source_arm_.go @@ -51,9 +51,12 @@ func (s *Source) Run(params defs.StaticSourceRunParams) error { } func (s *Source) runPrimary(params defs.StaticSourceRunParams) error { + var p cameraParams + p.fromConf(s.LogLevel, params.Conf) + var forma format.Format - if params.Conf.RPICameraCodec == "auto" || params.Conf.RPICameraCodec == "hardwareH264" || params.Conf.RPICameraCodec == "softwareH264" { + if p.Codec == "hardwareH264" || p.Codec == "softwareH264" { forma = &format.H264{ PayloadTyp: 96, PacketizationMode: 1, @@ -86,7 +89,7 @@ func (s *Source) runPrimary(params defs.StaticSourceRunParams) error { var encode func(au []byte) ([]*rtp.Packet, error) - if params.Conf.RPICameraCodec == "auto" || params.Conf.RPICameraCodec == "hardwareH264" || params.Conf.RPICameraCodec == "softwareH264" { + if p.Codec == "hardwareH264" || p.Codec == "softwareH264" { encH264 := &rtph264.Encoder{ PayloadType: 96, PayloadMaxSize: s.RTPMaxPayloadSize, @@ -159,18 +162,46 @@ func (s *Source) runPrimary(params defs.StaticSourceRunParams) error { var onDataSecondary func(pts int64, ntp time.Time, au []byte) if params.Conf.RPICameraSecondaryWidth != 0 { - secondaryEncMJPEG := &rtpmjpeg.Encoder{ - PayloadMaxSize: s.RTPMaxPayloadSize, - } - err := secondaryEncMJPEG.Init() - if err != nil { - panic(err) + var encodeSecondary func(au []byte) ([]*rtp.Packet, error) + + if p.SecondaryCodec == "hardwareH264" || p.SecondaryCodec == "softwareH264" { + secondaryEncH264 := &rtph264.Encoder{ + PayloadType: 96, + PayloadMaxSize: s.RTPMaxPayloadSize, + PacketizationMode: 1, + } + err := secondaryEncH264.Init() + if err != nil { + panic(err) + } + + encodeSecondary = func(au []byte) ([]*rtp.Packet, error) { + var nalus h264.AnnexB + err = nalus.Unmarshal(au) + if err != nil { + return nil, err + } + + return secondaryEncH264.Encode(nalus) + } + } else { + secondaryEncMJPEG := &rtpmjpeg.Encoder{ + PayloadMaxSize: s.RTPMaxPayloadSize, + } + err := secondaryEncMJPEG.Init() + if err != nil { + panic(err) + } + + encodeSecondary = func(au []byte) ([]*rtp.Packet, error) { + return secondaryEncMJPEG.Encode(au) + } } onDataSecondary = func(pts int64, ntp time.Time, au []byte) { initializeSubStream() - pkts, err2 := secondaryEncMJPEG.Encode(au) + pkts, err2 := encodeSecondary(au) if err2 != nil { s.Log(logger.Error, err2.Error()) return @@ -178,7 +209,6 @@ func (s *Source) runPrimary(params defs.StaticSourceRunParams) error { for _, pkt := range pkts { pkt.Timestamp = uint32(pts) - pkt.PayloadType = 96 subStream.WriteUnit(mediaSecondary, mediaSecondary.Formats[0], &unit.Unit{ PTS: pts, NTP: ntp, @@ -194,9 +224,6 @@ func (s *Source) runPrimary(params defs.StaticSourceRunParams) error { } }() - var p cameraParams - p.fromConf(s.LogLevel, params.Conf) - cam := &camera{ params: p, onData: onData, @@ -230,6 +257,9 @@ func (s *Source) runPrimary(params defs.StaticSourceRunParams) error { } func (s *Source) runSecondary(params defs.StaticSourceRunParams) error { + var p cameraParams + p.fromConf(s.LogLevel, params.Conf) + r := &secondaryReader{} r.ctx, r.ctxCancel = context.WithCancel(context.Background()) defer r.ctxCancel() @@ -241,9 +271,19 @@ func (s *Source) runSecondary(params defs.StaticSourceRunParams) error { defer path.RemoveReader(defs.PathRemoveReaderReq{Author: r}) + var forma format.Format + if p.Codec == "hardwareH264" || p.Codec == "softwareH264" { + forma = &format.H264{ + PayloadTyp: 96, + PacketizationMode: 1, + } + } else { + forma = &format.MJPEG{} + } + media := &description.Media{ Type: description.MediaTypeVideo, - Formats: []format.Format{&format.MJPEG{}}, + Formats: []format.Format{forma}, } res := s.Parent.SetReady(defs.PathSourceStaticSetReadyReq{ @@ -260,18 +300,15 @@ func (s *Source) runSecondary(params defs.StaticSourceRunParams) error { primaryStream.OrigDesc.Medias[1], primaryStream.OrigDesc.Medias[1].Formats[0], func(u *unit.Unit) error { - pkt := u.RTPPackets[0] - - newPkt := &rtp.Packet{ - Header: pkt.Header, - Payload: pkt.Payload, + clone := *u.RTPPackets[0] + if p.Codec != "hardwareH264" && p.Codec != "softwareH264" { + clone.PayloadType = 26 } - newPkt.PayloadType = 26 res.SubStream.WriteUnit(media, media.Formats[0], &unit.Unit{ PTS: u.PTS, NTP: u.NTP, - RTPPackets: []*rtp.Packet{newPkt}, + RTPPackets: []*rtp.Packet{&clone}, }) return nil })