rtmp: support additional Enhanced-RTMP features (#3685)

new features:
* support publishing Opus and AC-3 tracks
* support publishing more than 2 tracks. This is compatible with OBS multitrack video and OBS VOD audio track
This commit is contained in:
aler9 2024-12-28 23:59:42 +01:00
parent df3362aef8
commit 04d4e1668f
44 changed files with 2558 additions and 1012 deletions

View File

@ -26,8 +26,8 @@ Live streams can be published to the server with:
|[WebRTC servers](#webrtc-servers)|WHEP|AV1, VP9, VP8, [H265](#supported-browsers), H264|Opus, G722, G711 (PCMA, PCMU)|
|[RTSP clients](#rtsp-clients)|UDP, TCP, RTSPS|AV1, VP9, VP8, H265, H264, MPEG-4 Video (H263, Xvid), MPEG-1/2 Video, M-JPEG and any RTP-compatible codec|Opus, MPEG-4 Audio (AAC), MPEG-1/2 Audio (MP3), AC-3, G726, G722, G711 (PCMA, PCMU), LPCM and any RTP-compatible codec|
|[RTSP cameras and servers](#rtsp-cameras-and-servers)|UDP, UDP-Multicast, TCP, RTSPS|AV1, VP9, VP8, H265, H264, MPEG-4 Video (H263, Xvid), MPEG-1/2 Video, M-JPEG and any RTP-compatible codec|Opus, MPEG-4 Audio (AAC), MPEG-1/2 Audio (MP3), AC-3, G726, G722, G711 (PCMA, PCMU), LPCM and any RTP-compatible codec|
|[RTMP clients](#rtmp-clients)|RTMP, RTMPS, Enhanced RTMP|AV1, VP9, H265, H264|MPEG-4 Audio (AAC), MPEG-1/2 Audio (MP3), G711 (PCMA, PCMU), LPCM|
|[RTMP cameras and servers](#rtmp-cameras-and-servers)|RTMP, RTMPS, Enhanced RTMP|AV1, VP9, H265, H264|MPEG-4 Audio (AAC), MPEG-1/2 Audio (MP3), G711 (PCMA, PCMU), LPCM|
|[RTMP clients](#rtmp-clients)|RTMP, RTMPS, Enhanced RTMP|AV1, VP9, H265, H264|Opus, MPEG-4 Audio (AAC), MPEG-1/2 Audio (MP3), AC-3, G711 (PCMA, PCMU), LPCM|
|[RTMP cameras and servers](#rtmp-cameras-and-servers)|RTMP, RTMPS, Enhanced RTMP|AV1, VP9, H265, H264|Opus, MPEG-4 Audio (AAC), MPEG-1/2 Audio (MP3), AC-3, G711 (PCMA, PCMU), LPCM|
|[HLS cameras and servers](#hls-cameras-and-servers)|Low-Latency HLS, MP4-based HLS, legacy HLS|AV1, VP9, [H265](#supported-browsers-1), H264|Opus, MPEG-4 Audio (AAC)|
|[UDP/MPEG-TS](#udpmpeg-ts)|Unicast, broadcast, multicast|H265, H264, MPEG-4 Video (H263, Xvid), MPEG-1/2 Video|Opus, MPEG-4 Audio (AAC), MPEG-1/2 Audio (MP3), AC-3|
|[Raspberry Pi Cameras](#raspberry-pi-cameras)||H264||
@ -2438,9 +2438,10 @@ All the code in this repository is released under the [MIT License](LICENSE). Co
|----|----|
|[RTSP / RTP / RTCP specifications](https://github.com/bluenviron/gortsplib#specifications)|RTSP|
|[HLS specifications](https://github.com/bluenviron/gohlslib#specifications)|HLS|
|[RTMP](https://rtmp.veriskope.com/pdf/rtmp_specification_1.0.pdf)|RTMP|
|[Enhanced RTMP v1](https://veovera.org/docs/enhanced/enhanced-rtmp-v1.pdf)|RTMP|
|[Action Message Format](https://rtmp.veriskope.com/pdf/amf0-file-format-specification.pdf)|RTMP|
|[Action Message Format - AMF 0](https://veovera.org/docs/legacy/amf0-file-format-spec.pdf)|RTMP|
|[FLV](https://veovera.org/docs/legacy/video-file-format-v10-1-spec.pdf)|RTMP|
|[RTMP](https://veovera.org/docs/legacy/rtmp-v1-0-spec.pdf)|RTMP|
|[Enhanced RTMP v2](https://veovera.org/docs/enhanced/enhanced-rtmp-v2.pdf)|RTMP|
|[WebRTC: Real-Time Communication in Browsers](https://www.w3.org/TR/webrtc/)|WebRTC|
|[RFC8835, Transports for WebRTC](https://datatracker.ietf.org/doc/html/rfc8835)|WebRTC|
|[RFC7742, WebRTC Video Processing and Codec Requirements](https://datatracker.ietf.org/doc/html/rfc7742)|WebRTC|

View File

@ -433,7 +433,10 @@ func TestAPIProtocolListGet(t *testing.T) {
conn, err := rtmp.NewClientConn(nconn, u, true)
require.NoError(t, err)
_, err = rtmp.NewWriter(conn, test.FormatH264, nil)
w, err := rtmp.NewWriter(conn, test.FormatH264, nil)
require.NoError(t, err)
err = w.WriteH264(2*time.Second, 2*time.Second, [][]byte{{5, 2, 3, 4}})
require.NoError(t, err)
time.Sleep(500 * time.Millisecond)
@ -1006,7 +1009,10 @@ func TestAPIProtocolKick(t *testing.T) {
conn, err := rtmp.NewClientConn(nconn, u, true)
require.NoError(t, err)
_, err = rtmp.NewWriter(conn, test.FormatH264, nil)
w, err := rtmp.NewWriter(conn, test.FormatH264, nil)
require.NoError(t, err)
err = w.WriteH264(2*time.Second, 2*time.Second, [][]byte{{5, 2, 3, 4}})
require.NoError(t, err)
case "webrtc":

View File

@ -206,8 +206,12 @@ webrtc_sessions_bytes_sent 0
conn, err := rtmp.NewClientConn(nconn, u, true)
require.NoError(t, err)
_, err = rtmp.NewWriter(conn, test.FormatH264, nil)
w, err := rtmp.NewWriter(conn, test.FormatH264, nil)
require.NoError(t, err)
err = w.WriteH264(2*time.Second, 2*time.Second, [][]byte{{5, 2, 3, 4}})
require.NoError(t, err)
<-terminate
}()
@ -223,8 +227,12 @@ webrtc_sessions_bytes_sent 0
conn, err := rtmp.NewClientConn(nconn, u, true)
require.NoError(t, err)
_, err = rtmp.NewWriter(conn, test.FormatH264, nil)
w, err := rtmp.NewWriter(conn, test.FormatH264, nil)
require.NoError(t, err)
err = w.WriteH264(2*time.Second, 2*time.Second, [][]byte{{5, 2, 3, 4}})
require.NoError(t, err)
<-terminate
}()
@ -436,7 +444,7 @@ webrtc_sessions_bytes_sent 0
wg.Wait()
})
t.Run("servers deleted", func(t *testing.T) {
t.Run("servers disabled", func(t *testing.T) {
httpRequest(t, hc, http.MethodPatch, "http://localhost:9997/v3/config/global/patch", map[string]interface{}{
"rtsp": false,
"rtmp": false,

View File

@ -362,6 +362,37 @@ func TestPathRunOnRead(t *testing.T) {
require.NoError(t, err)
defer source.Close()
writerDone := make(chan struct{})
defer func() { <-writerDone }()
writerTerminate := make(chan struct{})
defer close(writerTerminate)
go func() {
defer close(writerDone)
i := 0
for {
select {
case <-time.After(100 * time.Millisecond):
case <-writerTerminate:
return
}
err2 := source.WritePacketRTP(media0, &rtp.Packet{
Header: rtp.Header{
Version: 2,
Marker: true,
PayloadType: 96,
SequenceNumber: uint16(123 + i),
Timestamp: uint32(45343 + i*90000),
SSRC: 563423,
},
Payload: []byte{5},
})
require.NoError(t, err2)
i++
}
}()
switch ca {
case "rtsp":
reader := gortsplib.Client{}
@ -426,6 +457,23 @@ func TestPathRunOnRead(t *testing.T) {
conn, err := rtmp.NewClientConn(nconn, u, false)
require.NoError(t, err)
go func() {
for i := uint16(0); i < 3; i++ {
err2 := source.WritePacketRTP(media0, &rtp.Packet{
Header: rtp.Header{
Version: 2,
Marker: true,
PayloadType: 96,
SequenceNumber: 123 + i,
Timestamp: 45343 + uint32(i)*2*90000,
SSRC: 563423,
},
Payload: []byte{5},
})
require.NoError(t, err2)
}
}()
_, err = rtmp.NewReader(conn)
require.NoError(t, err)
@ -455,37 +503,6 @@ func TestPathRunOnRead(t *testing.T) {
Log: test.NilLogger,
}
writerDone := make(chan struct{})
defer func() { <-writerDone }()
writerTerminate := make(chan struct{})
defer close(writerTerminate)
go func() {
defer close(writerDone)
i := uint16(0)
for {
select {
case <-time.After(100 * time.Millisecond):
case <-writerTerminate:
return
}
err2 := source.WritePacketRTP(media0, &rtp.Packet{
Header: rtp.Header{
Version: 2,
Marker: true,
PayloadType: 96,
SequenceNumber: 123 + i,
Timestamp: 45343,
SSRC: 563423,
},
Payload: []byte{5},
})
require.NoError(t, err2)
i++
}
}()
_, err = c.Read(context.Background())
require.NoError(t, err)
defer checkClose(t, c.Close)

View File

@ -86,7 +86,6 @@ func setupVideo(
return (*w).WriteH264(
timestampToDuration(tunit.PTS, videoFormatH264.ClockRate()),
timestampToDuration(dts, videoFormatH264.ClockRate()),
idrPresent,
tunit.AU)
})

View File

@ -1,26 +0,0 @@
package message
import (
"fmt"
"github.com/bluenviron/mediamtx/internal/protocols/rtmp/rawmessage"
)
// ExtendedMPEG2TSSequenceStart is a MPEG2-TS sequence start extended message.
type ExtendedMPEG2TSSequenceStart struct {
FourCC FourCC
}
func (m *ExtendedMPEG2TSSequenceStart) unmarshal(raw *rawmessage.Message) error {
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")
}
func (m ExtendedMPEG2TSSequenceStart) marshal() (*rawmessage.Message, error) {
return nil, fmt.Errorf("TODO")
}

View File

@ -1,26 +0,0 @@
package message
import (
"fmt"
"github.com/bluenviron/mediamtx/internal/protocols/rtmp/rawmessage"
)
// ExtendedSequenceEnd is a sequence end extended message.
type ExtendedSequenceEnd struct {
FourCC FourCC
}
func (m *ExtendedSequenceEnd) unmarshal(raw *rawmessage.Message) error {
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 nil
}
func (m ExtendedSequenceEnd) marshal() (*rawmessage.Message, error) {
return nil, fmt.Errorf("TODO")
}

View File

@ -18,19 +18,15 @@ const (
TypeSetChunkSize Type = 1
TypeAbortMessage Type = 2
TypeAcknowledge Type = 3
TypeUserControl Type = 4
TypeSetWindowAckSize Type = 5
TypeSetPeerBandwidth Type = 6
TypeUserControl Type = 4
TypeCommandAMF3 Type = 17
TypeCommandAMF0 Type = 20
TypeDataAMF3 Type = 15
TypeDataAMF0 Type = 18
TypeAudio Type = 8
TypeVideo Type = 9
TypeAudio Type = 8
TypeVideo Type = 9
TypeDataAMF3 Type = 15
TypeDataAMF0 Type = 18
TypeCommandAMF3 Type = 17
TypeCommandAMF0 Type = 20
)
// UserControlType is a user control type.
@ -47,27 +43,48 @@ const (
UserControlTypePingResponse UserControlType = 7
)
// ExtendedType is a message extended type.
type ExtendedType uint8
// AudioExType is an audio message extended type.
type AudioExType uint8
// message extended types.
// audio message extended types.
const (
ExtendedTypeSequenceStart ExtendedType = 0
ExtendedTypeCodedFrames ExtendedType = 1
ExtendedTypeSequenceEnd ExtendedType = 2
ExtendedTypeFramesX ExtendedType = 3
ExtendedTypeMetadata ExtendedType = 4
ExtendedTypeMPEG2TSSequenceStart ExtendedType = 5
AudioExTypeSequenceStart AudioExType = 0
AudioExTypeCodedFrames AudioExType = 1
AudioExTypeSequenceEnd AudioExType = 2
AudioExTypeMultichannelConfig AudioExType = 4
AudioExTypeMultitrack AudioExType = 5
)
// FourCC is an identifier of a video codec.
// VideoExType is a video message extended type.
type VideoExType uint8
// video message extended types.
const (
VideoExTypeSequenceStart VideoExType = 0
VideoExTypeCodedFrames VideoExType = 1
VideoExTypeSequenceEnd VideoExType = 2
VideoExTypeFramesX VideoExType = 3
VideoExTypeMetadata VideoExType = 4
VideoExTypeMPEG2TSSequenceStart VideoExType = 5
VideoExTypeMultitrack VideoExType = 6
)
// FourCC is an identifier of a Extended-RTMP codec.
type FourCC uint32
// video codec identifiers.
// codec identifiers.
var (
// video
FourCCAV1 FourCC = 'a'<<24 | 'v'<<16 | '0'<<8 | '1'
FourCCVP9 FourCC = 'v'<<24 | 'p'<<16 | '0'<<8 | '9'
FourCCHEVC FourCC = 'h'<<24 | 'v'<<16 | 'c'<<8 | '1'
FourCCAVC FourCC = 'a'<<24 | 'v'<<16 | 'c'<<8 | '1'
// audio
FourCCOpus FourCC = 'O'<<24 | 'p'<<16 | 'u'<<8 | 's'
FourCCAC3 FourCC = 'a'<<24 | 'c'<<16 | '-'<<8 | '3'
FourCCMP4A FourCC = 'm'<<24 | 'p'<<16 | '4'<<8 | 'a'
FourCCMP3 FourCC = '.'<<24 | 'm'<<16 | 'p'<<8 | '3'
)
// Message is a message.

View File

@ -0,0 +1,61 @@
package message
import (
"fmt"
"time"
"github.com/bluenviron/mediamtx/internal/protocols/rtmp/rawmessage"
)
// AudioExCodedFrames is a CodedFrames extended message.
type AudioExCodedFrames struct {
ChunkStreamID byte
DTS time.Duration
MessageStreamID uint32
FourCC FourCC
Payload []byte
}
func (m *AudioExCodedFrames) unmarshal(raw *rawmessage.Message) error {
if len(raw.Body) < 5 {
return fmt.Errorf("not enough bytes")
}
m.ChunkStreamID = raw.ChunkStreamID
m.DTS = raw.Timestamp
m.MessageStreamID = raw.MessageStreamID
m.FourCC = FourCC(raw.Body[1])<<24 | FourCC(raw.Body[2])<<16 | FourCC(raw.Body[3])<<8 | FourCC(raw.Body[4])
switch m.FourCC {
case FourCCOpus, FourCCAC3, FourCCMP4A:
default:
return fmt.Errorf("unsupported fourCC: %v", m.FourCC)
}
m.Payload = raw.Body[5:]
return nil
}
func (m AudioExCodedFrames) marshalBodySize() int {
return 5 + len(m.Payload)
}
func (m AudioExCodedFrames) marshal() (*rawmessage.Message, error) {
body := make([]byte, m.marshalBodySize())
body[0] = (9 << 4) | byte(AudioExTypeCodedFrames)
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)
return &rawmessage.Message{
ChunkStreamID: m.ChunkStreamID,
Timestamp: m.DTS,
Type: uint8(TypeAudio),
MessageStreamID: m.MessageStreamID,
Body: body,
}, nil
}

View File

@ -0,0 +1,107 @@
package message
import (
"fmt"
"github.com/bluenviron/mediamtx/internal/protocols/rtmp/rawmessage"
)
// AudioExChannelOrder is an audio channel order.
type AudioExChannelOrder uint8
// audio channel orders.
const (
AudioExChannelOrderUnspecified AudioExChannelOrder = 0
AudioExChannelOrderNative AudioExChannelOrder = 1
AudioExChannelOrderCustom AudioExChannelOrder = 2
)
// AudioExMultichannelConfig is a multichannel config extended message.
type AudioExMultichannelConfig struct {
ChunkStreamID byte
MessageStreamID uint32
FourCC FourCC
AudioChannelOrder AudioExChannelOrder
ChannelCount uint8
AudioChannelMapping uint8 // if AudioChannelOrder == AudioExChannelOrderCustom
AudioChannelFlags uint32 // if AudioChannelOrder == AudioExChannelOrderNative
}
func (m *AudioExMultichannelConfig) unmarshal(raw *rawmessage.Message) error {
if len(raw.Body) < 7 {
return fmt.Errorf("not enough bytes")
}
m.ChunkStreamID = raw.ChunkStreamID
m.MessageStreamID = raw.MessageStreamID
m.FourCC = FourCC(raw.Body[1])<<24 | FourCC(raw.Body[2])<<16 | FourCC(raw.Body[3])<<8 | FourCC(raw.Body[4])
switch m.FourCC {
case FourCCOpus, FourCCAC3, FourCCMP4A:
default:
return fmt.Errorf("unsupported fourCC: %v", m.FourCC)
}
m.AudioChannelOrder = AudioExChannelOrder(raw.Body[5])
m.ChannelCount = raw.Body[6]
switch m.AudioChannelOrder {
case AudioExChannelOrderCustom:
if len(raw.Body) != 8 {
return fmt.Errorf("invalid AudioExMultichannelConfig size")
}
m.AudioChannelMapping = raw.Body[7]
case AudioExChannelOrderNative:
if len(raw.Body) != 11 {
return fmt.Errorf("invalid AudioExMultichannelConfig size")
}
m.AudioChannelFlags = uint32(raw.Body[7])<<24 | uint32(raw.Body[8])<<16 |
uint32(raw.Body[9])<<8 | uint32(raw.Body[10])
case AudioExChannelOrderUnspecified:
if len(raw.Body) != 7 {
return fmt.Errorf("invalid AudioExMultichannelConfig size")
}
default:
return fmt.Errorf("invalid AudioChannelOrder: %v", m.AudioChannelOrder)
}
return nil
}
func (m AudioExMultichannelConfig) marshal() (*rawmessage.Message, error) {
var addBody []byte
switch m.AudioChannelOrder {
case AudioExChannelOrderCustom:
addBody = []byte{m.AudioChannelMapping}
case AudioExChannelOrderNative:
addBody = []byte{
byte(m.AudioChannelFlags >> 24),
byte(m.AudioChannelFlags >> 16),
byte(m.AudioChannelFlags >> 8),
byte(m.AudioChannelFlags),
}
}
body := make([]byte, 7+len(addBody))
body[0] = (9 << 4) | byte(AudioExTypeMultichannelConfig)
body[1] = uint8(m.FourCC >> 24)
body[2] = uint8(m.FourCC >> 16)
body[3] = uint8(m.FourCC >> 8)
body[4] = uint8(m.FourCC)
body[5] = uint8(m.AudioChannelOrder)
body[6] = m.ChannelCount
copy(body[7:], addBody)
return &rawmessage.Message{
ChunkStreamID: m.ChunkStreamID,
Type: uint8(TypeAudio),
MessageStreamID: m.MessageStreamID,
Body: body,
}, nil
}

View File

@ -0,0 +1,95 @@
package message //nolint:dupl
import (
"fmt"
"github.com/bluenviron/mediamtx/internal/protocols/rtmp/rawmessage"
)
// AudioExMultitrackType is a multitrack type.
type AudioExMultitrackType uint8
// multitrack types.
const (
AudioExMultitrackTypeOneTrack AudioExMultitrackType = 0
AudioExMultitrackTypeManyTracks AudioExMultitrackType = 1
AudioExMultitrackTypeManyTracksManyCodecs AudioExMultitrackType = 2
)
// AudioExMultitrack is a multitrack extended message.
type AudioExMultitrack struct {
MultitrackType AudioExMultitrackType
TrackID uint8
Wrapped Message
}
func (m *AudioExMultitrack) unmarshal(raw *rawmessage.Message) error { //nolint:dupl
if len(raw.Body) < 7 {
return fmt.Errorf("not enough bytes")
}
m.MultitrackType = AudioExMultitrackType(raw.Body[1] >> 4)
switch m.MultitrackType {
case AudioExMultitrackTypeOneTrack:
default:
return fmt.Errorf("unsupported multitrack type: %v", m.MultitrackType)
}
packetType := AudioExType(raw.Body[1] & 0b1111)
switch packetType {
case AudioExTypeSequenceStart:
m.Wrapped = &AudioExSequenceStart{}
case AudioExTypeSequenceEnd:
m.Wrapped = &AudioExSequenceEnd{}
case AudioExTypeMultichannelConfig:
m.Wrapped = &AudioExMultichannelConfig{}
case AudioExTypeCodedFrames:
m.Wrapped = &AudioExCodedFrames{}
default:
return fmt.Errorf("unsupported audio multitrack packet type: %v", packetType)
}
m.TrackID = raw.Body[6]
wrappedBody := make([]byte, 5+len(raw.Body[7:]))
copy(wrappedBody[1:], raw.Body[2:]) // fourCC
copy(wrappedBody[5:], raw.Body[7:]) // body
err := m.Wrapped.unmarshal(&rawmessage.Message{
ChunkStreamID: raw.ChunkStreamID,
MessageStreamID: raw.MessageStreamID,
Timestamp: raw.Timestamp,
Body: wrappedBody,
})
if err != nil {
return err
}
return nil
}
func (m AudioExMultitrack) marshal() (*rawmessage.Message, error) {
wrappedEnc, err := m.Wrapped.marshal()
if err != nil {
return nil, err
}
body := make([]byte, 7+len(wrappedEnc.Body)-5)
body[0] = (9 << 4) | byte(AudioExTypeMultitrack)
body[1] = wrappedEnc.Body[0] & 0b1111
copy(body[2:], wrappedEnc.Body[1:])
body[6] = m.TrackID
copy(body[7:], wrappedEnc.Body[5:])
return &rawmessage.Message{
ChunkStreamID: wrappedEnc.ChunkStreamID,
MessageStreamID: wrappedEnc.MessageStreamID,
Timestamp: wrappedEnc.Timestamp,
Type: uint8(TypeAudio),
Body: body,
}, nil
}

View File

@ -0,0 +1,49 @@
package message
import (
"fmt"
"github.com/bluenviron/mediamtx/internal/protocols/rtmp/rawmessage"
)
// AudioExSequenceEnd is a sequence end extended message.
type AudioExSequenceEnd struct {
ChunkStreamID byte
MessageStreamID uint32
FourCC FourCC
}
func (m *AudioExSequenceEnd) unmarshal(raw *rawmessage.Message) error {
if len(raw.Body) != 5 {
return fmt.Errorf("not enough bytes")
}
m.ChunkStreamID = raw.ChunkStreamID
m.MessageStreamID = raw.MessageStreamID
m.FourCC = FourCC(raw.Body[1])<<24 | FourCC(raw.Body[2])<<16 | FourCC(raw.Body[3])<<8 | FourCC(raw.Body[4])
switch m.FourCC {
case FourCCOpus, FourCCAC3, FourCCMP4A:
default:
return fmt.Errorf("unsupported fourCC: %v", m.FourCC)
}
return nil
}
func (m AudioExSequenceEnd) marshal() (*rawmessage.Message, error) {
body := make([]byte, 5)
body[0] = (9 << 4) | byte(AudioExTypeSequenceEnd)
body[1] = uint8(m.FourCC >> 24)
body[2] = uint8(m.FourCC >> 16)
body[3] = uint8(m.FourCC >> 8)
body[4] = uint8(m.FourCC)
return &rawmessage.Message{
ChunkStreamID: m.ChunkStreamID,
Type: uint8(TypeAudio),
MessageStreamID: m.MessageStreamID,
Body: body,
}, nil
}

View File

@ -0,0 +1,89 @@
package message
import (
"fmt"
"github.com/bluenviron/mediacommon/pkg/codecs/mpeg4audio"
"github.com/bluenviron/mediamtx/internal/protocols/rtmp/rawmessage"
)
// AudioExSequenceStart is a sequence start extended message.
type AudioExSequenceStart struct {
ChunkStreamID byte
MessageStreamID uint32
FourCC FourCC
OpusHeader *OpusIDHeader
AACHeader *mpeg4audio.AudioSpecificConfig
}
func (m *AudioExSequenceStart) unmarshal(raw *rawmessage.Message) error {
if len(raw.Body) < 5 {
return fmt.Errorf("not enough bytes")
}
m.ChunkStreamID = raw.ChunkStreamID
m.MessageStreamID = raw.MessageStreamID
m.FourCC = FourCC(raw.Body[1])<<24 | FourCC(raw.Body[2])<<16 | FourCC(raw.Body[3])<<8 | FourCC(raw.Body[4])
switch m.FourCC {
case FourCCOpus:
m.OpusHeader = &OpusIDHeader{}
err := m.OpusHeader.unmarshal(raw.Body[5:])
if err != nil {
return fmt.Errorf("invalid Opus ID header: %w", err)
}
case FourCCAC3:
if len(raw.Body) != 5 {
return fmt.Errorf("unexpected size")
}
case FourCCMP4A:
m.AACHeader = &mpeg4audio.AudioSpecificConfig{}
err := m.AACHeader.Unmarshal(raw.Body[5:])
if err != nil {
return fmt.Errorf("invalid MPEG-4 audio config: %w", err)
}
default:
return fmt.Errorf("unsupported fourCC: %v", m.FourCC)
}
return nil
}
func (m AudioExSequenceStart) marshal() (*rawmessage.Message, error) {
var addBody []byte
switch m.FourCC {
case FourCCOpus:
buf, err := m.OpusHeader.marshal()
if err != nil {
return nil, err
}
addBody = buf
case FourCCMP4A:
buf, err := m.AACHeader.Marshal()
if err != nil {
return nil, err
}
addBody = buf
}
body := make([]byte, 5+len(addBody))
body[0] = (9 << 4) | byte(AudioExTypeSequenceStart)
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:], addBody)
return &rawmessage.Message{
ChunkStreamID: m.ChunkStreamID,
Type: uint8(TypeAudio),
MessageStreamID: m.MessageStreamID,
Body: body,
}, nil
}

View File

@ -7,8 +7,8 @@ import (
"github.com/bluenviron/mediamtx/internal/protocols/rtmp/rawmessage"
)
// ExtendedCodedFrames is a CodedFrames extended message.
type ExtendedCodedFrames struct {
// VideoExCodedFrames is a CodedFrames extended message.
type VideoExCodedFrames struct {
ChunkStreamID byte
DTS time.Duration
MessageStreamID uint32
@ -17,40 +17,46 @@ type ExtendedCodedFrames struct {
Payload []byte
}
func (m *ExtendedCodedFrames) unmarshal(raw *rawmessage.Message) error {
if len(raw.Body) < 8 {
func (m *VideoExCodedFrames) unmarshal(raw *rawmessage.Message) error {
if len(raw.Body) < 5 {
return fmt.Errorf("not enough bytes")
}
m.ChunkStreamID = raw.ChunkStreamID
m.DTS = raw.Timestamp
m.MessageStreamID = raw.MessageStreamID
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 {
m.FourCC = FourCC(raw.Body[1])<<24 | FourCC(raw.Body[2])<<16 | FourCC(raw.Body[3])<<8 | FourCC(raw.Body[4])
switch m.FourCC {
case FourCCAVC, FourCCHEVC:
if len(raw.Body) < 8 {
return fmt.Errorf("bnot enough bytes")
}
m.PTSDelta = time.Duration(uint32(raw.Body[5])<<16|uint32(raw.Body[6])<<8|uint32(raw.Body[7])) * time.Millisecond
m.Payload = raw.Body[8:]
} else {
case FourCCAV1, FourCCVP9:
m.Payload = raw.Body[5:]
default:
return fmt.Errorf("unsupported fourCC: %v", m.FourCC)
}
return nil
}
func (m ExtendedCodedFrames) marshalBodySize() int {
var l int
if m.FourCC == FourCCHEVC {
l = 8 + len(m.Payload)
} else {
l = 5 + len(m.Payload)
func (m VideoExCodedFrames) marshalBodySize() int {
switch m.FourCC {
case FourCCAVC, FourCCHEVC:
return 8 + len(m.Payload)
}
return l
return 5 + len(m.Payload)
}
func (m ExtendedCodedFrames) marshal() (*rawmessage.Message, error) {
func (m VideoExCodedFrames) marshal() (*rawmessage.Message, error) {
body := make([]byte, m.marshalBodySize())
body[0] = 0b10000000 | byte(ExtendedTypeCodedFrames)
body[0] = 0b10000000 | byte(VideoExTypeCodedFrames)
body[1] = uint8(m.FourCC >> 24)
body[2] = uint8(m.FourCC >> 16)
body[3] = uint8(m.FourCC >> 8)

View File

@ -7,8 +7,8 @@ import (
"github.com/bluenviron/mediamtx/internal/protocols/rtmp/rawmessage"
)
// ExtendedFramesX is a FramesX extended message.
type ExtendedFramesX struct {
// VideoExFramesX is a FramesX extended message.
type VideoExFramesX struct {
ChunkStreamID byte
DTS time.Duration
MessageStreamID uint32
@ -16,7 +16,7 @@ type ExtendedFramesX struct {
Payload []byte
}
func (m *ExtendedFramesX) unmarshal(raw *rawmessage.Message) error {
func (m *VideoExFramesX) unmarshal(raw *rawmessage.Message) error {
if len(raw.Body) < 6 {
return fmt.Errorf("not enough bytes")
}
@ -24,20 +24,27 @@ func (m *ExtendedFramesX) unmarshal(raw *rawmessage.Message) error {
m.ChunkStreamID = raw.ChunkStreamID
m.DTS = raw.Timestamp
m.MessageStreamID = raw.MessageStreamID
m.FourCC = FourCC(raw.Body[1])<<24 | FourCC(raw.Body[2])<<16 | FourCC(raw.Body[3])<<8 | FourCC(raw.Body[4])
switch m.FourCC {
case FourCCAV1, FourCCVP9, FourCCHEVC, FourCCAVC:
default:
return fmt.Errorf("unsupported fourCC: %v", m.FourCC)
}
m.Payload = raw.Body[5:]
return nil
}
func (m ExtendedFramesX) marshalBodySize() int {
func (m VideoExFramesX) marshalBodySize() int {
return 5 + len(m.Payload)
}
func (m ExtendedFramesX) marshal() (*rawmessage.Message, error) {
func (m VideoExFramesX) marshal() (*rawmessage.Message, error) {
body := make([]byte, m.marshalBodySize())
body[0] = 0b10000000 | byte(ExtendedTypeFramesX)
body[0] = 0b10000000 | byte(VideoExTypeFramesX)
body[1] = uint8(m.FourCC >> 24)
body[2] = uint8(m.FourCC >> 16)
body[3] = uint8(m.FourCC >> 8)

View File

@ -8,8 +8,8 @@ import (
"github.com/bluenviron/mediamtx/internal/protocols/rtmp/rawmessage"
)
// ExtendedMetadata is a metadata extended message.
type ExtendedMetadata struct {
// VideoExMetadata is a metadata extended message.
type VideoExMetadata struct {
ChunkStreamID byte
DTS time.Duration
MessageStreamID uint32
@ -17,7 +17,7 @@ type ExtendedMetadata struct {
Payload amf0.Data
}
func (m *ExtendedMetadata) unmarshal(raw *rawmessage.Message) error {
func (m *VideoExMetadata) unmarshal(raw *rawmessage.Message) error {
if len(raw.Body) < 6 {
return fmt.Errorf("invalid body size")
}
@ -25,7 +25,13 @@ func (m *ExtendedMetadata) unmarshal(raw *rawmessage.Message) error {
m.ChunkStreamID = raw.ChunkStreamID
m.DTS = raw.Timestamp
m.MessageStreamID = raw.MessageStreamID
m.FourCC = FourCC(raw.Body[1])<<24 | FourCC(raw.Body[2])<<16 | FourCC(raw.Body[3])<<8 | FourCC(raw.Body[4])
switch m.FourCC {
case FourCCAV1, FourCCVP9, FourCCHEVC:
default:
return fmt.Errorf("unsupported fourCC: %v", m.FourCC)
}
var err error
m.Payload, err = amf0.Unmarshal(raw.Body[5:])
@ -36,7 +42,7 @@ func (m *ExtendedMetadata) unmarshal(raw *rawmessage.Message) error {
return nil
}
func (m ExtendedMetadata) marshalBodySize() (int, error) {
func (m VideoExMetadata) marshalBodySize() (int, error) {
ms, err := m.Payload.MarshalSize()
if err != nil {
return 0, err
@ -44,14 +50,14 @@ func (m ExtendedMetadata) marshalBodySize() (int, error) {
return 5 + ms, nil
}
func (m ExtendedMetadata) marshal() (*rawmessage.Message, error) {
func (m VideoExMetadata) marshal() (*rawmessage.Message, error) {
mbs, err := m.marshalBodySize()
if err != nil {
return nil, err
}
body := make([]byte, mbs)
body[0] = 0b10000000 | byte(ExtendedTypeMetadata)
body[0] = 0b10000000 | byte(VideoExTypeMetadata)
body[1] = uint8(m.FourCC >> 24)
body[2] = uint8(m.FourCC >> 16)
body[3] = uint8(m.FourCC >> 8)

View File

@ -0,0 +1,95 @@
package message //nolint:dupl
import (
"fmt"
"github.com/bluenviron/mediamtx/internal/protocols/rtmp/rawmessage"
)
// VideoExMultitrackType is a multitrack type.
type VideoExMultitrackType uint8
// multitrack types.
const (
VideoExMultitrackTypeOneTrack VideoExMultitrackType = 0
VideoExMultitrackTypeManyTracks VideoExMultitrackType = 1
VideoExMultitrackTypeManyTracksManyCodecs VideoExMultitrackType = 2
)
// VideoExMultitrack is a multitrack extended message.
type VideoExMultitrack struct {
MultitrackType VideoExMultitrackType
TrackID uint8
Wrapped Message
}
func (m *VideoExMultitrack) unmarshal(raw *rawmessage.Message) error { //nolint:dupl
if len(raw.Body) < 7 {
return fmt.Errorf("not enough bytes")
}
m.MultitrackType = VideoExMultitrackType(raw.Body[1] >> 4)
switch m.MultitrackType {
case VideoExMultitrackTypeOneTrack:
default:
return fmt.Errorf("unsupported multitrack type: %v", m.MultitrackType)
}
packetType := VideoExType(raw.Body[1] & 0b1111)
switch packetType {
case VideoExTypeSequenceStart:
m.Wrapped = &VideoExSequenceStart{}
case VideoExTypeSequenceEnd:
m.Wrapped = &VideoExSequenceEnd{}
case VideoExTypeCodedFrames:
m.Wrapped = &VideoExCodedFrames{}
case VideoExTypeFramesX:
m.Wrapped = &VideoExFramesX{}
default:
return fmt.Errorf("unsupported video multitrack packet type: %v", packetType)
}
m.TrackID = raw.Body[6]
wrappedBody := make([]byte, 5+len(raw.Body[7:]))
copy(wrappedBody[1:], raw.Body[2:]) // fourCC
copy(wrappedBody[5:], raw.Body[7:]) // body
err := m.Wrapped.unmarshal(&rawmessage.Message{
ChunkStreamID: raw.ChunkStreamID,
MessageStreamID: raw.MessageStreamID,
Timestamp: raw.Timestamp,
Body: wrappedBody,
})
if err != nil {
return err
}
return nil
}
func (m VideoExMultitrack) marshal() (*rawmessage.Message, error) {
wrappedEnc, err := m.Wrapped.marshal()
if err != nil {
return nil, err
}
body := make([]byte, 7+len(wrappedEnc.Body)-5)
body[0] = 0b10000000 | byte(VideoExTypeMultitrack)
body[1] = wrappedEnc.Body[0] & 0b1111
copy(body[2:], wrappedEnc.Body[1:])
body[6] = m.TrackID
copy(body[7:], wrappedEnc.Body[5:])
return &rawmessage.Message{
ChunkStreamID: wrappedEnc.ChunkStreamID,
MessageStreamID: wrappedEnc.MessageStreamID,
Timestamp: wrappedEnc.Timestamp,
Type: uint8(TypeVideo),
Body: body,
}, nil
}

View File

@ -6,40 +6,43 @@ import (
"github.com/bluenviron/mediamtx/internal/protocols/rtmp/rawmessage"
)
// ExtendedSequenceStart is a sequence start extended message.
type ExtendedSequenceStart struct {
// VideoExSequenceEnd is a sequence end extended message.
type VideoExSequenceEnd struct {
ChunkStreamID byte
MessageStreamID uint32
FourCC FourCC
Config []byte
}
func (m *ExtendedSequenceStart) unmarshal(raw *rawmessage.Message) error {
if len(raw.Body) < 6 {
func (m *VideoExSequenceEnd) unmarshal(raw *rawmessage.Message) error {
if len(raw.Body) != 5 {
return fmt.Errorf("not enough bytes")
}
m.ChunkStreamID = raw.ChunkStreamID
m.MessageStreamID = raw.MessageStreamID
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:]
switch m.FourCC {
case FourCCAV1, FourCCVP9, FourCCHEVC, FourCCAVC:
default:
return fmt.Errorf("unsupported fourCC: %v", m.FourCC)
}
return nil
}
func (m ExtendedSequenceStart) marshalBodySize() int {
return 5 + len(m.Config)
func (m VideoExSequenceEnd) marshalBodySize() int {
return 5
}
func (m ExtendedSequenceStart) marshal() (*rawmessage.Message, error) {
func (m VideoExSequenceEnd) marshal() (*rawmessage.Message, error) {
body := make([]byte, m.marshalBodySize())
body[0] = 0b10000000 | byte(ExtendedTypeSequenceStart)
body[0] = 0b10000000 | byte(VideoExTypeSequenceEnd)
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)
return &rawmessage.Message{
ChunkStreamID: m.ChunkStreamID,

View File

@ -0,0 +1,121 @@
package message
import (
"bytes"
"fmt"
"github.com/abema/go-mp4"
"github.com/bluenviron/mediamtx/internal/protocols/rtmp/rawmessage"
)
// VideoExSequenceStart is a sequence start extended message.
type VideoExSequenceStart struct {
ChunkStreamID byte
MessageStreamID uint32
FourCC FourCC
AV1Header *mp4.Av1C
VP9Header *mp4.VpcC
HEVCHeader *mp4.HvcC
AVCHeader *mp4.AVCDecoderConfiguration
}
func (m *VideoExSequenceStart) unmarshal(raw *rawmessage.Message) error {
if len(raw.Body) < 5 {
return fmt.Errorf("not enough bytes")
}
m.ChunkStreamID = raw.ChunkStreamID
m.MessageStreamID = raw.MessageStreamID
m.FourCC = FourCC(raw.Body[1])<<24 | FourCC(raw.Body[2])<<16 | FourCC(raw.Body[3])<<8 | FourCC(raw.Body[4])
switch m.FourCC {
case FourCCAV1:
m.AV1Header = &mp4.Av1C{}
_, err := mp4.Unmarshal(bytes.NewReader(raw.Body[5:]), uint64(len(raw.Body[5:])), m.AV1Header, mp4.Context{})
if err != nil {
return fmt.Errorf("invalid AV1 configuration: %w", err)
}
case FourCCVP9:
m.VP9Header = &mp4.VpcC{}
_, err := mp4.Unmarshal(bytes.NewReader(raw.Body[5:]), uint64(len(raw.Body[5:])), m.VP9Header, mp4.Context{})
if err != nil {
return fmt.Errorf("invalid VP9 configuration: %w", err)
}
case FourCCHEVC:
m.HEVCHeader = &mp4.HvcC{}
_, err := mp4.Unmarshal(bytes.NewReader(raw.Body[5:]), uint64(len(raw.Body[5:])), m.HEVCHeader, mp4.Context{})
if err != nil {
return fmt.Errorf("invalid H265 configuration: %w", err)
}
case FourCCAVC:
m.AVCHeader = &mp4.AVCDecoderConfiguration{}
m.AVCHeader.SetType(mp4.BoxTypeAvcC())
_, err := mp4.Unmarshal(bytes.NewReader(raw.Body[5:]), uint64(len(raw.Body[5:])), m.AVCHeader, mp4.Context{})
if err != nil {
return fmt.Errorf("invalid H264 configuration: %w", err)
}
default:
return fmt.Errorf("unsupported fourCC: %v", m.FourCC)
}
return nil
}
func (m VideoExSequenceStart) marshal() (*rawmessage.Message, error) {
var addBody []byte
switch m.FourCC {
case FourCCAV1:
var buf bytes.Buffer
_, err := mp4.Marshal(&buf, m.AV1Header, mp4.Context{})
if err != nil {
return nil, err
}
addBody = buf.Bytes()
case FourCCVP9:
var buf bytes.Buffer
_, err := mp4.Marshal(&buf, m.VP9Header, mp4.Context{})
if err != nil {
return nil, err
}
addBody = buf.Bytes()
case FourCCHEVC:
var buf bytes.Buffer
_, err := mp4.Marshal(&buf, m.HEVCHeader, mp4.Context{})
if err != nil {
return nil, err
}
addBody = buf.Bytes()
case FourCCAVC:
var buf bytes.Buffer
_, err := mp4.Marshal(&buf, m.AVCHeader, mp4.Context{})
if err != nil {
return nil, err
}
addBody = buf.Bytes()
}
body := make([]byte, 5+len(addBody))
body[0] = 0b10000000 | byte(VideoExTypeSequenceStart)
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:], addBody)
return &rawmessage.Message{
ChunkStreamID: m.ChunkStreamID,
Type: uint8(TypeVideo),
MessageStreamID: m.MessageStreamID,
Body: body,
}, nil
}

View File

@ -0,0 +1,76 @@
package message
import (
"bytes"
"fmt"
)
var magicSignature = []byte{'O', 'p', 'u', 's', 'H', 'e', 'a', 'd'}
// OpusIDHeader is an Opus identification header.
// Specification: https://datatracker.ietf.org/doc/html/rfc7845#section-5.1
type OpusIDHeader struct {
Version uint8
ChannelCount uint8
PreSkip uint16
InputSampleRate uint32
OutputGain uint16
ChannelMappingFamily uint8
ChannelMappingTable []uint8
}
func (h *OpusIDHeader) unmarshal(buf []byte) error {
if len(buf) < 19 {
return fmt.Errorf("not enough bytes")
}
if !bytes.Equal(buf[:8], magicSignature) {
return fmt.Errorf("magic signature not corresponds")
}
h.Version = buf[8]
if h.Version != 1 {
return fmt.Errorf("invalid version: %v", h.Version)
}
h.ChannelCount = buf[9]
h.PreSkip = uint16(buf[10])<<8 | uint16(buf[11])
h.InputSampleRate = uint32(buf[12])<<24 | uint32(buf[13])<<16 | uint32(buf[14])<<8 | uint32(buf[15])
h.OutputGain = uint16(buf[16])<<8 | uint16(buf[17])
h.ChannelMappingFamily = buf[18]
h.ChannelMappingTable = buf[19:]
return nil
}
func (h OpusIDHeader) marshalSize() int {
return 19 + len(h.ChannelMappingTable)
}
func (h OpusIDHeader) marshalTo(buf []byte) (int, error) {
copy(buf[0:], magicSignature)
buf[8] = 1
buf[9] = h.ChannelCount
buf[10] = byte(h.PreSkip >> 8)
buf[11] = byte(h.PreSkip)
buf[12] = byte(h.InputSampleRate >> 24)
buf[13] = byte(h.InputSampleRate >> 16)
buf[14] = byte(h.InputSampleRate >> 8)
buf[15] = byte(h.InputSampleRate)
buf[16] = byte(h.OutputGain >> 8)
buf[17] = byte(h.OutputGain)
buf[18] = h.ChannelMappingFamily
n := copy(buf[19:], h.ChannelMappingTable)
return 19 + n, nil
}
func (h OpusIDHeader) marshal() ([]byte, error) {
buf := make([]byte, h.marshalSize())
_, err := h.marshalTo(buf)
if err != nil {
return nil, err
}
return buf, nil
}

View File

@ -62,45 +62,65 @@ func allocateMessage(raw *rawmessage.Message) (Message, error) {
return &DataAMF0{}, nil
case TypeAudio:
if len(raw.Body) < 1 {
return nil, fmt.Errorf("not enough bytes")
}
if (raw.Body[0] >> 4) == 9 {
extendedType := AudioExType(raw.Body[0] & 0x0F)
switch extendedType {
case AudioExTypeSequenceStart:
return &AudioExSequenceStart{}, nil
case AudioExTypeSequenceEnd:
return &AudioExSequenceEnd{}, nil
case AudioExTypeMultichannelConfig:
return &AudioExMultichannelConfig{}, nil
case AudioExTypeCodedFrames:
return &AudioExCodedFrames{}, nil
case AudioExTypeMultitrack:
return &AudioExMultitrack{}, nil
default:
return nil, fmt.Errorf("unsupported audio extended type: %v", extendedType)
}
}
return &Audio{}, nil
case TypeVideo:
if len(raw.Body) < 5 {
if len(raw.Body) < 1 {
return nil, fmt.Errorf("not enough bytes")
}
if (raw.Body[0] & 0b10000000) != 0 {
fourCC := FourCC(raw.Body[1])<<24 | FourCC(raw.Body[2])<<16 | FourCC(raw.Body[3])<<8 | FourCC(raw.Body[4])
switch fourCC {
case FourCCAV1, FourCCVP9, FourCCHEVC:
default:
return nil, fmt.Errorf("invalid fourCC: %v", fourCC)
}
extendedType := ExtendedType(raw.Body[0] & 0x0F)
extendedType := VideoExType(raw.Body[0] & 0x0F)
switch extendedType {
case ExtendedTypeSequenceStart:
return &ExtendedSequenceStart{}, nil
case VideoExTypeSequenceStart:
return &VideoExSequenceStart{}, nil
case ExtendedTypeCodedFrames:
return &ExtendedCodedFrames{}, nil
case VideoExTypeSequenceEnd:
return &VideoExSequenceEnd{}, nil
case ExtendedTypeSequenceEnd:
return &ExtendedSequenceEnd{}, nil
case VideoExTypeCodedFrames:
return &VideoExCodedFrames{}, nil
case ExtendedTypeFramesX:
return &ExtendedFramesX{}, nil
case VideoExTypeFramesX:
return &VideoExFramesX{}, nil
case ExtendedTypeMetadata:
return &ExtendedMetadata{}, nil
case VideoExTypeMetadata:
return &VideoExMetadata{}, nil
case ExtendedTypeMPEG2TSSequenceStart:
return &ExtendedMPEG2TSSequenceStart{}, nil
case VideoExTypeMultitrack:
return &VideoExMultitrack{}, nil
default:
return nil, fmt.Errorf("invalid extended type: %v", extendedType)
return nil, fmt.Errorf("unsupported video extended type: %v", extendedType)
}
}
return &Video{}, nil

View File

@ -5,8 +5,10 @@ import (
"testing"
"time"
"github.com/abema/go-mp4"
"github.com/stretchr/testify/require"
"github.com/bluenviron/mediacommon/pkg/codecs/mpeg4audio"
"github.com/bluenviron/mediamtx/internal/protocols/rtmp/amf0"
"github.com/bluenviron/mediamtx/internal/protocols/rtmp/bytecounter"
)
@ -22,8 +24,8 @@ var readWriterCases = []struct {
Value: 45953968,
},
[]byte{
0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x4, 0x3,
0x0, 0x0, 0x0, 0x0, 0x2, 0xbd, 0x33, 0xb0,
0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x03,
0x00, 0x00, 0x00, 0x00, 0x02, 0xbd, 0x33, 0xb0,
},
},
{
@ -39,8 +41,9 @@ var readWriterCases = []struct {
Payload: []byte{0x01, 0x02, 0x03, 0x04},
},
[]byte{
0x7, 0x5b, 0xc3, 0x6e, 0x0, 0x0, 0x5, 0x8, 0x0, 0x45, 0x31, 0xf, 0x2f,
0x01, 0x02, 0x03, 0x04,
0x07, 0x5b, 0xc3, 0x6e, 0x00, 0x00, 0x05, 0x08,
0x00, 0x45, 0x31, 0x0f, 0x2f, 0x01, 0x02, 0x03,
0x04,
},
},
{
@ -57,11 +60,126 @@ var readWriterCases = []struct {
Payload: []byte{0x5A, 0xC0, 0x77, 0x40},
},
[]byte{
0x7, 0x5b, 0xc3, 0x6e, 0x0, 0x0, 0x6, 0x8,
0x0, 0x45, 0x31, 0xf, 0xaf, 0x1, 0x5a, 0xc0,
0x07, 0x5b, 0xc3, 0x6e, 0x00, 0x00, 0x06, 0x08,
0x00, 0x45, 0x31, 0x0f, 0xaf, 0x01, 0x5a, 0xc0,
0x77, 0x40,
},
},
{
"audio ex sequence start opus",
&AudioExSequenceStart{
ChunkStreamID: 0x4,
MessageStreamID: 0x1000000,
FourCC: FourCCOpus,
OpusHeader: &OpusIDHeader{
Version: 0x1,
ChannelCount: 0x2,
PreSkip: 0x3801,
InputSampleRate: 0xc05d0000,
ChannelMappingTable: []uint8{},
},
},
[]byte{
0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x08,
0x01, 0x00, 0x00, 0x00, 0x90, 0x4f, 0x70, 0x75,
0x73, 0x4f, 0x70, 0x75, 0x73, 0x48, 0x65, 0x61,
0x64, 0x01, 0x02, 0x38, 0x01, 0xc0, 0x5d, 0x00,
0x00, 0x00, 0x00, 0x00,
},
},
{
"audio ex sequence start aac",
&AudioExSequenceStart{
ChunkStreamID: 0x4,
MessageStreamID: 0x1000000,
FourCC: FourCCMP4A,
AACHeader: &mpeg4audio.AudioSpecificConfig{
Type: mpeg4audio.ObjectTypeAACLC,
SampleRate: 48000,
ChannelCount: 2,
},
},
[]byte{
0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x08,
0x01, 0x00, 0x00, 0x00, 0x90, 0x6d, 0x70, 0x34,
0x61, 0x11, 0x90,
},
},
{
"audio ex sequence start ac3",
&AudioExSequenceStart{
ChunkStreamID: 0x4,
MessageStreamID: 0x1000000,
FourCC: FourCCAC3,
},
[]byte{
0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x08,
0x01, 0x00, 0x00, 0x00, 0x90, 0x61, 0x63, 0x2d,
0x33,
},
},
{
"audio ex sequence end",
&AudioExSequenceEnd{
ChunkStreamID: 4,
MessageStreamID: 0x1000000,
FourCC: FourCCOpus,
},
[]byte{
0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x08,
0x01, 0x00, 0x00, 0x00, 0x92, 0x4f, 0x70, 0x75,
0x73,
},
},
{
"audio ex coded frames",
&AudioExCodedFrames{
ChunkStreamID: 4,
DTS: 15100 * time.Millisecond,
MessageStreamID: 0x1000000,
FourCC: FourCCOpus,
Payload: []byte{1, 2, 3},
},
[]byte{
0x04, 0x00, 0x3a, 0xfc, 0x00, 0x00, 0x08, 0x08,
0x01, 0x00, 0x00, 0x00, 0x91, 0x4f, 0x70, 0x75,
0x73, 0x01, 0x02, 0x03,
},
},
{
"audio ex multichannel config",
&AudioExMultichannelConfig{
ChunkStreamID: 0x4,
MessageStreamID: 0x1000000,
FourCC: 0x4f707573,
AudioChannelOrder: 0x1,
ChannelCount: 0x2,
AudioChannelMapping: 0x0,
AudioChannelFlags: 0x3,
},
[]byte{
0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0b, 0x08,
0x01, 0x00, 0x00, 0x00, 0x94, 0x4f, 0x70, 0x75,
0x73, 0x01, 0x02, 0x00, 0x00, 0x00, 0x03,
},
},
{
"audio ex multitrack",
&AudioExMultitrack{
MultitrackType: AudioExMultitrackTypeOneTrack,
TrackID: 1,
Wrapped: &AudioExSequenceStart{
ChunkStreamID: 0x4,
MessageStreamID: 0x1000000,
FourCC: FourCCAC3,
},
},
[]byte{
0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x08,
0x01, 0x00, 0x00, 0x00, 0x95, 0x00, 0x61, 0x63,
0x2d, 0x33, 0x01,
},
},
{
"command amf0",
&CommandAMF0{
@ -78,14 +196,14 @@ var readWriterCases = []struct {
},
},
[]byte{
0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2f, 0x14,
0x0, 0x5, 0x44, 0x9b, 0x2, 0x0, 0xc, 0x69,
0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2f, 0x14,
0x00, 0x05, 0x44, 0x9b, 0x02, 0x00, 0x0c, 0x69,
0x38, 0x79, 0x79, 0x74, 0x68, 0x72, 0x65, 0x72,
0x67, 0x72, 0x65, 0x0, 0x40, 0xeb, 0x91, 0x0,
0x0, 0x0, 0x0, 0x0, 0x3, 0x0, 0x2, 0x6b,
0x31, 0x2, 0x0, 0x2, 0x76, 0x31, 0x0, 0x2,
0x6b, 0x32, 0x2, 0x0, 0x2, 0x76, 0x32, 0x0,
0x0, 0x9, 0x5,
0x67, 0x72, 0x65, 0x00, 0x40, 0xeb, 0x91, 0x00,
0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x02, 0x6b,
0x31, 0x02, 0x00, 0x02, 0x76, 0x31, 0x00, 0x02,
0x6b, 0x32, 0x02, 0x00, 0x02, 0x76, 0x32, 0x00,
0x00, 0x09, 0x05,
},
},
{
@ -100,9 +218,9 @@ var readWriterCases = []struct {
},
},
[]byte{
0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x13, 0x12,
0x0, 0x5, 0x44, 0x9b, 0x0, 0x40, 0x6d, 0x40,
0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0x0, 0x6,
0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x13, 0x12,
0x00, 0x05, 0x44, 0x9b, 0x00, 0x40, 0x6d, 0x40,
0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x06,
0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x05,
},
},
@ -112,8 +230,8 @@ var readWriterCases = []struct {
Value: 10000,
},
[]byte{
0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x4, 0x1,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x27, 0x10,
0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x01,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x27, 0x10,
},
},
{
@ -122,8 +240,8 @@ var readWriterCases = []struct {
Value: 10000,
},
[]byte{
0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x4, 0x1,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x27, 0x10,
0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x01,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x27, 0x10,
},
},
{
@ -132,8 +250,8 @@ var readWriterCases = []struct {
Value: 10000,
},
[]byte{
0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x4, 0x1,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x27, 0x10,
0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x01,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x27, 0x10,
},
},
{
@ -142,8 +260,8 @@ var readWriterCases = []struct {
ServerTime: 569834435,
},
[]byte{
0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x6, 0x4,
0x0, 0x0, 0x0, 0x0, 0x0, 0x6, 0x21, 0xf6,
0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x04,
0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x21, 0xf6,
0xfb, 0xc3,
},
},
@ -153,8 +271,8 @@ var readWriterCases = []struct {
ServerTime: 569834435,
},
[]byte{
0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x6, 0x4,
0x0, 0x0, 0x0, 0x0, 0x0, 0x7, 0x21, 0xf6,
0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x04,
0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x21, 0xf6,
0xfb, 0xc3,
},
},
@ -165,9 +283,9 @@ var readWriterCases = []struct {
BufferLength: 235345,
},
[]byte{
0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0xa, 0x4,
0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0,
0x8a, 0xce, 0x0, 0x3, 0x97, 0x51,
0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x04,
0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00,
0x8a, 0xce, 0x00, 0x03, 0x97, 0x51,
},
},
{
@ -176,8 +294,8 @@ var readWriterCases = []struct {
StreamID: 35534,
},
[]byte{
0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x6, 0x4,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x04,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x8a, 0xce,
},
},
@ -187,8 +305,8 @@ var readWriterCases = []struct {
StreamID: 35534,
},
[]byte{
0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x6, 0x4,
0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0,
0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x04,
0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00,
0x8a, 0xce,
},
},
@ -198,8 +316,8 @@ var readWriterCases = []struct {
StreamID: 35534,
},
[]byte{
0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x6, 0x4,
0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0,
0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x04,
0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00,
0x8a, 0xce,
},
},
@ -209,8 +327,8 @@ var readWriterCases = []struct {
StreamID: 35534,
},
[]byte{
0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x6, 0x4,
0x0, 0x0, 0x0, 0x0, 0x0, 0x4, 0x0, 0x0,
0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x04,
0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00,
0x8a, 0xce,
},
},
@ -233,22 +351,211 @@ var readWriterCases = []struct {
},
},
{
"extended sequence start",
&ExtendedSequenceStart{
ChunkStreamID: 4,
"video ex sequence start av1",
&VideoExSequenceStart{
ChunkStreamID: 6,
MessageStreamID: 0x1000000,
FourCC: FourCCHEVC,
Config: []byte{0x01, 0x02, 0x03},
FourCC: FourCCAV1,
AV1Header: &mp4.Av1C{
Marker: 0x1,
Version: 0x1,
SeqLevelIdx0: 0x8,
ChromaSubsamplingX: 0x1,
ChromaSubsamplingY: 0x1,
ConfigOBUs: []uint8{0xa, 0xb, 0x0, 0x0, 0x0, 0x42, 0xab, 0xbf, 0xc3, 0x70, 0xb, 0xe0, 0x1},
},
},
[]byte{
0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x09,
0x01, 0x00, 0x00, 0x00, 0x80, 0x68, 0x76, 0x63,
0x31, 0x01, 0x02, 0x03,
0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0x09,
0x01, 0x00, 0x00, 0x00, 0x80, 0x61, 0x76, 0x30,
0x31, 0x81, 0x08, 0x0c, 0x00, 0x0a, 0x0b, 0x00,
0x00, 0x00, 0x42, 0xab, 0xbf, 0xc3, 0x70, 0x0b,
0xe0, 0x01,
},
},
{
"extended coded frames",
&ExtendedCodedFrames{
"video ex sequence start hevc",
&VideoExSequenceStart{
ChunkStreamID: 4,
MessageStreamID: 0x1000000,
FourCC: FourCCHEVC,
HEVCHeader: &mp4.HvcC{
ConfigurationVersion: 0x1,
GeneralProfileIdc: 0x1,
GeneralProfileCompatibility: [32]bool{
false, true, false, false, false, false, false, false, false,
false, false, false, false, false, false, false, false, false,
false, false, false, false, false, false, false, false, false,
false, false, false, false, false,
},
GeneralConstraintIndicator: [6]uint8{0x90, 0x0, 0x0, 0x0, 0x0, 0x0},
GeneralLevelIdc: 0x7b,
Reserved1: 0xf,
Reserved2: 0x3f,
Reserved3: 0x3f,
ChromaFormatIdc: 0x1,
Reserved4: 0x1f,
Reserved5: 0x1f,
TemporalIdNested: 0x3,
LengthSizeMinusOne: 0x3,
NumOfNaluArrays: 0x3,
NaluArrays: []mp4.HEVCNaluArray{
{
Completeness: true,
NaluType: 0x20,
NumNalus: 0x1,
Nalus: []mp4.HEVCNalu{{
Length: 0x17,
NALUnit: []uint8{
0x40, 0x01, 0x0c, 0x01, 0xff, 0xff, 0x01, 0x40,
0x00, 0x00, 0x03, 0x00, 0x90, 0x00, 0x00, 0x03,
0x00, 0x00, 0x03, 0x00, 0x7b, 0xac, 0x09,
},
}},
},
{
Completeness: true,
NaluType: 0x21,
NumNalus: 0x1,
Nalus: []mp4.HEVCNalu{{
Length: 0x3d,
NALUnit: []uint8{
0x42, 0x01, 0x01, 0x01, 0x40, 0x00, 0x00, 0x03,
0x00, 0x90, 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, 0x02, 0x62, 0x5a, 0x00,
0x00, 0x13, 0x12, 0xd0, 0x20,
},
}},
},
{
Completeness: true,
NaluType: 0x22,
NumNalus: 0x1,
Nalus: []mp4.HEVCNalu{{
Length: 0x7,
NALUnit: []uint8{
0x44, 0x01, 0xc0, 0xf7, 0xc0, 0xcc, 0x90,
},
}},
},
},
},
},
[]byte{
0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x86, 0x09,
0x01, 0x00, 0x00, 0x00, 0x80, 0x68, 0x76, 0x63,
0x31, 0x01, 0x01, 0x40, 0x00, 0x00, 0x00, 0x90,
0x00, 0x00, 0x00, 0x00, 0x00, 0x7b, 0xf0, 0x00,
0xfc, 0xfd, 0xf8, 0xf8, 0x00, 0x00, 0x0f, 0x03,
0xa0, 0x00, 0x01, 0x00, 0x17, 0x40, 0x01, 0x0c,
0x01, 0xff, 0xff, 0x01, 0x40, 0x00, 0x00, 0x03,
0x00, 0x90, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03,
0x00, 0x7b, 0xac, 0x09, 0xa1, 0x00, 0x01, 0x00,
0x3d, 0x42, 0x01, 0x01, 0x01, 0x40, 0x00, 0x00,
0x03, 0x00, 0x90, 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, 0x02, 0x62, 0x5a,
0x00, 0x00, 0x13, 0x12, 0xd0, 0x20, 0xa2, 0x00,
0x01, 0x00, 0x07, 0x44, 0xc4, 0x01, 0xc0, 0xf7,
0xc0, 0xcc, 0x90,
},
},
{
"video ex sequence start vp9",
&VideoExSequenceStart{
ChunkStreamID: 6,
MessageStreamID: 0x1000000,
FourCC: FourCCVP9,
VP9Header: &mp4.VpcC{
FullBox: mp4.FullBox{Version: 0x1},
Level: 0x28,
BitDepth: 0x8,
ChromaSubsampling: 0x1,
ColourPrimaries: 0x2,
TransferCharacteristics: 0x2,
MatrixCoefficients: 0x2,
CodecInitializationData: []uint8{},
},
},
[]byte{
0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x11, 0x09,
0x01, 0x00, 0x00, 0x00, 0x80, 0x76, 0x70, 0x30,
0x39, 0x01, 0x00, 0x00, 0x00, 0x00, 0x28, 0x82,
0x02, 0x02, 0x02, 0x00, 0x00,
},
},
{
"video ex sequence start h264",
&VideoExSequenceStart{
ChunkStreamID: 0x4,
MessageStreamID: 0x1000000,
FourCC: FourCCAVC,
AVCHeader: &mp4.AVCDecoderConfiguration{
AnyTypeBox: mp4.AnyTypeBox{Type: mp4.BoxType{0x61, 0x76, 0x63, 0x43}},
ConfigurationVersion: 0x1,
Profile: 0x4d,
ProfileCompatibility: 0x40,
Level: 0x1e,
Reserved: 0x3f,
LengthSizeMinusOne: 0x3,
Reserved2: 0x7,
NumOfSequenceParameterSets: 0x1,
SequenceParameterSets: []mp4.AVCParameterSet{
{
Length: 0x23,
NALUnit: []uint8{
0x67, 0x4d, 0x40, 0x1e, 0x96, 0x56, 0x05, 0x01,
0x7f, 0xcb, 0x80, 0xb5, 0x01, 0x01, 0x01, 0x40,
0x00, 0x00, 0xfa, 0x00, 0x00, 0x3a, 0x98, 0x38,
0x00, 0x00, 0x7a, 0x10, 0x00, 0x0f, 0x42, 0x5b,
0xbc, 0xb8, 0x28,
},
},
},
NumOfPictureParameterSets: 0x1,
PictureParameterSets: []mp4.AVCParameterSet{
{
Length: 0x4,
NALUnit: []uint8{0x68, 0xee, 0x3c, 0x80},
},
},
},
},
[]byte{
0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x37, 0x09,
0x01, 0x00, 0x00, 0x00, 0x80, 0x61, 0x76, 0x63,
0x31, 0x01, 0x4d, 0x40, 0x1e, 0xff, 0xe1, 0x00,
0x23, 0x67, 0x4d, 0x40, 0x1e, 0x96, 0x56, 0x05,
0x01, 0x7f, 0xcb, 0x80, 0xb5, 0x01, 0x01, 0x01,
0x40, 0x00, 0x00, 0xfa, 0x00, 0x00, 0x3a, 0x98,
0x38, 0x00, 0x00, 0x7a, 0x10, 0x00, 0x0f, 0x42,
0x5b, 0xbc, 0xb8, 0x28, 0x01, 0x00, 0x04, 0x68,
0xee, 0x3c, 0x80,
},
},
{
"video ex sequence end",
&VideoExSequenceEnd{
ChunkStreamID: 4,
MessageStreamID: 0x1000000,
FourCC: FourCCAV1,
},
[]byte{
0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x09,
0x01, 0x00, 0x00, 0x00, 0x82, 0x61, 0x76, 0x30,
0x31,
},
},
{
"video ex coded frames",
&VideoExCodedFrames{
ChunkStreamID: 4,
DTS: 15100 * time.Millisecond,
MessageStreamID: 0x1000000,
@ -263,8 +570,8 @@ var readWriterCases = []struct {
},
},
{
"extended frames x",
&ExtendedFramesX{
"video ex frames x",
&VideoExFramesX{
ChunkStreamID: 4,
DTS: 15100 * time.Millisecond,
MessageStreamID: 0x1000000,
@ -278,13 +585,13 @@ var readWriterCases = []struct {
},
},
{
"extended metadata",
&ExtendedMetadata{
"video ex metadata",
&VideoExMetadata{
ChunkStreamID: 0x6,
DTS: 0,
MessageStreamID: 0x1000000,
FourCC: 0x68766331,
Payload: []interface{}{"colorInfo", amf0.Object{amf0.ObjectEntry{Key: "colorConfig", Value: amf0.Object{}}}},
Payload: []interface{}{"colorInfo", amf0.Object{{Key: "colorConfig", Value: amf0.Object{}}}},
},
[]byte{
0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x26, 0x09,
@ -296,6 +603,34 @@ var readWriterCases = []struct {
0x00, 0x09,
},
},
{
"video ex multitrack",
&VideoExMultitrack{
MultitrackType: VideoExMultitrackTypeOneTrack,
TrackID: 1,
Wrapped: &VideoExSequenceStart{
ChunkStreamID: 6,
MessageStreamID: 0x1000000,
FourCC: FourCCVP9,
VP9Header: &mp4.VpcC{
FullBox: mp4.FullBox{Version: 0x1},
Level: 0x28,
BitDepth: 0x8,
ChromaSubsampling: 0x1,
ColourPrimaries: 0x2,
TransferCharacteristics: 0x2,
MatrixCoefficients: 0x2,
CodecInitializationData: []uint8{},
},
},
},
[]byte{
0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x13, 0x09,
0x01, 0x00, 0x00, 0x00, 0x86, 0x00, 0x76, 0x70,
0x30, 0x39, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00,
0x28, 0x82, 0x02, 0x02, 0x02, 0x00, 0x00,
},
},
}
func TestReader(t *testing.T) {
@ -311,11 +646,9 @@ func TestReader(t *testing.T) {
}
func FuzzReader(f *testing.F) {
f.Add([]byte{
0x04, 0x00, 0x3a, 0xfc, 0x00, 0x00, 0x08, 0x09,
0x01, 0x00, 0x00, 0x00, 0x88, 0x68, 0x76, 0x63,
0x31, 0x01, 0x02, 0x03,
})
for _, ca := range readWriterCases {
f.Add(ca.enc)
}
f.Fuzz(func(_ *testing.T, b []byte) {
bcr := bytecounter.NewReader(bytes.NewReader(b))

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -26,114 +26,177 @@ func durationToTimestamp(d time.Duration, clockRate int) int64 {
// ToStream maps a RTMP stream to a MediaMTX stream.
func ToStream(r *Reader, stream **stream.Stream) ([]*description.Media, error) {
videoFormat, audioFormat := r.Tracks()
var medias []*description.Media
if videoFormat != nil {
medi := &description.Media{
Type: description.MediaTypeVideo,
Formats: []format.Format{videoFormat},
}
medias = append(medias, medi)
for _, track := range r.Tracks() {
ctrack := track
switch videoFormat.(type) {
switch ttrack := track.(type) {
case *format.AV1:
r.OnDataAV1(func(pts time.Duration, tu [][]byte) {
(*stream).WriteUnit(medi, videoFormat, &unit.AV1{
medi := &description.Media{
Type: description.MediaTypeVideo,
Formats: []format.Format{ctrack},
}
medias = append(medias, medi)
r.OnDataAV1(ttrack, func(pts time.Duration, tu [][]byte) {
(*stream).WriteUnit(medi, ctrack, &unit.AV1{
Base: unit.Base{
NTP: time.Now(),
PTS: durationToTimestamp(pts, videoFormat.ClockRate()),
PTS: durationToTimestamp(pts, ctrack.ClockRate()),
},
TU: tu,
})
})
case *format.VP9:
r.OnDataVP9(func(pts time.Duration, frame []byte) {
(*stream).WriteUnit(medi, videoFormat, &unit.VP9{
medi := &description.Media{
Type: description.MediaTypeVideo,
Formats: []format.Format{ctrack},
}
medias = append(medias, medi)
r.OnDataVP9(ttrack, func(pts time.Duration, frame []byte) {
(*stream).WriteUnit(medi, ctrack, &unit.VP9{
Base: unit.Base{
NTP: time.Now(),
PTS: durationToTimestamp(pts, videoFormat.ClockRate()),
PTS: durationToTimestamp(pts, ctrack.ClockRate()),
},
Frame: frame,
})
})
case *format.H265:
r.OnDataH265(func(pts time.Duration, au [][]byte) {
(*stream).WriteUnit(medi, videoFormat, &unit.H265{
medi := &description.Media{
Type: description.MediaTypeVideo,
Formats: []format.Format{ctrack},
}
medias = append(medias, medi)
r.OnDataH265(ttrack, func(pts time.Duration, au [][]byte) {
(*stream).WriteUnit(medi, ctrack, &unit.H265{
Base: unit.Base{
NTP: time.Now(),
PTS: durationToTimestamp(pts, videoFormat.ClockRate()),
PTS: durationToTimestamp(pts, ctrack.ClockRate()),
},
AU: au,
})
})
case *format.H264:
r.OnDataH264(func(pts time.Duration, au [][]byte) {
(*stream).WriteUnit(medi, videoFormat, &unit.H264{
medi := &description.Media{
Type: description.MediaTypeVideo,
Formats: []format.Format{ctrack},
}
medias = append(medias, medi)
r.OnDataH264(ttrack, func(pts time.Duration, au [][]byte) {
(*stream).WriteUnit(medi, ctrack, &unit.H264{
Base: unit.Base{
NTP: time.Now(),
PTS: durationToTimestamp(pts, videoFormat.ClockRate()),
PTS: durationToTimestamp(pts, ctrack.ClockRate()),
},
AU: au,
})
})
default:
panic("should not happen")
}
}
case *format.Opus:
medi := &description.Media{
Type: description.MediaTypeAudio,
Formats: []format.Format{ctrack},
}
medias = append(medias, medi)
if audioFormat != nil {
medi := &description.Media{
Type: description.MediaTypeAudio,
Formats: []format.Format{audioFormat},
}
medias = append(medias, medi)
switch audioFormat.(type) {
case *format.MPEG4Audio:
r.OnDataMPEG4Audio(func(pts time.Duration, au []byte) {
(*stream).WriteUnit(medi, audioFormat, &unit.MPEG4Audio{
r.OnDataOpus(ttrack, func(pts time.Duration, packet []byte) {
(*stream).WriteUnit(medi, ctrack, &unit.Opus{
Base: unit.Base{
NTP: time.Now(),
PTS: durationToTimestamp(pts, audioFormat.ClockRate()),
PTS: durationToTimestamp(pts, ctrack.ClockRate()),
},
Packets: [][]byte{packet},
})
})
case *format.MPEG4Audio:
medi := &description.Media{
Type: description.MediaTypeAudio,
Formats: []format.Format{ctrack},
}
medias = append(medias, medi)
r.OnDataMPEG4Audio(ttrack, func(pts time.Duration, au []byte) {
(*stream).WriteUnit(medi, ctrack, &unit.MPEG4Audio{
Base: unit.Base{
NTP: time.Now(),
PTS: durationToTimestamp(pts, ctrack.ClockRate()),
},
AUs: [][]byte{au},
})
})
case *format.MPEG1Audio:
r.OnDataMPEG1Audio(func(pts time.Duration, frame []byte) {
(*stream).WriteUnit(medi, audioFormat, &unit.MPEG1Audio{
medi := &description.Media{
Type: description.MediaTypeAudio,
Formats: []format.Format{ctrack},
}
medias = append(medias, medi)
r.OnDataMPEG1Audio(ttrack, func(pts time.Duration, frame []byte) {
(*stream).WriteUnit(medi, ctrack, &unit.MPEG1Audio{
Base: unit.Base{
NTP: time.Now(),
PTS: durationToTimestamp(pts, audioFormat.ClockRate()),
PTS: durationToTimestamp(pts, ctrack.ClockRate()),
},
Frames: [][]byte{frame},
})
})
case *format.AC3:
medi := &description.Media{
Type: description.MediaTypeAudio,
Formats: []format.Format{ctrack},
}
medias = append(medias, medi)
r.OnDataAC3(ttrack, func(pts time.Duration, frame []byte) {
(*stream).WriteUnit(medi, ctrack, &unit.AC3{
Base: unit.Base{
NTP: time.Now(),
PTS: durationToTimestamp(pts, ctrack.ClockRate()),
},
Frames: [][]byte{frame},
})
})
case *format.G711:
r.OnDataG711(func(pts time.Duration, samples []byte) {
(*stream).WriteUnit(medi, audioFormat, &unit.G711{
medi := &description.Media{
Type: description.MediaTypeAudio,
Formats: []format.Format{ctrack},
}
medias = append(medias, medi)
r.OnDataG711(ttrack, func(pts time.Duration, samples []byte) {
(*stream).WriteUnit(medi, ctrack, &unit.G711{
Base: unit.Base{
NTP: time.Now(),
PTS: durationToTimestamp(pts, audioFormat.ClockRate()),
PTS: durationToTimestamp(pts, ctrack.ClockRate()),
},
Samples: samples,
})
})
case *format.LPCM:
r.OnDataLPCM(func(pts time.Duration, samples []byte) {
(*stream).WriteUnit(medi, audioFormat, &unit.LPCM{
medi := &description.Media{
Type: description.MediaTypeAudio,
Formats: []format.Format{ctrack},
}
medias = append(medias, medi)
r.OnDataLPCM(ttrack, func(pts time.Duration, samples []byte) {
(*stream).WriteUnit(medi, ctrack, &unit.LPCM{
Base: unit.Base{
NTP: time.Now(),
PTS: durationToTimestamp(pts, audioFormat.ClockRate()),
PTS: durationToTimestamp(pts, ctrack.ClockRate()),
},
Samples: samples,
})

View File

@ -166,7 +166,7 @@ func (w *Writer) writeTracks(videoTrack format.Format, audioTrack format.Format)
}
// WriteH264 writes H264 data.
func (w *Writer) WriteH264(pts time.Duration, dts time.Duration, idrPresent bool, au [][]byte) error {
func (w *Writer) WriteH264(pts time.Duration, dts time.Duration, au [][]byte) error {
avcc, err := h264.AVCCMarshal(au)
if err != nil {
return err
@ -176,7 +176,7 @@ func (w *Writer) WriteH264(pts time.Duration, dts time.Duration, idrPresent bool
ChunkStreamID: message.VideoChunkStreamID,
MessageStreamID: 0x1000000,
Codec: message.CodecH264,
IsKeyFrame: idrPresent,
IsKeyFrame: h264.IDRPresent(au),
Type: message.VideoTypeAU,
Payload: avcc,
DTS: dts,

View File

@ -9,6 +9,7 @@ import (
"time"
"github.com/bluenviron/gortsplib/v4/pkg/description"
"github.com/bluenviron/gortsplib/v4/pkg/format"
"github.com/bluenviron/mediamtx/internal/auth"
"github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/defs"
@ -143,6 +144,12 @@ func TestServerPublish(t *testing.T) {
w, err := rtmp.NewWriter(conn, test.FormatH264, test.FormatMPEG4Audio)
require.NoError(t, err)
err = w.WriteH264(
2*time.Second, 2*time.Second, [][]byte{
{5, 2, 3, 4},
})
require.NoError(t, err)
<-path.streamCreated
recv := make(chan struct{})
@ -166,9 +173,10 @@ func TestServerPublish(t *testing.T) {
path.stream.StartReader(reader)
defer path.stream.RemoveReader(reader)
err = w.WriteH264(0, 0, true, [][]byte{
{5, 2, 3, 4},
})
err = w.WriteH264(
3*time.Second, 3*time.Second, [][]byte{
{5, 2, 3, 4},
})
require.NoError(t, err)
<-recv
@ -241,27 +249,49 @@ func TestServerRead(t *testing.T) {
require.NoError(t, err)
defer nconn.Close()
go func() {
stream.WaitRunningReader()
stream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.H264{
Base: unit.Base{
NTP: time.Time{},
},
AU: [][]byte{
{5, 2, 3, 4}, // IDR
},
})
stream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.H264{
Base: unit.Base{
NTP: time.Time{},
PTS: 2 * 90000,
},
AU: [][]byte{
{5, 2, 3, 4}, // IDR
},
})
stream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.H264{
Base: unit.Base{
NTP: time.Time{},
PTS: 3 * 90000,
},
AU: [][]byte{
{5, 2, 3, 4}, // IDR
},
})
}()
conn, err := rtmp.NewClientConn(nconn, u, false)
require.NoError(t, err)
r, err := rtmp.NewReader(conn)
require.NoError(t, err)
videoTrack, _ := r.Tracks()
require.Equal(t, test.FormatH264, videoTrack)
tracks := r.Tracks()
require.Equal(t, []format.Format{test.FormatH264}, tracks)
stream.WaitRunningReader()
stream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.H264{
Base: unit.Base{
NTP: time.Time{},
},
AU: [][]byte{
{5, 2, 3, 4}, // IDR
},
})
r.OnDataH264(func(_ time.Duration, au [][]byte) {
r.OnDataH264(tracks[0].(*format.H264), func(_ time.Duration, au [][]byte) {
require.Equal(t, [][]byte{
test.FormatH264.SPS,
test.FormatH264.PPS,

View File

@ -54,7 +54,10 @@ func TestSource(t *testing.T) {
w, err := rtmp.NewWriter(conn, test.FormatH264, test.FormatMPEG4Audio)
require.NoError(t, err)
err = w.WriteH264(0, 0, true, [][]byte{{0x05, 0x02, 0x03, 0x04}})
err = w.WriteH264(2*time.Second, 2*time.Second, [][]byte{{5, 2, 3, 4}})
require.NoError(t, err)
err = w.WriteH264(3*time.Second, 3*time.Second, [][]byte{{5, 2, 3, 4}})
require.NoError(t, err)
}()