mirror of
https://github.com/bluenviron/mediamtx
synced 2025-01-03 05:22:30 +00:00
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:
parent
df3362aef8
commit
04d4e1668f
11
README.md
11
README.md
@ -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|
|
||||
|
@ -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":
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -86,7 +86,6 @@ func setupVideo(
|
||||
return (*w).WriteH264(
|
||||
timestampToDuration(tunit.PTS, videoFormatH264.ClockRate()),
|
||||
timestampToDuration(dts, videoFormatH264.ClockRate()),
|
||||
idrPresent,
|
||||
tunit.AU)
|
||||
})
|
||||
|
||||
|
@ -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")
|
||||
}
|
@ -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")
|
||||
}
|
@ -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.
|
||||
|
61
internal/protocols/rtmp/message/msg_audio_ex_coded_frames.go
Normal file
61
internal/protocols/rtmp/message/msg_audio_ex_coded_frames.go
Normal 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
|
||||
}
|
@ -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
|
||||
}
|
95
internal/protocols/rtmp/message/msg_audio_ex_multitrack.go
Normal file
95
internal/protocols/rtmp/message/msg_audio_ex_multitrack.go
Normal 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
|
||||
}
|
49
internal/protocols/rtmp/message/msg_audio_ex_sequence_end.go
Normal file
49
internal/protocols/rtmp/message/msg_audio_ex_sequence_end.go
Normal 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
|
||||
}
|
@ -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
|
||||
}
|
@ -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)
|
@ -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)
|
@ -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)
|
95
internal/protocols/rtmp/message/msg_video_ex_multitrack.go
Normal file
95
internal/protocols/rtmp/message/msg_video_ex_multitrack.go
Normal 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
|
||||
}
|
@ -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,
|
121
internal/protocols/rtmp/message/msg_video_ex_sequence_start.go
Normal file
121
internal/protocols/rtmp/message/msg_video_ex_sequence_start.go
Normal 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
|
||||
}
|
76
internal/protocols/rtmp/message/opus_id_header.go
Normal file
76
internal/protocols/rtmp/message/opus_id_header.go
Normal 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
|
||||
}
|
@ -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
|
||||
|
@ -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
@ -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,
|
||||
})
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
}()
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user