support publishing AV1/H265 with OBS 30 (#2217) (#2234)
Some checks reported warnings
lint / code (push) Has been cancelled
lint / mod-tidy (push) Has been cancelled
lint / apidocs (push) Has been cancelled
test / test64 (push) Has been cancelled
test / test32 (push) Has been cancelled
test / test_highlevel (push) Has been cancelled

This commit is contained in:
Alessandro Ros
2023-08-22 22:56:23 +02:00
committed by GitHub
parent accfc49f9c
commit 1133c734ab
14 changed files with 193 additions and 80 deletions

View File

@@ -77,15 +77,19 @@ func (m *Audio) Unmarshal(raw *rawmessage.Message) error {
return nil return nil
} }
// Marshal implements Message. func (m Audio) marshalBodySize() int {
func (m Audio) Marshal() (*rawmessage.Message, error) {
var l int var l int
if m.Codec == CodecMPEG1Audio { if m.Codec == CodecMPEG1Audio {
l = 1 + len(m.Payload) l = 1 + len(m.Payload)
} else { } else {
l = 2 + len(m.Payload) l = 2 + len(m.Payload)
} }
body := make([]byte, l) return l
}
// Marshal implements Message.
func (m Audio) Marshal() (*rawmessage.Message, error) {
body := make([]byte, m.marshalBodySize())
body[0] = m.Codec<<4 | m.Rate<<2 | m.Depth<<1 | m.Channels body[0] = m.Codec<<4 | m.Rate<<2 | m.Depth<<1 | m.Channels

View File

@@ -12,7 +12,7 @@ type ExtendedCodedFrames struct {
ChunkStreamID byte ChunkStreamID byte
DTS time.Duration DTS time.Duration
MessageStreamID uint32 MessageStreamID uint32
FourCC [4]byte FourCC FourCC
PTSDelta time.Duration PTSDelta time.Duration
Payload []byte Payload []byte
} }
@@ -26,7 +26,7 @@ func (m *ExtendedCodedFrames) Unmarshal(raw *rawmessage.Message) error {
m.ChunkStreamID = raw.ChunkStreamID m.ChunkStreamID = raw.ChunkStreamID
m.DTS = raw.Timestamp m.DTS = raw.Timestamp
m.MessageStreamID = raw.MessageStreamID m.MessageStreamID = raw.MessageStreamID
copy(m.FourCC[:], raw.Body[1:5]) m.FourCC = FourCC(raw.Body[1])<<24 | FourCC(raw.Body[2])<<16 | FourCC(raw.Body[3])<<8 | FourCC(raw.Body[4])
if m.FourCC == FourCCHEVC { if m.FourCC == FourCCHEVC {
m.PTSDelta = time.Duration(uint32(raw.Body[5])<<16|uint32(raw.Body[6])<<8|uint32(raw.Body[7])) * time.Millisecond m.PTSDelta = time.Duration(uint32(raw.Body[5])<<16|uint32(raw.Body[6])<<8|uint32(raw.Body[7])) * time.Millisecond
@@ -38,18 +38,25 @@ func (m *ExtendedCodedFrames) Unmarshal(raw *rawmessage.Message) error {
return nil return nil
} }
// Marshal implements Message. func (m ExtendedCodedFrames) marshalBodySize() int {
func (m ExtendedCodedFrames) Marshal() (*rawmessage.Message, error) {
var l int var l int
if m.FourCC == FourCCHEVC { if m.FourCC == FourCCHEVC {
l = 8 + len(m.Payload) l = 8 + len(m.Payload)
} else { } else {
l = 5 + len(m.Payload) l = 5 + len(m.Payload)
} }
body := make([]byte, l) return l
}
// Marshal implements Message.
func (m ExtendedCodedFrames) Marshal() (*rawmessage.Message, error) {
body := make([]byte, m.marshalBodySize())
body[0] = 0b10000000 | byte(ExtendedTypeCodedFrames) body[0] = 0b10000000 | byte(ExtendedTypeCodedFrames)
copy(body[1:5], m.FourCC[:]) body[1] = uint8(m.FourCC >> 24)
body[2] = uint8(m.FourCC >> 16)
body[3] = uint8(m.FourCC >> 8)
body[4] = uint8(m.FourCC)
if m.FourCC == FourCCHEVC { if m.FourCC == FourCCHEVC {
tmp := uint32(m.PTSDelta / time.Millisecond) tmp := uint32(m.PTSDelta / time.Millisecond)

View File

@@ -1,6 +1,7 @@
package message package message
import ( import (
"fmt"
"time" "time"
"github.com/bluenviron/mediamtx/internal/rtmp/rawmessage" "github.com/bluenviron/mediamtx/internal/rtmp/rawmessage"
@@ -11,27 +12,38 @@ type ExtendedFramesX struct {
ChunkStreamID byte ChunkStreamID byte
DTS time.Duration DTS time.Duration
MessageStreamID uint32 MessageStreamID uint32
FourCC [4]byte FourCC FourCC
Payload []byte Payload []byte
} }
// Unmarshal implements Message. // Unmarshal implements Message.
func (m *ExtendedFramesX) Unmarshal(raw *rawmessage.Message) error { func (m *ExtendedFramesX) Unmarshal(raw *rawmessage.Message) error {
if len(raw.Body) < 6 {
return fmt.Errorf("not enough bytes")
}
m.ChunkStreamID = raw.ChunkStreamID m.ChunkStreamID = raw.ChunkStreamID
m.DTS = raw.Timestamp m.DTS = raw.Timestamp
m.MessageStreamID = raw.MessageStreamID m.MessageStreamID = raw.MessageStreamID
copy(m.FourCC[:], raw.Body[1:5]) m.FourCC = FourCC(raw.Body[1])<<24 | FourCC(raw.Body[2])<<16 | FourCC(raw.Body[3])<<8 | FourCC(raw.Body[4])
m.Payload = raw.Body[5:] m.Payload = raw.Body[5:]
return nil return nil
} }
func (m ExtendedFramesX) marshalBodySize() int {
return 5 + len(m.Payload)
}
// Marshal implements Message. // Marshal implements Message.
func (m ExtendedFramesX) Marshal() (*rawmessage.Message, error) { func (m ExtendedFramesX) Marshal() (*rawmessage.Message, error) {
body := make([]byte, 5+len(m.Payload)) body := make([]byte, m.marshalBodySize())
body[0] = 0b10000000 | byte(ExtendedTypeFramesX) body[0] = 0b10000000 | byte(ExtendedTypeFramesX)
copy(body[1:5], m.FourCC[:]) body[1] = uint8(m.FourCC >> 24)
body[2] = uint8(m.FourCC >> 16)
body[3] = uint8(m.FourCC >> 8)
body[4] = uint8(m.FourCC)
copy(body[5:], m.Payload) copy(body[5:], m.Payload)
return &rawmessage.Message{ return &rawmessage.Message{

View File

@@ -8,12 +8,16 @@ import (
// ExtendedMetadata is a metadata extended message. // ExtendedMetadata is a metadata extended message.
type ExtendedMetadata struct { type ExtendedMetadata struct {
FourCC [4]byte FourCC FourCC
} }
// Unmarshal implements Message. // Unmarshal implements Message.
func (m *ExtendedMetadata) Unmarshal(raw *rawmessage.Message) error { func (m *ExtendedMetadata) Unmarshal(raw *rawmessage.Message) error {
copy(m.FourCC[:], raw.Body[1:5]) if len(raw.Body) != 5 {
return fmt.Errorf("invalid body size")
}
m.FourCC = FourCC(raw.Body[1])<<24 | FourCC(raw.Body[2])<<16 | FourCC(raw.Body[3])<<8 | FourCC(raw.Body[4])
return fmt.Errorf("ExtendedMetadata is not implemented yet") return fmt.Errorf("ExtendedMetadata is not implemented yet")
} }

View File

@@ -8,12 +8,16 @@ import (
// ExtendedMPEG2TSSequenceStart is a MPEG2-TS sequence start extended message. // ExtendedMPEG2TSSequenceStart is a MPEG2-TS sequence start extended message.
type ExtendedMPEG2TSSequenceStart struct { type ExtendedMPEG2TSSequenceStart struct {
FourCC [4]byte FourCC FourCC
} }
// Unmarshal implements Message. // Unmarshal implements Message.
func (m *ExtendedMPEG2TSSequenceStart) Unmarshal(raw *rawmessage.Message) error { func (m *ExtendedMPEG2TSSequenceStart) Unmarshal(raw *rawmessage.Message) error {
copy(m.FourCC[:], raw.Body[1:5]) if len(raw.Body) != 5 {
return fmt.Errorf("invalid body size")
}
m.FourCC = FourCC(raw.Body[1])<<24 | FourCC(raw.Body[2])<<16 | FourCC(raw.Body[3])<<8 | FourCC(raw.Body[4])
return fmt.Errorf("ExtendedMPEG2TSSequenceStart is not implemented yet") return fmt.Errorf("ExtendedMPEG2TSSequenceStart is not implemented yet")
} }

View File

@@ -8,7 +8,7 @@ import (
// ExtendedSequenceEnd is a sequence end extended message. // ExtendedSequenceEnd is a sequence end extended message.
type ExtendedSequenceEnd struct { type ExtendedSequenceEnd struct {
FourCC [4]byte FourCC FourCC
} }
// Unmarshal implements Message. // Unmarshal implements Message.
@@ -17,7 +17,7 @@ func (m *ExtendedSequenceEnd) Unmarshal(raw *rawmessage.Message) error {
return fmt.Errorf("invalid body size") return fmt.Errorf("invalid body size")
} }
copy(m.FourCC[:], raw.Body[1:5]) m.FourCC = FourCC(raw.Body[1])<<24 | FourCC(raw.Body[2])<<16 | FourCC(raw.Body[3])<<8 | FourCC(raw.Body[4])
return nil return nil
} }

View File

@@ -1,6 +1,8 @@
package message package message
import ( import (
"fmt"
"github.com/bluenviron/mediamtx/internal/rtmp/rawmessage" "github.com/bluenviron/mediamtx/internal/rtmp/rawmessage"
) )
@@ -8,26 +10,37 @@ import (
type ExtendedSequenceStart struct { type ExtendedSequenceStart struct {
ChunkStreamID byte ChunkStreamID byte
MessageStreamID uint32 MessageStreamID uint32
FourCC [4]byte FourCC FourCC
Config []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 {
if len(raw.Body) < 6 {
return fmt.Errorf("not enough bytes")
}
m.ChunkStreamID = raw.ChunkStreamID m.ChunkStreamID = raw.ChunkStreamID
m.MessageStreamID = raw.MessageStreamID m.MessageStreamID = raw.MessageStreamID
copy(m.FourCC[:], raw.Body[1:5]) m.FourCC = FourCC(raw.Body[1])<<24 | FourCC(raw.Body[2])<<16 | FourCC(raw.Body[3])<<8 | FourCC(raw.Body[4])
m.Config = raw.Body[5:] m.Config = raw.Body[5:]
return nil return nil
} }
func (m ExtendedSequenceStart) marshalBodySize() int {
return 5 + len(m.Config)
}
// Marshal implements Message. // Marshal implements Message.
func (m ExtendedSequenceStart) Marshal() (*rawmessage.Message, error) { func (m ExtendedSequenceStart) Marshal() (*rawmessage.Message, error) {
body := make([]byte, 5+len(m.Config)) body := make([]byte, m.marshalBodySize())
body[0] = 0b10000000 | byte(ExtendedTypeSequenceStart) body[0] = 0b10000000 | byte(ExtendedTypeSequenceStart)
copy(body[1:5], m.FourCC[:]) body[1] = uint8(m.FourCC >> 24)
body[2] = uint8(m.FourCC >> 16)
body[3] = uint8(m.FourCC >> 8)
body[4] = uint8(m.FourCC)
copy(body[5:], m.Config) copy(body[5:], m.Config)
return &rawmessage.Message{ return &rawmessage.Message{

View File

@@ -61,13 +61,13 @@ const (
) )
// FourCC is an identifier of a video codec. // FourCC is an identifier of a video codec.
type FourCC [4]byte type FourCC uint32
// video codec identifiers. // video codec identifiers.
var ( var (
FourCCAV1 FourCC = [4]byte{'a', 'v', '0', '1'} FourCCAV1 FourCC = 'a'<<24 | 'v'<<16 | '0'<<8 | '1'
FourCCVP9 FourCC = [4]byte{'v', 'p', '0', '9'} FourCCVP9 FourCC = 'v'<<24 | 'p'<<16 | '0'<<8 | '9'
FourCCHEVC FourCC = [4]byte{'h', 'v', 'c', '1'} FourCCHEVC FourCC = 'h'<<24 | 'v'<<16 | 'c'<<8 | '1'
) )
// Message is a message. // Message is a message.

View File

@@ -70,8 +70,7 @@ func allocateMessage(raw *rawmessage.Message) (Message, error) {
} }
if (raw.Body[0] & 0b10000000) != 0 { if (raw.Body[0] & 0b10000000) != 0 {
var fourCC [4]byte fourCC := FourCC(raw.Body[1])<<24 | FourCC(raw.Body[2])<<16 | FourCC(raw.Body[3])<<8 | FourCC(raw.Body[4])
copy(fourCC[:], raw.Body[1:5])
switch fourCC { switch fourCC {
case FourCCAV1, FourCCVP9, FourCCHEVC: case FourCCAV1, FourCCVP9, FourCCHEVC:

View File

@@ -232,6 +232,20 @@ var readWriterCases = []struct {
0x0a, 0x01, 0x02, 0x03, 0x0a, 0x01, 0x02, 0x03,
}, },
}, },
{
"extended sequence start",
&ExtendedSequenceStart{
ChunkStreamID: 4,
MessageStreamID: 0x1000000,
FourCC: FourCCHEVC,
Config: []byte{0x01, 0x02, 0x03},
},
[]byte{
0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x09,
0x01, 0x00, 0x00, 0x00, 0x80, 0x68, 0x76, 0x63,
0x31, 0x01, 0x02, 0x03,
},
},
{ {
"extended coded frames", "extended coded frames",
&ExtendedCodedFrames{ &ExtendedCodedFrames{

View File

@@ -74,9 +74,13 @@ func (m *Video) Unmarshal(raw *rawmessage.Message) error {
return nil return nil
} }
func (m Video) marshalBodySize() int {
return 5 + len(m.Payload)
}
// Marshal implements Message. // Marshal implements Message.
func (m Video) Marshal() (*rawmessage.Message, error) { func (m Video) Marshal() (*rawmessage.Message, error) {
body := make([]byte, 5+len(m.Payload)) body := make([]byte, m.marshalBodySize())
if m.IsKeyFrame { if m.IsKeyFrame {
body[0] = flvio.FRAME_KEY << 4 body[0] = flvio.FRAME_KEY << 4

View File

@@ -98,7 +98,8 @@ func tracksFromMetadata(conn *Conn, payload []interface{}) (formats.Format, form
case 0: case 0:
return false, nil return false, nil
case message.CodecH264: case message.CodecH264, float64(message.FourCCAV1),
float64(message.FourCCVP9), float64(message.FourCCHEVC):
return true, nil return true, nil
} }

View File

@@ -50,6 +50,58 @@ func TestReadTracks(t *testing.T) {
0x44, 0x01, 0xc0, 0xf7, 0xc0, 0xcc, 0x90, 0x44, 0x01, 0xc0, 0xf7, 0xc0, 0xcc, 0x90,
} }
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,
}},
},
},
}
for _, ca := range []struct { for _, ca := range []struct {
name string name string
videoTrack formats.Format videoTrack formats.Format
@@ -180,6 +232,16 @@ func TestReadTracks(t *testing.T) {
}, },
nil, nil,
}, },
{
"obs 30",
&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
@@ -566,57 +628,46 @@ func TestReadTracks(t *testing.T) {
}) })
require.NoError(t, err) require.NoError(t, err)
var spsp h265.SPS var buf bytes.Buffer
err = spsp.Unmarshal(h265SPS) _, err = mp4.Marshal(&buf, hvcc, mp4.Context{})
require.NoError(t, err) require.NoError(t, err)
hvcc := &mp4.HvcC{ err = mrw.Write(&message.ExtendedSequenceStart{
ConfigurationVersion: 1, ChunkStreamID: 4,
GeneralProfileIdc: spsp.ProfileTierLevel.GeneralProfileIdc, MessageStreamID: 0x1000000,
GeneralProfileCompatibility: spsp.ProfileTierLevel.GeneralProfileCompatibilityFlag, FourCC: message.FourCCHEVC,
GeneralConstraintIndicator: [6]uint8{ Config: buf.Bytes(),
h265SPS[7], h265SPS[8], h265SPS[9], })
h265SPS[10], h265SPS[11], h265SPS[12], require.NoError(t, err)
},
GeneralLevelIdc: spsp.ProfileTierLevel.GeneralLevelIdc, case "obs 30":
// MinSpatialSegmentationIdc err := mrw.Write(&message.DataAMF0{
// ParallelismType ChunkStreamID: 4,
ChromaFormatIdc: uint8(spsp.ChromaFormatIdc), MessageStreamID: 1,
BitDepthLumaMinus8: uint8(spsp.BitDepthLumaMinus8), Payload: []interface{}{
BitDepthChromaMinus8: uint8(spsp.BitDepthChromaMinus8), "@setDataFrame",
// AvgFrameRate "onMetaData",
// ConstantFrameRate flvio.AMFMap{
NumTemporalLayers: 1, {
// TemporalIdNested K: "videodatarate",
LengthSizeMinusOne: 3, V: float64(0),
NumOfNaluArrays: 3, },
NaluArrays: []mp4.HEVCNaluArray{ {
{ K: "videocodecid",
NaluType: byte(h265.NALUType_VPS_NUT), V: float64(message.FourCCHEVC),
NumNalus: 1, },
Nalus: []mp4.HEVCNalu{{ {
Length: uint16(len(h265VPS)), K: "audiodatarate",
NALUnit: h265VPS, V: float64(0),
}}, },
}, {
{ K: "audiocodecid",
NaluType: byte(h265.NALUType_SPS_NUT), V: float64(0),
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,
}},
}, },
}, },
} })
require.NoError(t, err)
var buf bytes.Buffer var buf bytes.Buffer
_, err = mp4.Marshal(&buf, hvcc, mp4.Context{}) _, err = mp4.Marshal(&buf, hvcc, mp4.Context{})

View File

@@ -14,9 +14,9 @@ readTimeout: 10s
# Timeout of write operations. # Timeout of write operations.
writeTimeout: 10s writeTimeout: 10s
# Number of read buffers. # Number of read buffers.
# A higher value allows a wider throughput, a lower value allows to save RAM. # A higher value allows to increase throughput, a lower value allows to save RAM.
readBufferCount: 512 readBufferCount: 512
# Maximum size of payload of outgoing UDP packets. # Maximum size of outgoing UDP packets.
# This can be decreased to avoid fragmentation on networks with a low UDP MTU. # This can be decreased to avoid fragmentation on networks with a low UDP MTU.
udpMaxPayloadSize: 1472 udpMaxPayloadSize: 1472