support encoding secondary stream with H264 (2/2) (#4485) (#5898)

This commit is contained in:
Alessandro Ros
2026-06-28 11:19:23 +02:00
committed by GitHub
parent 1a7c10af1e
commit bf098c7eb5
7 changed files with 139 additions and 38 deletions
+35 -2
View File
@@ -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
+16 -12
View File
@@ -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:
@@ -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)
}
@@ -1 +1 @@
22aa13d2506cbfab4369114aca23643a2b5f0b2df681ef4407d4cd358973a2c7
1ef561be0f00c745871b936bf86f6c6ca973a752ee335a2ec33f3ae05187adb3
@@ -1 +1 @@
6c58145ec21aae6a362e233e17e2fd93aa778848b5462848861128310270b3aa
890f0eee1889f279e904ea669b67d2c54d9872f0cb68a17f1c4fa574a5b61a82
@@ -1 +1 @@
v2.7.0
v2.8.0
+58 -21
View File
@@ -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
})