allow RTMP streaming with codecid=av01 or hvc1 (#2232)

* allow RTMP streaming with codecid=av01 or hvc1

Prior to this change, when trying to stream AV1 over enhanced RTMP using
XSplit Broadcaster, the server was refusing the content with
"unsupported video codec: av01" message.

* add tests

---------

Co-authored-by: aler9 <46489434+aler9@users.noreply.github.com>
This commit is contained in:
Xavier Hallade
2023-08-22 22:35:09 +02:00
committed by GitHub
parent c4409026e6
commit accfc49f9c
3 changed files with 173 additions and 65 deletions

View File

@@ -1,19 +1,21 @@
package message package message
import ( import (
"fmt"
"github.com/bluenviron/mediamtx/internal/rtmp/rawmessage" "github.com/bluenviron/mediamtx/internal/rtmp/rawmessage"
) )
// ExtendedSequenceStart is a sequence start extended message. // ExtendedSequenceStart is a sequence start extended message.
type ExtendedSequenceStart struct { type ExtendedSequenceStart struct {
FourCC [4]byte ChunkStreamID byte
Config []byte MessageStreamID uint32
FourCC [4]byte
Config []byte
} }
// Unmarshal implements Message. // Unmarshal implements Message.
func (m *ExtendedSequenceStart) Unmarshal(raw *rawmessage.Message) error { func (m *ExtendedSequenceStart) Unmarshal(raw *rawmessage.Message) error {
m.ChunkStreamID = raw.ChunkStreamID
m.MessageStreamID = raw.MessageStreamID
copy(m.FourCC[:], raw.Body[1:5]) copy(m.FourCC[:], raw.Body[1:5])
m.Config = raw.Body[5:] m.Config = raw.Body[5:]
@@ -22,5 +24,16 @@ func (m *ExtendedSequenceStart) Unmarshal(raw *rawmessage.Message) error {
// Marshal implements Message. // Marshal implements Message.
func (m ExtendedSequenceStart) Marshal() (*rawmessage.Message, error) { func (m ExtendedSequenceStart) Marshal() (*rawmessage.Message, error) {
return nil, fmt.Errorf("TODO") body := make([]byte, 5+len(m.Config))
body[0] = 0b10000000 | byte(ExtendedTypeSequenceStart)
copy(body[1:5], m.FourCC[:])
copy(body[5:], m.Config)
return &rawmessage.Message{
ChunkStreamID: m.ChunkStreamID,
Type: uint8(TypeVideo),
MessageStreamID: m.MessageStreamID,
Body: body,
}, nil
} }

View File

@@ -103,7 +103,7 @@ func tracksFromMetadata(conn *Conn, payload []interface{}) (formats.Format, form
} }
case string: case string:
if vt == "avc1" { if vt == "avc1" || vt == "hvc1" || vt == "av01" {
return true, nil return true, nil
} }
} }

View File

@@ -5,8 +5,10 @@ import (
"testing" "testing"
"time" "time"
"github.com/abema/go-mp4"
"github.com/bluenviron/gortsplib/v3/pkg/formats" "github.com/bluenviron/gortsplib/v3/pkg/formats"
"github.com/bluenviron/mediacommon/pkg/codecs/h264" "github.com/bluenviron/mediacommon/pkg/codecs/h264"
"github.com/bluenviron/mediacommon/pkg/codecs/h265"
"github.com/bluenviron/mediacommon/pkg/codecs/mpeg4audio" "github.com/bluenviron/mediacommon/pkg/codecs/mpeg4audio"
"github.com/notedit/rtmp/format/flv/flvio" "github.com/notedit/rtmp/format/flv/flvio"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@@ -17,16 +19,37 @@ import (
) )
func TestReadTracks(t *testing.T) { func TestReadTracks(t *testing.T) {
sps := []byte{ h264SPS := []byte{
0x67, 0x64, 0x00, 0x0c, 0xac, 0x3b, 0x50, 0xb0, 0x67, 0x64, 0x00, 0x0c, 0xac, 0x3b, 0x50, 0xb0,
0x4b, 0x42, 0x00, 0x00, 0x03, 0x00, 0x02, 0x00, 0x4b, 0x42, 0x00, 0x00, 0x03, 0x00, 0x02, 0x00,
0x00, 0x03, 0x00, 0x3d, 0x08, 0x00, 0x03, 0x00, 0x3d, 0x08,
} }
pps := []byte{ h264PPS := []byte{
0x68, 0xee, 0x3c, 0x80, 0x68, 0xee, 0x3c, 0x80,
} }
h265VPS := []byte{
0x40, 0x01, 0x0c, 0x01, 0xff, 0xff, 0x01, 0x40,
0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00,
0x03, 0x00, 0x00, 0x03, 0x00, 0x7b, 0xac, 0x09,
}
h265SPS := []byte{
0x42, 0x01, 0x01, 0x01, 0x40, 0x00, 0x00, 0x03,
0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00,
0x03, 0x00, 0x7b, 0xa0, 0x03, 0xc0, 0x80, 0x11,
0x07, 0xcb, 0x96, 0xb4, 0xa4, 0x25, 0x92, 0xe3,
0x01, 0x6a, 0x02, 0x02, 0x02, 0x08, 0x00, 0x00,
0x03, 0x00, 0x08, 0x00, 0x00, 0x03, 0x01, 0xe3,
0x00, 0x2e, 0xf2, 0x88, 0x00, 0x09, 0x89, 0x60,
0x00, 0x04, 0xc4, 0xb4, 0x20,
}
h265PPS := []byte{
0x44, 0x01, 0xc0, 0xf7, 0xc0, 0xcc, 0x90,
}
for _, ca := range []struct { for _, ca := range []struct {
name string name string
videoTrack formats.Format videoTrack formats.Format
@@ -36,8 +59,8 @@ func TestReadTracks(t *testing.T) {
"video+audio", "video+audio",
&formats.H264{ &formats.H264{
PayloadTyp: 96, PayloadTyp: 96,
SPS: sps, SPS: h264SPS,
PPS: pps, PPS: h264PPS,
PacketizationMode: 1, PacketizationMode: 1,
}, },
&formats.MPEG4Audio{ &formats.MPEG4Audio{
@@ -56,8 +79,8 @@ func TestReadTracks(t *testing.T) {
"video", "video",
&formats.H264{ &formats.H264{
PayloadTyp: 96, PayloadTyp: 96,
SPS: sps, SPS: h264SPS,
PPS: pps, PPS: h264PPS,
PacketizationMode: 1, PacketizationMode: 1,
}, },
nil, nil,
@@ -66,8 +89,8 @@ func TestReadTracks(t *testing.T) {
"metadata without codec id, video+audio", "metadata without codec id, video+audio",
&formats.H264{ &formats.H264{
PayloadTyp: 96, PayloadTyp: 96,
SPS: sps, SPS: h264SPS,
PPS: pps, PPS: h264PPS,
PacketizationMode: 1, PacketizationMode: 1,
}, },
&formats.MPEG4Audio{ &formats.MPEG4Audio{
@@ -86,8 +109,8 @@ func TestReadTracks(t *testing.T) {
"metadata without codec id, video only", "metadata without codec id, video only",
&formats.H264{ &formats.H264{
PayloadTyp: 96, PayloadTyp: 96,
SPS: sps, SPS: h264SPS,
PPS: pps, PPS: h264PPS,
PacketizationMode: 1, PacketizationMode: 1,
}, },
nil, nil,
@@ -96,8 +119,8 @@ func TestReadTracks(t *testing.T) {
"missing metadata, video+audio", "missing metadata, video+audio",
&formats.H264{ &formats.H264{
PayloadTyp: 96, PayloadTyp: 96,
SPS: sps, SPS: h264SPS,
PPS: pps, PPS: h264PPS,
PacketizationMode: 1, PacketizationMode: 1,
}, },
&formats.MPEG4Audio{ &formats.MPEG4Audio{
@@ -131,24 +154,9 @@ func TestReadTracks(t *testing.T) {
"obs studio pre 29.1 h265", "obs studio pre 29.1 h265",
&formats.H265{ &formats.H265{
PayloadTyp: 96, PayloadTyp: 96,
VPS: []byte{ VPS: h265VPS,
0x40, 0x01, 0x0c, 0x01, 0xff, 0xff, 0x01, 0x40, SPS: h265SPS,
0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, PPS: h265PPS,
0x03, 0x00, 0x00, 0x03, 0x00, 0x7b, 0xac, 0x09,
},
SPS: []byte{
0x42, 0x01, 0x01, 0x01, 0x40, 0x00, 0x00, 0x03,
0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00,
0x03, 0x00, 0x7b, 0xa0, 0x03, 0xc0, 0x80, 0x11,
0x07, 0xcb, 0x96, 0xb4, 0xa4, 0x25, 0x92, 0xe3,
0x01, 0x6a, 0x02, 0x02, 0x02, 0x08, 0x00, 0x00,
0x03, 0x00, 0x08, 0x00, 0x00, 0x03, 0x01, 0xe3,
0x00, 0x2e, 0xf2, 0x88, 0x00, 0x09, 0x89, 0x60,
0x00, 0x04, 0xc4, 0xb4, 0x20,
},
PPS: []byte{
0x44, 0x01, 0xc0, 0xf7, 0xc0, 0xcc, 0x90,
},
}, },
&formats.MPEG4Audio{ &formats.MPEG4Audio{
PayloadTyp: 96, PayloadTyp: 96,
@@ -162,6 +170,16 @@ func TestReadTracks(t *testing.T) {
IndexDeltaLength: 3, IndexDeltaLength: 3,
}, },
}, },
{
"xplit broadcaster",
&formats.H265{
PayloadTyp: 96,
VPS: h265VPS,
SPS: h265SPS,
PPS: h265PPS,
},
nil,
},
} { } {
t.Run(ca.name, func(t *testing.T) { t.Run(ca.name, func(t *testing.T) {
var buf bytes.Buffer var buf bytes.Buffer
@@ -199,8 +217,8 @@ func TestReadTracks(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
buf, _ := h264conf.Conf{ buf, _ := h264conf.Conf{
SPS: sps, SPS: h264SPS,
PPS: pps, PPS: h264PPS,
}.Marshal() }.Marshal()
err = mrw.Write(&message.Video{ err = mrw.Write(&message.Video{
@@ -262,8 +280,8 @@ func TestReadTracks(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
buf, _ := h264conf.Conf{ buf, _ := h264conf.Conf{
SPS: sps, SPS: h264SPS,
PPS: pps, PPS: h264PPS,
}.Marshal() }.Marshal()
err = mrw.Write(&message.Video{ err = mrw.Write(&message.Video{
@@ -302,8 +320,8 @@ func TestReadTracks(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
buf, _ := h264conf.Conf{ buf, _ := h264conf.Conf{
SPS: sps, SPS: h264SPS,
PPS: pps, PPS: h264PPS,
}.Marshal() }.Marshal()
err = mrw.Write(&message.Video{ err = mrw.Write(&message.Video{
@@ -361,8 +379,8 @@ func TestReadTracks(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
buf, _ := h264conf.Conf{ buf, _ := h264conf.Conf{
SPS: sps, SPS: h264SPS,
PPS: pps, PPS: h264PPS,
}.Marshal() }.Marshal()
err = mrw.Write(&message.Video{ err = mrw.Write(&message.Video{
@@ -388,8 +406,8 @@ func TestReadTracks(t *testing.T) {
case "missing metadata, video+audio": case "missing metadata, video+audio":
buf, _ := h264conf.Conf{ buf, _ := h264conf.Conf{
SPS: sps, SPS: h264SPS,
PPS: pps, PPS: h264PPS,
}.Marshal() }.Marshal()
err := mrw.Write(&message.Video{ err := mrw.Write(&message.Video{
@@ -484,25 +502,9 @@ func TestReadTracks(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
avcc, err := h264.AVCCMarshal([][]byte{ avcc, err := h264.AVCCMarshal([][]byte{
{ // VPS h265VPS,
0x40, 0x01, 0x0c, 0x01, 0xff, 0xff, 0x01, 0x40, h265SPS,
0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, h265PPS,
0x03, 0x00, 0x00, 0x03, 0x00, 0x7b, 0xac, 0x09,
},
{ // SPS
0x42, 0x01, 0x01, 0x01, 0x40, 0x00, 0x00, 0x03,
0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00,
0x03, 0x00, 0x7b, 0xa0, 0x03, 0xc0, 0x80, 0x11,
0x07, 0xcb, 0x96, 0xb4, 0xa4, 0x25, 0x92, 0xe3,
0x01, 0x6a, 0x02, 0x02, 0x02, 0x08, 0x00, 0x00,
0x03, 0x00, 0x08, 0x00, 0x00, 0x03, 0x01, 0xe3,
0x00, 0x2e, 0xf2, 0x88, 0x00, 0x09, 0x89, 0x60,
0x00, 0x04, 0xc4, 0xb4, 0x20,
},
{
// PPS
0x44, 0x01, 0xc0, 0xf7, 0xc0, 0xcc, 0x90,
},
}) })
require.NoError(t, err) require.NoError(t, err)
@@ -534,6 +536,99 @@ func TestReadTracks(t *testing.T) {
Payload: enc, Payload: enc,
}) })
require.NoError(t, err) require.NoError(t, err)
case "xplit broadcaster":
err := mrw.Write(&message.DataAMF0{
ChunkStreamID: 4,
MessageStreamID: 1,
Payload: []interface{}{
"@setDataFrame",
"onMetaData",
flvio.AMFMap{
{
K: "videodatarate",
V: float64(0),
},
{
K: "videocodecid",
V: "hvc1",
},
{
K: "audiodatarate",
V: float64(0),
},
{
K: "audiocodecid",
V: float64(0),
},
},
},
})
require.NoError(t, err)
var spsp h265.SPS
err = spsp.Unmarshal(h265SPS)
require.NoError(t, err)
hvcc := &mp4.HvcC{
ConfigurationVersion: 1,
GeneralProfileIdc: spsp.ProfileTierLevel.GeneralProfileIdc,
GeneralProfileCompatibility: spsp.ProfileTierLevel.GeneralProfileCompatibilityFlag,
GeneralConstraintIndicator: [6]uint8{
h265SPS[7], h265SPS[8], h265SPS[9],
h265SPS[10], h265SPS[11], h265SPS[12],
},
GeneralLevelIdc: spsp.ProfileTierLevel.GeneralLevelIdc,
// MinSpatialSegmentationIdc
// ParallelismType
ChromaFormatIdc: uint8(spsp.ChromaFormatIdc),
BitDepthLumaMinus8: uint8(spsp.BitDepthLumaMinus8),
BitDepthChromaMinus8: uint8(spsp.BitDepthChromaMinus8),
// AvgFrameRate
// ConstantFrameRate
NumTemporalLayers: 1,
// TemporalIdNested
LengthSizeMinusOne: 3,
NumOfNaluArrays: 3,
NaluArrays: []mp4.HEVCNaluArray{
{
NaluType: byte(h265.NALUType_VPS_NUT),
NumNalus: 1,
Nalus: []mp4.HEVCNalu{{
Length: uint16(len(h265VPS)),
NALUnit: h265VPS,
}},
},
{
NaluType: byte(h265.NALUType_SPS_NUT),
NumNalus: 1,
Nalus: []mp4.HEVCNalu{{
Length: uint16(len(h265SPS)),
NALUnit: h265SPS,
}},
},
{
NaluType: byte(h265.NALUType_PPS_NUT),
NumNalus: 1,
Nalus: []mp4.HEVCNalu{{
Length: uint16(len(h265PPS)),
NALUnit: h265PPS,
}},
},
},
}
var buf bytes.Buffer
_, err = mp4.Marshal(&buf, hvcc, mp4.Context{})
require.NoError(t, err)
err = mrw.Write(&message.ExtendedSequenceStart{
ChunkStreamID: 4,
MessageStreamID: 0x1000000,
FourCC: message.FourCCHEVC,
Config: buf.Bytes(),
})
require.NoError(t, err)
} }
c := newNoHandshakeConn(&buf) c := newNoHandshakeConn(&buf)