From e8124e2f56bbe8acf3dccf82be03327f3e854672 Mon Sep 17 00:00:00 2001 From: Alessandro Ros Date: Thu, 4 May 2023 20:37:25 +0200 Subject: [PATCH] support publishing H265 and AV1 tracks with Enhanced RTMP (#1393) (#1446) (#1621) (#1756) --- README.md | 32 +- go.mod | 6 +- go.sum | 8 +- internal/core/rtmp_conn.go | 100 +- internal/core/rtmp_server_test.go | 12 +- internal/core/rtmp_source.go | 9 +- internal/core/rtmp_source_test.go | 6 +- internal/core/webrtc_conn.go | 3 +- internal/formatprocessor/av1.go | 133 +++ internal/formatprocessor/h264.go | 2 +- internal/formatprocessor/h265.go | 2 +- internal/formatprocessor/processor.go | 3 + internal/rtmp/chunk/chunk0.go | 6 +- internal/rtmp/chunk/chunk0_test.go | 2 +- internal/rtmp/chunk/chunk1.go | 6 +- internal/rtmp/chunk/chunk1_test.go | 2 +- internal/rtmp/chunk/messagetype.go | 24 - internal/rtmp/conn.go | 476 +-------- internal/rtmp/conn_test.go | 992 +----------------- .../{msg_acknowledge.go => acknowledge.go} | 11 +- .../rtmp/message/{msg_audio.go => audio.go} | 31 +- .../{msg_command_amf0.go => command_amf0.go} | 11 +- .../{msg_data_amf0.go => data_amf0.go} | 11 +- .../rtmp/message/extended_coded_frames.go | 71 ++ internal/rtmp/message/extended_frames_x.go | 44 + internal/rtmp/message/extended_metadata.go | 24 + .../extended_mpeg2ts_sequence_start.go | 24 + .../rtmp/message/extended_sequence_end.go | 28 + .../rtmp/message/extended_sequence_start.go | 26 + internal/rtmp/message/message.go | 60 ++ internal/rtmp/message/msg_usercontrol.go | 12 - internal/rtmp/message/reader.go | 105 +- internal/rtmp/message/reader_test.go | 73 +- internal/rtmp/message/readwriter.go | 8 +- internal/rtmp/message/readwriter_test.go | 6 +- ...{msg_setchunksize.go => set_chunk_size.go} | 13 +- ...peerbandwidth.go => set_peer_bandwidth.go} | 13 +- ...indowacksize.go => set_window_ack_size.go} | 13 +- ...equest.go => user_control_ping_request.go} | 11 +- ...ponse.go => user_control_ping_response.go} | 11 +- ...h.go => user_control_set_buffer_length.go} | 11 +- ...mbegin.go => user_control_stream_begin.go} | 11 +- ...treamdry.go => user_control_stream_dry.go} | 11 +- ...treameof.go => user_control_stream_eof.go} | 11 +- ....go => user_control_stream_is_recorded.go} | 11 +- .../rtmp/message/{msg_video.go => video.go} | 36 +- internal/rtmp/message/writer.go | 4 +- internal/rtmp/rawmessage/message.go | 4 +- internal/rtmp/rawmessage/reader.go | 2 +- internal/rtmp/rawmessage/reader_test.go | 30 +- internal/rtmp/rawmessage/writer.go | 2 +- internal/rtmp/rawmessage/writer_test.go | 4 +- internal/rtmp/tracks/boxes_av1.go | 37 + internal/rtmp/tracks/read.go | 396 +++++++ internal/rtmp/tracks/read_test.go | 484 +++++++++ internal/rtmp/tracks/write.go | 107 ++ internal/rtmp/tracks/write_test.go | 96 ++ 57 files changed, 1971 insertions(+), 1716 deletions(-) create mode 100644 internal/formatprocessor/av1.go delete mode 100644 internal/rtmp/chunk/messagetype.go rename internal/rtmp/message/{msg_acknowledge.go => acknowledge.go} (70%) rename internal/rtmp/message/{msg_audio.go => audio.go} (68%) rename internal/rtmp/message/{msg_command_amf0.go => command_amf0.go} (77%) rename internal/rtmp/message/{msg_data_amf0.go => data_amf0.go} (69%) create mode 100644 internal/rtmp/message/extended_coded_frames.go create mode 100644 internal/rtmp/message/extended_frames_x.go create mode 100644 internal/rtmp/message/extended_metadata.go create mode 100644 internal/rtmp/message/extended_mpeg2ts_sequence_start.go create mode 100644 internal/rtmp/message/extended_sequence_end.go create mode 100644 internal/rtmp/message/extended_sequence_start.go delete mode 100644 internal/rtmp/message/msg_usercontrol.go rename internal/rtmp/message/{msg_setchunksize.go => set_chunk_size.go} (65%) rename internal/rtmp/message/{msg_setpeerbandwidth.go => set_peer_bandwidth.go} (65%) rename internal/rtmp/message/{msg_setwindowacksize.go => set_window_ack_size.go} (63%) rename internal/rtmp/message/{msg_usercontrol_pingrequest.go => user_control_ping_request.go} (70%) rename internal/rtmp/message/{msg_usercontrol_pingresponse.go => user_control_ping_response.go} (70%) rename internal/rtmp/message/{msg_usercontrol_setbufferlength.go => user_control_set_buffer_length.go} (75%) rename internal/rtmp/message/{msg_usercontrol_streambegin.go => user_control_stream_begin.go} (70%) rename internal/rtmp/message/{msg_usercontrol_streamdry.go => user_control_stream_dry.go} (70%) rename internal/rtmp/message/{msg_usercontrol_streameof.go => user_control_stream_eof.go} (70%) rename internal/rtmp/message/{msg_usercontrol_streamisrecorded.go => user_control_stream_is_recorded.go} (69%) rename internal/rtmp/message/{msg_video.go => video.go} (63%) create mode 100644 internal/rtmp/tracks/boxes_av1.go create mode 100644 internal/rtmp/tracks/read.go create mode 100644 internal/rtmp/tracks/read_test.go create mode 100644 internal/rtmp/tracks/write.go create mode 100644 internal/rtmp/tracks/write_test.go diff --git a/README.md b/README.md index 4644ec43..f5ba25ec 100644 --- a/README.md +++ b/README.md @@ -8,24 +8,24 @@ Live streams can be published to the server with: -|protocol|variants|codecs| -|--------|--------|------| -|RTSP clients (FFmpeg, GStreamer, etc)|UDP, TCP, RTSPS|H264, H265, VP8, VP9, AV1, MPEG-2 Video, M-JPEG, MPEG-4 Video (H263, Xvid), MPEG-2 Audio (MP3), MPEG-4 Audio (AAC), Opus, G711, G722, LPCM and any RTP-compatible codec| -|RTSP servers and cameras|UDP, UDP-Multicast, TCP, RTSPS|H264, H265, VP8, VP9, AV1, MPEG-2 Video, M-JPEG, MPEG-4 Video (H263, Xvid), MPEG-2 Audio (MP3), MPEG-4 Audio (AAC), Opus, G711, G722, LPCM and any RTP-compatible codec| -|RTMP clients (OBS Studio)|RTMP, RTMPS|H264, H265, MPEG-2 Audio (MP3), MPEG-4 Audio (AAC)| -|RTMP servers and cameras|RTMP, RTMPS|H264, MPEG-2 Audio (MP3), MPEG-4 Audio (AAC)| -|HLS servers and cameras|Low-Latency HLS, MP4-based HLS, legacy HLS|H264, H265, MPEG-4 Audio (AAC), Opus| -|UDP/MPEG-TS streams|Unicast, broadcast, multicast|H264, H265, MPEG-4 Audio (AAC), Opus| -|Raspberry Pi Cameras||H264| +|protocol|variants|video codecs|audio codecs| +|--------|--------|------------|------------| +|RTSP clients (FFmpeg, GStreamer)|UDP, TCP, RTSPS|AV1, VP9, VP8, H265, H264, MPEG-4 Video (H263, Xvid), MPEG-2 Video, M-JPEG and any RTP-compatible codec|Opus, MPEG-4 Audio (AAC), MPEG-2 Audio (MP3), G722, G711, LPCM and any RTP-compatible codec| +|RTSP servers and cameras|UDP, UDP-Multicast, TCP, RTSPS|AV1, VP9, VP8, H265, H264, MPEG-4 Video (H263, Xvid), MPEG-2 Video, M-JPEG and any RTP-compatible codec|Opus, MPEG-4 Audio (AAC), MPEG-2 Audio (MP3), G722, G711, LPCM and any RTP-compatible codec| +|RTMP clients (OBS Studio)|RTMP, RTMPS, Enhanced RTMP|AV1, H265, H264|MPEG-4 Audio (AAC), MPEG-2 Audio (MP3)| +|RTMP servers and cameras|RTMP, RTMPS, Enhanced RTMP|H264|MPEG-4 Audio (AAC), MPEG-2 Audio (MP3)| +|HLS servers and cameras|Low-Latency HLS, MP4-based HLS, legacy HLS|H265, H264|Opus, MPEG-4 Audio (AAC)| +|UDP/MPEG-TS streams|Unicast, broadcast, multicast|H265, H264|Opus, MPEG-4 Audio (AAC)| +|Raspberry Pi Cameras||H264|| And can be read from the server with: -|protocol|variants|codecs| -|--------|--------|------| -|RTSP|UDP, UDP-Multicast, TCP, RTSPS|H264, H265, VP8, VP9, AV1, MPEG-2 Video, M-JPEG, MPEG-4 Video (H263, Xvid), MPEG-2 Audio (MP3), MPEG-4 Audio (AAC), Opus, G711, G722, LPCM and any RTP-compatible codec| -|RTMP|RTMP, RTMPS|H264, MPEG-2 Audio (MP3), MPEG-4 Audio (AAC)| -|HLS|Low-Latency HLS, MP4-based HLS, legacy HLS|H264, H265, MPEG-4 Audio (AAC), Opus| -|WebRTC||H264, VP8, VP9, Opus, G711, G722| +|protocol|variants|video codecs|audio codecs| +|--------|--------|------------|------------| +|RTSP|UDP, UDP-Multicast, TCP, RTSPS|AV1, VP9, VP8, H265, H264, MPEG-4 Video (H263, Xvid), MPEG-2 Video, M-JPEG and any RTP-compatible codec|Opus, MPEG-4 Audio (AAC), MPEG-2 Audio (MP3), G722, G711, LPCM and any RTP-compatible codec| +|RTMP|RTMP, RTMPS, Enhanced RTMP|H264|MPEG-4 Audio (AAC), MPEG-2 Audio (MP3)| +|HLS|Low-Latency HLS, MP4-based HLS, legacy HLS|H265, H264|Opus, MPEG-4 Audio (AAC)| +|WebRTC||VP9, VP8, H264|Opus, G722, G711| Features: @@ -1201,7 +1201,9 @@ For more advanced options, you can create and serve a custom web page by startin * [RTSP/RTP/RTCP standards](https://github.com/bluenviron/gortsplib#standards) * [HLS standards](https://github.com/bluenviron/gohlslib#standards) +* [Codec standards](https://github.com/bluenviron/mediacommon#standards) * [RTMP specification](https://rtmp.veriskope.com/pdf/rtmp_specification_1.0.pdf) +* [Enhanced RTMP](https://raw.githubusercontent.com/veovera/enhanced-rtmp/main/enhanced-rtmp-v1.pdf) * [Golang project layout](https://github.com/golang-standards/project-layout) ## Links diff --git a/go.mod b/go.mod index 86f23472..97a4e300 100644 --- a/go.mod +++ b/go.mod @@ -4,11 +4,12 @@ go 1.20 require ( code.cloudfoundry.org/bytefmt v0.0.0 + github.com/abema/go-mp4 v0.10.1 github.com/alecthomas/kong v0.7.1 github.com/asticode/go-astits v1.11.0 github.com/bluenviron/gohlslib v0.2.3 - github.com/bluenviron/gortsplib/v3 v3.4.0 - github.com/bluenviron/mediacommon v0.4.2 + github.com/bluenviron/gortsplib/v3 v3.5.0 + github.com/bluenviron/mediacommon v0.5.0 github.com/fsnotify/fsnotify v1.6.0 github.com/gin-gonic/gin v1.9.0 github.com/google/uuid v1.3.0 @@ -27,7 +28,6 @@ require ( ) require ( - github.com/abema/go-mp4 v0.10.1 // indirect github.com/aler9/writerseeker v0.0.0-20220601075008-6f0e685b9c82 // indirect github.com/asticode/go-astikit v0.30.0 // indirect github.com/bytedance/sonic v1.8.0 // indirect diff --git a/go.sum b/go.sum index dc5af0f1..7656cbce 100644 --- a/go.sum +++ b/go.sum @@ -14,10 +14,10 @@ github.com/asticode/go-astits v1.11.0 h1:GTHUXht0ZXAJXsVbsLIcyfHr1Bchi4QQwMARw2Z github.com/asticode/go-astits v1.11.0/go.mod h1:QSHmknZ51pf6KJdHKZHJTLlMegIrhega3LPWz3ND/iI= github.com/bluenviron/gohlslib v0.2.3 h1:vZmpjh2qWHaCvwwha04tgu8Kz9p4CuSBRLayD2yf89A= github.com/bluenviron/gohlslib v0.2.3/go.mod h1:loD97sTtBh/nBcw8yZJgXc71A6XQb0FsDWXFRkl7Yj4= -github.com/bluenviron/gortsplib/v3 v3.4.0 h1:N4ticlV5YqRFDNvU52CRJgBQ0hHnxerDLfsd5wf5GI0= -github.com/bluenviron/gortsplib/v3 v3.4.0/go.mod h1:Th3S/suqfnpV81y31YpE1hcOP9odMqvIjOB7RV1+2lU= -github.com/bluenviron/mediacommon v0.4.2 h1:rdghY3g70+fdviapO2hL6CHpOGeTd7KbH1aEZnMwh88= -github.com/bluenviron/mediacommon v0.4.2/go.mod h1:t0dqPsWUTchyvib0MhixIwXEgvDX4V9G+I0GzWLQRb8= +github.com/bluenviron/gortsplib/v3 v3.5.0 h1:8d6DYcwVhghObgBFOnoJwK6xf1ZiAQ8Vi7DRv6DGLdw= +github.com/bluenviron/gortsplib/v3 v3.5.0/go.mod h1:gc6Z8pBUMC9QBqYxcOY9eVxjDPOrmFcwVH61Xs3Gu2A= +github.com/bluenviron/mediacommon v0.5.0 h1:YsVFlEknaXWhZGfz+Y1QbuzXLMVSmHODc7OnRqZoITY= +github.com/bluenviron/mediacommon v0.5.0/go.mod h1:t0dqPsWUTchyvib0MhixIwXEgvDX4V9G+I0GzWLQRb8= github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= github.com/bytedance/sonic v1.8.0 h1:ea0Xadu+sHlu7x5O3gKhRpQ1IKiMrSiHttPF0ybECuA= github.com/bytedance/sonic v1.8.0/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= diff --git a/internal/core/rtmp_conn.go b/internal/core/rtmp_conn.go index fa2ce004..5d069cb6 100644 --- a/internal/core/rtmp_conn.go +++ b/internal/core/rtmp_conn.go @@ -13,6 +13,7 @@ import ( "github.com/bluenviron/gortsplib/v3/pkg/formats" "github.com/bluenviron/gortsplib/v3/pkg/media" "github.com/bluenviron/gortsplib/v3/pkg/ringbuffer" + "github.com/bluenviron/mediacommon/pkg/codecs/av1" "github.com/bluenviron/mediacommon/pkg/codecs/h264" "github.com/bluenviron/mediacommon/pkg/codecs/mpeg2audio" "github.com/bluenviron/mediacommon/pkg/codecs/mpeg4audio" @@ -46,10 +47,10 @@ func getRTMPWriteFunc(medi *media.Media, format formats.Format, stream *stream) switch format.(type) { case *formats.H264: return func(msg interface{}) error { - tmsg := msg.(*message.MsgVideo) + tmsg := msg.(*message.Video) switch tmsg.Type { - case message.MsgVideoTypeConfig: + case message.VideoTypeConfig: var conf h264conf.Conf err := conf.Unmarshal(tmsg.Payload) if err != nil { @@ -67,7 +68,7 @@ func getRTMPWriteFunc(medi *media.Media, format formats.Format, stream *stream) NTP: time.Now(), }) - case message.MsgVideoTypeAU: + case message.VideoTypeAU: au, err := h264.AVCCUnmarshal(tmsg.Payload) if err != nil { return fmt.Errorf("unable to decode AVCC: %v", err) @@ -85,25 +86,68 @@ func getRTMPWriteFunc(medi *media.Media, format formats.Format, stream *stream) case *formats.H265: return func(msg interface{}) error { - tmsg := msg.(*message.MsgVideo) + switch tmsg := msg.(type) { + case *message.Video: + au, err := h264.AVCCUnmarshal(tmsg.Payload) + if err != nil { + return fmt.Errorf("unable to decode AVCC: %v", err) + } - au, err := h264.AVCCUnmarshal(tmsg.Payload) - if err != nil { - return fmt.Errorf("unable to decode AVCC: %v", err) + stream.writeUnit(medi, format, &formatprocessor.UnitH265{ + PTS: tmsg.DTS + tmsg.PTSDelta, + AU: au, + NTP: time.Now(), + }) + + case *message.ExtendedFramesX: + au, err := h264.AVCCUnmarshal(tmsg.Payload) + if err != nil { + return fmt.Errorf("unable to decode AVCC: %v", err) + } + + stream.writeUnit(medi, format, &formatprocessor.UnitH265{ + PTS: tmsg.DTS, + AU: au, + NTP: time.Now(), + }) + + case *message.ExtendedCodedFrames: + au, err := h264.AVCCUnmarshal(tmsg.Payload) + if err != nil { + return fmt.Errorf("unable to decode AVCC: %v", err) + } + + stream.writeUnit(medi, format, &formatprocessor.UnitH265{ + PTS: tmsg.DTS + tmsg.PTSDelta, + AU: au, + NTP: time.Now(), + }) } - stream.writeUnit(medi, format, &formatprocessor.UnitH265{ - PTS: tmsg.DTS + tmsg.PTSDelta, - AU: au, - NTP: time.Now(), - }) + return nil + } + + case *formats.AV1: + return func(msg interface{}) error { + if tmsg, ok := msg.(*message.ExtendedCodedFrames); ok { + obus, err := av1.BitstreamUnmarshal(tmsg.Payload, true) + if err != nil { + return fmt.Errorf("unable to decode bitstream: %v", err) + } + + stream.writeUnit(medi, format, &formatprocessor.UnitAV1{ + PTS: tmsg.DTS, + OBUs: obus, + NTP: time.Now(), + }) + } return nil } case *formats.MPEG2Audio: return func(msg interface{}) error { - tmsg := msg.(*message.MsgAudio) + tmsg := msg.(*message.Audio) stream.writeUnit(medi, format, &formatprocessor.UnitMPEG2Audio{ PTS: tmsg.DTS, @@ -116,9 +160,9 @@ func getRTMPWriteFunc(medi *media.Media, format formats.Format, stream *stream) case *formats.MPEG4Audio: return func(msg interface{}) error { - tmsg := msg.(*message.MsgAudio) + tmsg := msg.(*message.Audio) - if tmsg.AACType == message.MsgAudioAACTypeAU { + if tmsg.AACType == message.AudioAACTypeAU { stream.writeUnit(medi, format, &formatprocessor.UnitMPEG4Audio{ PTS: tmsg.DTS, AUs: [][]byte{tmsg.Payload}, @@ -496,12 +540,12 @@ func (c *rtmpConn) findVideoFormat(stream *stream, ringBuffer *ringbuffer.RingBu } c.nconn.SetWriteDeadline(time.Now().Add(time.Duration(c.writeTimeout))) - err = c.conn.WriteMessage(&message.MsgVideo{ - ChunkStreamID: message.MsgVideoChunkStreamID, + err = c.conn.WriteMessage(&message.Video{ + ChunkStreamID: message.VideoChunkStreamID, MessageStreamID: 0x1000000, Codec: message.CodecH264, IsKeyFrame: idrPresent, - Type: message.MsgVideoTypeAU, + Type: message.VideoTypeAU, Payload: avcc, DTS: dts, PTSDelta: pts - dts, @@ -557,14 +601,14 @@ func (c *rtmpConn) findAudioFormat(stream *stream, ringBuffer *ringbuffer.RingBu for i, au := range tunit.AUs { c.nconn.SetWriteDeadline(time.Now().Add(time.Duration(c.writeTimeout))) - err := c.conn.WriteMessage(&message.MsgAudio{ - ChunkStreamID: message.MsgAudioChunkStreamID, + err := c.conn.WriteMessage(&message.Audio{ + ChunkStreamID: message.AudioChunkStreamID, MessageStreamID: 0x1000000, Codec: message.CodecMPEG4Audio, Rate: flvio.SOUND_44Khz, Depth: flvio.SOUND_16BIT, Channels: flvio.SOUND_STEREO, - AACType: message.MsgAudioAACTypeAU, + AACType: message.AudioAACTypeAU, Payload: au, DTS: pts + time.Duration(i)*mpeg4audio.SamplesPerAccessUnit* time.Second/time.Duration(audioFormatMPEG4.ClockRate()), @@ -635,8 +679,8 @@ func (c *rtmpConn) findAudioFormat(stream *stream, ringBuffer *ringbuffer.RingBu rate = flvio.SOUND_22Khz } - msg := &message.MsgAudio{ - ChunkStreamID: message.MsgAudioChunkStreamID, + msg := &message.Audio{ + ChunkStreamID: message.AudioChunkStreamID, MessageStreamID: 0x1000000, Codec: message.CodecMPEG2Audio, Rate: rate, @@ -751,23 +795,23 @@ func (c *rtmpConn) runPublish(ctx context.Context, u *url.URL) error { return err } - switch tmsg := msg.(type) { - case *message.MsgVideo: + switch msg.(type) { + case *message.Video, *message.ExtendedFramesX, *message.ExtendedCodedFrames: if videoFormat == nil { return fmt.Errorf("received a video packet, but track is not set up") } - err := videoWriteFunc(tmsg) + err := videoWriteFunc(msg) if err != nil { c.Log(logger.Warn, "%v", err) } - case *message.MsgAudio: + case *message.Audio: if audioFormat == nil { return fmt.Errorf("received an audio packet, but track is not set up") } - err := audioWriteFunc(tmsg) + err := audioWriteFunc(msg) if err != nil { c.Log(logger.Warn, "%v", err) } diff --git a/internal/core/rtmp_server_test.go b/internal/core/rtmp_server_test.go index 41986263..4e89420f 100644 --- a/internal/core/rtmp_server_test.go +++ b/internal/core/rtmp_server_test.go @@ -145,12 +145,12 @@ func TestRTMPServerPublishRead(t *testing.T) { require.Equal(t, videoTrack, videoTrack1) require.Equal(t, audioTrack, audioTrack2) - err = conn1.WriteMessage(&message.MsgVideo{ - ChunkStreamID: message.MsgVideoChunkStreamID, + err = conn1.WriteMessage(&message.Video{ + ChunkStreamID: message.VideoChunkStreamID, MessageStreamID: 0x1000000, Codec: message.CodecH264, IsKeyFrame: true, - Type: message.MsgVideoTypeAU, + Type: message.VideoTypeAU, Payload: []byte{ 0x00, 0x00, 0x00, 0x04, 0x05, 0x02, 0x03, 0x04, // IDR 1 0x00, 0x00, 0x00, 0x04, 0x05, 0x02, 0x03, 0x04, // IDR 2 @@ -160,12 +160,12 @@ func TestRTMPServerPublishRead(t *testing.T) { msg1, err := conn2.ReadMessage() require.NoError(t, err) - require.Equal(t, &message.MsgVideo{ - ChunkStreamID: message.MsgVideoChunkStreamID, + require.Equal(t, &message.Video{ + ChunkStreamID: message.VideoChunkStreamID, MessageStreamID: 0x1000000, Codec: message.CodecH264, IsKeyFrame: true, - Type: message.MsgVideoTypeAU, + Type: message.VideoTypeAU, Payload: []byte{ 0x00, 0x00, 0x00, 0x19, // SPS 0x67, 0x42, 0xc0, 0x28, 0xd9, 0x00, 0x78, 0x02, diff --git a/internal/core/rtmp_source.go b/internal/core/rtmp_source.go index b4543cc6..1a0ae55b 100644 --- a/internal/core/rtmp_source.go +++ b/internal/core/rtmp_source.go @@ -113,8 +113,9 @@ func (s *rtmpSource) run(ctx context.Context, cnf *conf.PathConf, reloadConf cha return err } - if _, ok := videoFormat.(*formats.H265); ok { - return fmt.Errorf("proxying H265 streams with RTMP is not supported") + switch videoFormat.(type) { + case *formats.H265, *formats.AV1: + return fmt.Errorf("proxying H265 or AV1 tracks with RTMP is not supported") } var medias media.Medias @@ -165,7 +166,7 @@ func (s *rtmpSource) run(ctx context.Context, cnf *conf.PathConf, reloadConf cha } switch tmsg := msg.(type) { - case *message.MsgVideo: + case *message.Video: if videoFormat == nil { return fmt.Errorf("received an H264 packet, but track is not set up") } @@ -175,7 +176,7 @@ func (s *rtmpSource) run(ctx context.Context, cnf *conf.PathConf, reloadConf cha s.Log(logger.Warn, "%v", err) } - case *message.MsgAudio: + case *message.Audio: if audioFormat == nil { return fmt.Errorf("received an AAC packet, but track is not set up") } diff --git a/internal/core/rtmp_source_test.go b/internal/core/rtmp_source_test.go index 6f0b5513..28c41842 100644 --- a/internal/core/rtmp_source_test.go +++ b/internal/core/rtmp_source_test.go @@ -86,12 +86,12 @@ func TestRTMPSource(t *testing.T) { <-connected - err = conn.WriteMessage(&message.MsgVideo{ - ChunkStreamID: message.MsgVideoChunkStreamID, + err = conn.WriteMessage(&message.Video{ + ChunkStreamID: message.VideoChunkStreamID, MessageStreamID: 0x1000000, Codec: message.CodecH264, IsKeyFrame: true, - Type: message.MsgVideoTypeAU, + Type: message.VideoTypeAU, Payload: []byte{0x00, 0x00, 0x00, 0x04, 0x05, 0x02, 0x03, 0x04}, }) require.NoError(t, err) diff --git a/internal/core/webrtc_conn.go b/internal/core/webrtc_conn.go index e4a77cc6..f5776221 100644 --- a/internal/core/webrtc_conn.go +++ b/internal/core/webrtc_conn.go @@ -46,6 +46,7 @@ func newPeerConnection(configuration webrtc.Configuration, options ...func(*webrtc.API), ) (*webrtc.PeerConnection, error) { m := &webrtc.MediaEngine{} + if err := m.RegisterDefaultCodecs(); err != nil { return nil, err } @@ -328,7 +329,7 @@ func (c *webRTCConn) runInner(ctx context.Context) error { if tracks == nil { return fmt.Errorf( - "the stream doesn't contain any supported codec (which are currently VP9, VP8, H264, Opus, G722, G711)") + "the stream doesn't contain any supported codec, which are currently H264, VP8, VP9, G711, G722, Opus") } err = c.wsconn.WriteJSON(c.genICEServers()) diff --git a/internal/formatprocessor/av1.go b/internal/formatprocessor/av1.go new file mode 100644 index 00000000..05167f4a --- /dev/null +++ b/internal/formatprocessor/av1.go @@ -0,0 +1,133 @@ +package formatprocessor + +import ( + "fmt" + "time" + + "github.com/bluenviron/gortsplib/v3/pkg/formats" + "github.com/bluenviron/gortsplib/v3/pkg/formats/rtpav1" + "github.com/bluenviron/mediacommon/pkg/codecs/av1" + "github.com/pion/rtp" + + "github.com/aler9/mediamtx/internal/logger" +) + +// UnitAV1 is an AV1 data unit. +type UnitAV1 struct { + RTPPackets []*rtp.Packet + NTP time.Time + PTS time.Duration + OBUs [][]byte +} + +// GetRTPPackets implements Unit. +func (d *UnitAV1) GetRTPPackets() []*rtp.Packet { + return d.RTPPackets +} + +// GetNTP implements Unit. +func (d *UnitAV1) GetNTP() time.Time { + return d.NTP +} + +type formatProcessorAV1 struct { + udpMaxPayloadSize int + format *formats.AV1 + log logger.Writer + + encoder *rtpav1.Encoder + decoder *rtpav1.Decoder + lastKeyFrameReceived time.Time +} + +func newAV1( + udpMaxPayloadSize int, + forma *formats.AV1, + generateRTPPackets bool, + log logger.Writer, +) (*formatProcessorAV1, error) { + t := &formatProcessorAV1{ + udpMaxPayloadSize: udpMaxPayloadSize, + format: forma, + log: log, + } + + if generateRTPPackets { + t.encoder = &rtpav1.Encoder{ + PayloadMaxSize: t.udpMaxPayloadSize - 12, + } + t.encoder.Init() + t.lastKeyFrameReceived = time.Now() + } + + return t, nil +} + +func (t *formatProcessorAV1) checkKeyFrameInterval(containsKeyFrame bool) { + if containsKeyFrame { + t.lastKeyFrameReceived = time.Now() + } else { + now := time.Now() + if now.Sub(t.lastKeyFrameReceived) >= maxKeyFrameInterval { + t.lastKeyFrameReceived = now + t.log.Log(logger.Warn, "no AV1 key frames received in %v, stream can't be decoded", maxKeyFrameInterval) + } + } +} + +func (t *formatProcessorAV1) checkOBUs(obus [][]byte) { + containsKeyFrame, _ := av1.ContainsKeyFrame(obus) + t.checkKeyFrameInterval(containsKeyFrame) +} + +func (t *formatProcessorAV1) Process(unit Unit, hasNonRTSPReaders bool) error { //nolint:dupl + tunit := unit.(*UnitAV1) + + if tunit.RTPPackets != nil { + pkt := tunit.RTPPackets[0] + + // remove padding + pkt.Header.Padding = false + pkt.PaddingSize = 0 + + if pkt.MarshalSize() > t.udpMaxPayloadSize { + return fmt.Errorf("payload size (%d) is greater than maximum allowed (%d)", + pkt.MarshalSize(), t.udpMaxPayloadSize) + } + + // decode from RTP + if hasNonRTSPReaders { + if t.decoder == nil { + t.decoder = t.format.CreateDecoder() + t.lastKeyFrameReceived = time.Now() + } + + // DecodeUntilMarker() is necessary, otherwise Encode() generates partial groups + obus, pts, err := t.decoder.DecodeUntilMarker(pkt) + if err != nil { + if err == rtpav1.ErrNonStartingPacketAndNoPrevious || err == rtpav1.ErrMorePacketsNeeded { + return nil + } + return err + } + + tunit.OBUs = obus + t.checkOBUs(obus) + tunit.PTS = pts + } + + // route packet as is + return nil + } + + t.checkOBUs(tunit.OBUs) + + // encode into RTP + pkts, err := t.encoder.Encode(tunit.OBUs, tunit.PTS) + if err != nil { + return err + } + tunit.RTPPackets = pkts + + return nil +} diff --git a/internal/formatprocessor/h264.go b/internal/formatprocessor/h264.go index dda3673c..1d1b08ff 100644 --- a/internal/formatprocessor/h264.go +++ b/internal/formatprocessor/h264.go @@ -180,7 +180,7 @@ func (t *formatProcessorH264) checkKeyFrameInterval(isKeyFrame bool) { now := time.Now() if now.Sub(t.lastKeyFrameReceived) >= maxKeyFrameInterval { t.lastKeyFrameReceived = now - t.log.Log(logger.Warn, "no key frames received in %v, stream can't be decoded") + t.log.Log(logger.Warn, "no H264 key frames received in %v, stream can't be decoded") } } } diff --git a/internal/formatprocessor/h265.go b/internal/formatprocessor/h265.go index f7c60268..2b84cd13 100644 --- a/internal/formatprocessor/h265.go +++ b/internal/formatprocessor/h265.go @@ -200,7 +200,7 @@ func (t *formatProcessorH265) checkKeyFrameInterval(isKeyFrame bool) { now := time.Now() if now.Sub(t.lastKeyFrameReceived) >= maxKeyFrameInterval { t.lastKeyFrameReceived = now - t.log.Log(logger.Warn, "no key frames received in %v, stream can't be decoded") + t.log.Log(logger.Warn, "no H265 key frames received in %v, stream can't be decoded") } } } diff --git a/internal/formatprocessor/processor.go b/internal/formatprocessor/processor.go index 697abd5e..c3e87862 100644 --- a/internal/formatprocessor/processor.go +++ b/internal/formatprocessor/processor.go @@ -39,6 +39,9 @@ func New( case *formats.VP9: return newVP9(udpMaxPayloadSize, forma, generateRTPPackets, log) + case *formats.AV1: + return newAV1(udpMaxPayloadSize, forma, generateRTPPackets, log) + case *formats.MPEG2Audio: return newMPEG2Audio(udpMaxPayloadSize, forma, generateRTPPackets, log) diff --git a/internal/rtmp/chunk/chunk0.go b/internal/rtmp/chunk/chunk0.go index 0ad855c5..7bb76d09 100644 --- a/internal/rtmp/chunk/chunk0.go +++ b/internal/rtmp/chunk/chunk0.go @@ -11,7 +11,7 @@ import ( type Chunk0 struct { ChunkStreamID byte Timestamp uint32 - Type MessageType + Type uint8 MessageStreamID uint32 BodyLen uint32 Body []byte @@ -28,7 +28,7 @@ func (c *Chunk0) Read(r io.Reader, chunkMaxBodyLen uint32) error { c.ChunkStreamID = header[0] & 0x3F c.Timestamp = uint32(header[1])<<16 | uint32(header[2])<<8 | uint32(header[3]) c.BodyLen = uint32(header[4])<<16 | uint32(header[5])<<8 | uint32(header[6]) - c.Type = MessageType(header[7]) + c.Type = header[7] c.MessageStreamID = uint32(header[8])<<24 | uint32(header[9])<<16 | uint32(header[10])<<8 | uint32(header[11]) chunkBodyLen := c.BodyLen @@ -51,7 +51,7 @@ func (c Chunk0) Marshal() ([]byte, error) { buf[4] = byte(c.BodyLen >> 16) buf[5] = byte(c.BodyLen >> 8) buf[6] = byte(c.BodyLen) - buf[7] = byte(c.Type) + buf[7] = c.Type buf[8] = byte(c.MessageStreamID >> 24) buf[9] = byte(c.MessageStreamID >> 16) buf[10] = byte(c.MessageStreamID >> 8) diff --git a/internal/rtmp/chunk/chunk0_test.go b/internal/rtmp/chunk/chunk0_test.go index 91c61433..48105584 100644 --- a/internal/rtmp/chunk/chunk0_test.go +++ b/internal/rtmp/chunk/chunk0_test.go @@ -15,7 +15,7 @@ var chunk0enc = []byte{ var chunk0dec = Chunk0{ ChunkStreamID: 25, Timestamp: 11641233, - Type: MessageTypeCommandAMF0, + Type: 20, MessageStreamID: 56432445, BodyLen: 20, Body: []byte{0x01, 0x02, 0x03, 0x04}, diff --git a/internal/rtmp/chunk/chunk1.go b/internal/rtmp/chunk/chunk1.go index 9aea9b1a..3c2ed0bd 100644 --- a/internal/rtmp/chunk/chunk1.go +++ b/internal/rtmp/chunk/chunk1.go @@ -13,7 +13,7 @@ import ( type Chunk1 struct { ChunkStreamID byte TimestampDelta uint32 - Type MessageType + Type uint8 BodyLen uint32 Body []byte } @@ -29,7 +29,7 @@ func (c *Chunk1) Read(r io.Reader, chunkMaxBodyLen uint32) error { c.ChunkStreamID = header[0] & 0x3F c.TimestampDelta = uint32(header[1])<<16 | uint32(header[2])<<8 | uint32(header[3]) c.BodyLen = uint32(header[4])<<16 | uint32(header[5])<<8 | uint32(header[6]) - c.Type = MessageType(header[7]) + c.Type = header[7] chunkBodyLen := (c.BodyLen) if chunkBodyLen > chunkMaxBodyLen { @@ -51,7 +51,7 @@ func (c Chunk1) Marshal() ([]byte, error) { buf[4] = byte(c.BodyLen >> 16) buf[5] = byte(c.BodyLen >> 8) buf[6] = byte(c.BodyLen) - buf[7] = byte(c.Type) + buf[7] = c.Type copy(buf[8:], c.Body) return buf, nil } diff --git a/internal/rtmp/chunk/chunk1_test.go b/internal/rtmp/chunk/chunk1_test.go index e4ef0a1a..3d6cf33c 100644 --- a/internal/rtmp/chunk/chunk1_test.go +++ b/internal/rtmp/chunk/chunk1_test.go @@ -15,7 +15,7 @@ var chunk1enc = []byte{ var chunk1dec = Chunk1{ ChunkStreamID: 25, TimestampDelta: 11641233, - Type: MessageTypeCommandAMF0, + Type: 20, BodyLen: 20, Body: []byte{0x01, 0x02, 0x03, 0x04}, } diff --git a/internal/rtmp/chunk/messagetype.go b/internal/rtmp/chunk/messagetype.go deleted file mode 100644 index 5002d98b..00000000 --- a/internal/rtmp/chunk/messagetype.go +++ /dev/null @@ -1,24 +0,0 @@ -package chunk - -// MessageType is a message type. -type MessageType byte - -// message types. -const ( - MessageTypeSetChunkSize MessageType = 1 - MessageTypeAbortMessage MessageType = 2 - MessageTypeAcknowledge MessageType = 3 - MessageTypeSetWindowAckSize MessageType = 5 - MessageTypeSetPeerBandwidth MessageType = 6 - - MessageTypeUserControl MessageType = 4 - - MessageTypeCommandAMF3 MessageType = 17 - MessageTypeCommandAMF0 MessageType = 20 - - MessageTypeDataAMF3 MessageType = 15 - MessageTypeDataAMF0 MessageType = 18 - - MessageTypeAudio MessageType = 8 - MessageTypeVideo MessageType = 9 -) diff --git a/internal/rtmp/conn.go b/internal/rtmp/conn.go index f33fb1a9..31e32a00 100644 --- a/internal/rtmp/conn.go +++ b/internal/rtmp/conn.go @@ -2,26 +2,21 @@ package rtmp import ( - "errors" "fmt" "io" "net/url" "strings" - "time" "github.com/bluenviron/gortsplib/v3/pkg/formats" - "github.com/bluenviron/mediacommon/pkg/codecs/h264" - "github.com/bluenviron/mediacommon/pkg/codecs/h265" - "github.com/bluenviron/mediacommon/pkg/codecs/mpeg4audio" "github.com/notedit/rtmp/format/flv/flvio" "github.com/aler9/mediamtx/internal/rtmp/bytecounter" - "github.com/aler9/mediamtx/internal/rtmp/h264conf" "github.com/aler9/mediamtx/internal/rtmp/handshake" "github.com/aler9/mediamtx/internal/rtmp/message" + "github.com/aler9/mediamtx/internal/rtmp/tracks" ) -func resultIsOK1(res *message.MsgCommandAMF0) bool { +func resultIsOK1(res *message.CommandAMF0) bool { if len(res.Arguments) < 2 { return false } @@ -39,7 +34,7 @@ func resultIsOK1(res *message.MsgCommandAMF0) bool { return v == "status" } -func resultIsOK2(res *message.MsgCommandAMF0) bool { +func resultIsOK2(res *message.CommandAMF0) bool { if len(res.Arguments) < 2 { return false } @@ -126,27 +121,27 @@ func (c *Conn) BytesSent() uint64 { return c.bc.Writer.Count() } -func (c *Conn) readCommand() (*message.MsgCommandAMF0, error) { +func (c *Conn) readCommand() (*message.CommandAMF0, error) { for { msg, err := c.mrw.Read() if err != nil { return nil, err } - if cmd, ok := msg.(*message.MsgCommandAMF0); ok { + if cmd, ok := msg.(*message.CommandAMF0); ok { return cmd, nil } } } -func (c *Conn) readCommandResult(commandID int, commandName string, isValid func(*message.MsgCommandAMF0) bool) error { +func (c *Conn) readCommandResult(commandID int, commandName string, isValid func(*message.CommandAMF0) bool) error { for { msg, err := c.mrw.Read() if err != nil { return err } - if cmd, ok := msg.(*message.MsgCommandAMF0); ok { + if cmd, ok := msg.(*message.CommandAMF0); ok { if cmd.CommandID == commandID && cmd.Name == commandName { if !isValid(cmd) { return fmt.Errorf("server refused connect request") @@ -169,14 +164,14 @@ func (c *Conn) InitializeClient(u *url.URL, isPublishing bool) error { c.mrw = message.NewReadWriter(c.bc, false) - err = c.mrw.Write(&message.MsgSetWindowAckSize{ + err = c.mrw.Write(&message.SetWindowAckSize{ Value: 2500000, }) if err != nil { return err } - err = c.mrw.Write(&message.MsgSetPeerBandwidth{ + err = c.mrw.Write(&message.SetPeerBandwidth{ Value: 2500000, Type: 2, }) @@ -184,14 +179,14 @@ func (c *Conn) InitializeClient(u *url.URL, isPublishing bool) error { return err } - err = c.mrw.Write(&message.MsgSetChunkSize{ + err = c.mrw.Write(&message.SetChunkSize{ Value: 65536, }) if err != nil { return err } - err = c.mrw.Write(&message.MsgCommandAMF0{ + err = c.mrw.Write(&message.CommandAMF0{ ChunkStreamID: 3, Name: "connect", CommandID: 1, @@ -218,7 +213,7 @@ func (c *Conn) InitializeClient(u *url.URL, isPublishing bool) error { } if !isPublishing { - err = c.mrw.Write(&message.MsgCommandAMF0{ + err = c.mrw.Write(&message.CommandAMF0{ ChunkStreamID: 3, Name: "createStream", CommandID: 2, @@ -235,14 +230,14 @@ func (c *Conn) InitializeClient(u *url.URL, isPublishing bool) error { return err } - err = c.mrw.Write(&message.MsgUserControlSetBufferLength{ + err = c.mrw.Write(&message.UserControlSetBufferLength{ BufferLength: 0x64, }) if err != nil { return err } - err = c.mrw.Write(&message.MsgCommandAMF0{ + err = c.mrw.Write(&message.CommandAMF0{ ChunkStreamID: 4, MessageStreamID: 0x1000000, Name: "play", @@ -259,7 +254,7 @@ func (c *Conn) InitializeClient(u *url.URL, isPublishing bool) error { return c.readCommandResult(3, "onStatus", resultIsOK1) } - err = c.mrw.Write(&message.MsgCommandAMF0{ + err = c.mrw.Write(&message.CommandAMF0{ ChunkStreamID: 3, Name: "releaseStream", CommandID: 2, @@ -272,7 +267,7 @@ func (c *Conn) InitializeClient(u *url.URL, isPublishing bool) error { return err } - err = c.mrw.Write(&message.MsgCommandAMF0{ + err = c.mrw.Write(&message.CommandAMF0{ ChunkStreamID: 3, Name: "FCPublish", CommandID: 3, @@ -285,7 +280,7 @@ func (c *Conn) InitializeClient(u *url.URL, isPublishing bool) error { return err } - err = c.mrw.Write(&message.MsgCommandAMF0{ + err = c.mrw.Write(&message.CommandAMF0{ ChunkStreamID: 3, Name: "createStream", CommandID: 4, @@ -302,7 +297,7 @@ func (c *Conn) InitializeClient(u *url.URL, isPublishing bool) error { return err } - err = c.mrw.Write(&message.MsgCommandAMF0{ + err = c.mrw.Write(&message.CommandAMF0{ ChunkStreamID: 4, MessageStreamID: 0x1000000, Name: "publish", @@ -362,14 +357,14 @@ func (c *Conn) InitializeServer() (*url.URL, bool, error) { tcURL = strings.Trim(tcURL, "'") - err = c.mrw.Write(&message.MsgSetWindowAckSize{ + err = c.mrw.Write(&message.SetWindowAckSize{ Value: 2500000, }) if err != nil { return nil, false, err } - err = c.mrw.Write(&message.MsgSetPeerBandwidth{ + err = c.mrw.Write(&message.SetPeerBandwidth{ Value: 2500000, Type: 2, }) @@ -377,7 +372,7 @@ func (c *Conn) InitializeServer() (*url.URL, bool, error) { return nil, false, err } - err = c.mrw.Write(&message.MsgSetChunkSize{ + err = c.mrw.Write(&message.SetChunkSize{ Value: 65536, }) if err != nil { @@ -386,7 +381,7 @@ func (c *Conn) InitializeServer() (*url.URL, bool, error) { oe, _ := ma.GetFloat64("objectEncoding") - err = c.mrw.Write(&message.MsgCommandAMF0{ + err = c.mrw.Write(&message.CommandAMF0{ ChunkStreamID: cmd.ChunkStreamID, Name: "_result", CommandID: cmd.CommandID, @@ -415,7 +410,7 @@ func (c *Conn) InitializeServer() (*url.URL, bool, error) { switch cmd.Name { case "createStream": - err = c.mrw.Write(&message.MsgCommandAMF0{ + err = c.mrw.Write(&message.CommandAMF0{ ChunkStreamID: cmd.ChunkStreamID, Name: "_result", CommandID: cmd.CommandID, @@ -443,21 +438,21 @@ func (c *Conn) InitializeServer() (*url.URL, bool, error) { return nil, false, err } - err = c.mrw.Write(&message.MsgUserControlStreamIsRecorded{ + err = c.mrw.Write(&message.UserControlStreamIsRecorded{ StreamID: 1, }) if err != nil { return nil, false, err } - err = c.mrw.Write(&message.MsgUserControlStreamBegin{ + err = c.mrw.Write(&message.UserControlStreamBegin{ StreamID: 1, }) if err != nil { return nil, false, err } - err = c.mrw.Write(&message.MsgCommandAMF0{ + err = c.mrw.Write(&message.CommandAMF0{ ChunkStreamID: 5, MessageStreamID: 0x1000000, Name: "onStatus", @@ -475,7 +470,7 @@ func (c *Conn) InitializeServer() (*url.URL, bool, error) { return nil, false, err } - err = c.mrw.Write(&message.MsgCommandAMF0{ + err = c.mrw.Write(&message.CommandAMF0{ ChunkStreamID: 5, MessageStreamID: 0x1000000, Name: "onStatus", @@ -493,7 +488,7 @@ func (c *Conn) InitializeServer() (*url.URL, bool, error) { return nil, false, err } - err = c.mrw.Write(&message.MsgCommandAMF0{ + err = c.mrw.Write(&message.CommandAMF0{ ChunkStreamID: 5, MessageStreamID: 0x1000000, Name: "onStatus", @@ -511,7 +506,7 @@ func (c *Conn) InitializeServer() (*url.URL, bool, error) { return nil, false, err } - err = c.mrw.Write(&message.MsgCommandAMF0{ + err = c.mrw.Write(&message.CommandAMF0{ ChunkStreamID: 5, MessageStreamID: 0x1000000, Name: "onStatus", @@ -546,7 +541,7 @@ func (c *Conn) InitializeServer() (*url.URL, bool, error) { return nil, false, err } - err = c.mrw.Write(&message.MsgCommandAMF0{ + err = c.mrw.Write(&message.CommandAMF0{ ChunkStreamID: 5, Name: "onStatus", CommandID: cmd.CommandID, @@ -579,420 +574,13 @@ func (c *Conn) WriteMessage(msg message.Message) error { return c.mrw.Write(msg) } -func trackFromH264DecoderConfig(data []byte) (formats.Format, error) { - var conf h264conf.Conf - err := conf.Unmarshal(data) - if err != nil { - return nil, fmt.Errorf("unable to parse H264 config: %v", err) - } - - return &formats.H264{ - PayloadTyp: 96, - SPS: conf.SPS, - PPS: conf.PPS, - PacketizationMode: 1, - }, nil -} - -func trackFromAACDecoderConfig(data []byte) (*formats.MPEG4Audio, error) { - var mpegConf mpeg4audio.Config - err := mpegConf.Unmarshal(data) - if err != nil { - return nil, err - } - - return &formats.MPEG4Audio{ - PayloadTyp: 96, - Config: &mpegConf, - SizeLength: 13, - IndexLength: 3, - IndexDeltaLength: 3, - }, nil -} - -var errEmptyMetadata = errors.New("metadata is empty") - -func (c *Conn) readTracksFromMetadata(payload []interface{}) (formats.Format, formats.Format, error) { - if len(payload) != 1 { - return nil, nil, fmt.Errorf("invalid metadata") - } - - md, ok := payload[0].(flvio.AMFMap) - if !ok { - return nil, nil, fmt.Errorf("invalid metadata") - } - - var videoTrack formats.Format - var audioTrack formats.Format - - hasVideo, err := func() (bool, error) { - v, ok := md.GetV("videocodecid") - if !ok { - return false, nil - } - - switch vt := v.(type) { - case float64: - switch vt { - case 0: - return false, nil - - case message.CodecH264: - return true, nil - } - - case string: - if vt == "avc1" { - return true, nil - } - } - - return false, fmt.Errorf("unsupported video codec: %v", v) - }() - if err != nil { - return nil, nil, err - } - - hasAudio, err := func() (bool, error) { - v, ok := md.GetV("audiocodecid") - if !ok { - return false, nil - } - - switch vt := v.(type) { - case float64: - switch vt { - case 0: - return false, nil - - case message.CodecMPEG2Audio: - audioTrack = &formats.MPEG2Audio{} - return true, nil - - case message.CodecMPEG4Audio: - return true, nil - } - - case string: - if vt == "mp4a" { - return true, nil - } - } - - return false, fmt.Errorf("unsupported audio codec %v", v) - }() - if err != nil { - return nil, nil, err - } - - if !hasVideo && !hasAudio { - return nil, nil, errEmptyMetadata - } - - for { - if (!hasVideo || videoTrack != nil) && - (!hasAudio || audioTrack != nil) { - return videoTrack, audioTrack, nil - } - - msg, err := c.ReadMessage() - if err != nil { - return nil, nil, err - } - - switch tmsg := msg.(type) { - case *message.MsgVideo: - if !hasVideo { - return nil, nil, fmt.Errorf("unexpected video packet") - } - - if videoTrack == nil { - if tmsg.Type == message.MsgVideoTypeConfig { - videoTrack, err = trackFromH264DecoderConfig(tmsg.Payload) - if err != nil { - return nil, nil, err - } - } else if tmsg.Type == message.MsgVideoTypeAU && tmsg.IsKeyFrame { - nalus, err := h264.AVCCUnmarshal(tmsg.Payload) - if err != nil { - return nil, nil, err - } - - var h265VPS []byte - var h265SPS []byte - var h265PPS []byte - - for _, nalu := range nalus { - typ := h265.NALUType((nalu[0] >> 1) & 0b111111) - - switch typ { - case h265.NALUType_VPS_NUT: - h265VPS = append([]byte(nil), nalu...) - - case h265.NALUType_SPS_NUT: - h265SPS = append([]byte(nil), nalu...) - - case h265.NALUType_PPS_NUT: - h265PPS = append([]byte(nil), nalu...) - } - } - - if h265VPS != nil && h265SPS != nil && h265PPS != nil { - videoTrack = &formats.H265{ - PayloadTyp: 96, - VPS: h265VPS, - SPS: h265SPS, - PPS: h265PPS, - } - } - } - } - - case *message.MsgAudio: - if !hasAudio { - return nil, nil, fmt.Errorf("unexpected audio packet") - } - - if audioTrack == nil { - if tmsg.Codec == message.CodecMPEG4Audio && tmsg.AACType == message.MsgAudioAACTypeConfig { - audioTrack, err = trackFromAACDecoderConfig(tmsg.Payload) - if err != nil { - return nil, nil, err - } - } - } - } - } -} - -func (c *Conn) readTracksFromMessages(msg message.Message) (formats.Format, *formats.MPEG4Audio, error) { - var startTime *time.Duration - var videoTrack formats.Format - var audioTrack *formats.MPEG4Audio - - // analyze 1 second of packets -outer: - for { - switch tmsg := msg.(type) { - case *message.MsgVideo: - if startTime == nil { - v := tmsg.DTS - startTime = &v - } - - if tmsg.Type == message.MsgVideoTypeConfig { - if videoTrack == nil { - var err error - videoTrack, err = trackFromH264DecoderConfig(tmsg.Payload) - if err != nil { - return nil, nil, err - } - - // stop the analysis if both tracks are found - if videoTrack != nil && audioTrack != nil { - return videoTrack, audioTrack, nil - } - } - } - - if (tmsg.DTS - *startTime) >= 1*time.Second { - break outer - } - - case *message.MsgAudio: - if startTime == nil { - v := tmsg.DTS - startTime = &v - } - - if tmsg.AACType == message.MsgAudioAACTypeConfig { - if audioTrack == nil { - var err error - audioTrack, err = trackFromAACDecoderConfig(tmsg.Payload) - if err != nil { - return nil, nil, err - } - - // stop the analysis if both tracks are found - if videoTrack != nil && audioTrack != nil { - return videoTrack, audioTrack, nil - } - } - } - - if (tmsg.DTS - *startTime) >= 1*time.Second { - break outer - } - } - - var err error - msg, err = c.ReadMessage() - if err != nil { - return nil, nil, err - } - } - - if videoTrack == nil && audioTrack == nil { - return nil, nil, fmt.Errorf("no tracks found") - } - - return videoTrack, audioTrack, nil -} - // ReadTracks reads track informations. // It returns the video track and the audio track. func (c *Conn) ReadTracks() (formats.Format, formats.Format, error) { - msg, err := func() (message.Message, error) { - for { - msg, err := c.ReadMessage() - if err != nil { - return nil, err - } - - // skip play start and data start - if cmd, ok := msg.(*message.MsgCommandAMF0); ok && cmd.Name == "onStatus" { - continue - } - - // skip RtmpSampleAccess - if data, ok := msg.(*message.MsgDataAMF0); ok && len(data.Payload) >= 1 { - if s, ok := data.Payload[0].(string); ok && s == "|RtmpSampleAccess" { - continue - } - } - - return msg, nil - } - }() - if err != nil { - return nil, nil, err - } - - if data, ok := msg.(*message.MsgDataAMF0); ok && len(data.Payload) >= 1 { - payload := data.Payload - - if s, ok := payload[0].(string); ok && s == "@setDataFrame" { - payload = payload[1:] - } - - if len(payload) >= 1 { - if s, ok := payload[0].(string); ok && s == "onMetaData" { - videoTrack, audioTrack, err := c.readTracksFromMetadata(payload[1:]) - if err != nil { - if err == errEmptyMetadata { - msg, err := c.ReadMessage() - if err != nil { - return nil, nil, err - } - - return c.readTracksFromMessages(msg) - } - - return nil, nil, err - } - - return videoTrack, audioTrack, nil - } - } - } - - return c.readTracksFromMessages(msg) + return tracks.Read(c.mrw) } // WriteTracks writes track informations. func (c *Conn) WriteTracks(videoTrack formats.Format, audioTrack formats.Format) error { - err := c.WriteMessage(&message.MsgDataAMF0{ - ChunkStreamID: 4, - MessageStreamID: 0x1000000, - Payload: []interface{}{ - "@setDataFrame", - "onMetaData", - flvio.AMFMap{ - { - K: "videodatarate", - V: float64(0), - }, - { - K: "videocodecid", - V: func() float64 { - switch videoTrack.(type) { - case *formats.H264: - return message.CodecH264 - - default: - return 0 - } - }(), - }, - { - K: "audiodatarate", - V: float64(0), - }, - { - K: "audiocodecid", - V: func() float64 { - switch audioTrack.(type) { - case *formats.MPEG2Audio: - return message.CodecMPEG2Audio - - case *formats.MPEG4Audio: - return message.CodecMPEG4Audio - - default: - return 0 - } - }(), - }, - }, - }, - }) - if err != nil { - return err - } - - if videoTrack, ok := videoTrack.(*formats.H264); ok { - // write decoder config only if SPS and PPS are available. - // if they're not available yet, they're sent later. - if sps, pps := videoTrack.SafeParams(); sps != nil && pps != nil { - buf, _ := h264conf.Conf{ - SPS: sps, - PPS: pps, - }.Marshal() - - err = c.WriteMessage(&message.MsgVideo{ - ChunkStreamID: message.MsgVideoChunkStreamID, - MessageStreamID: 0x1000000, - Codec: message.CodecH264, - IsKeyFrame: true, - Type: message.MsgVideoTypeConfig, - Payload: buf, - }) - if err != nil { - return err - } - } - } - - if mpeg4audioTrack, ok := audioTrack.(*formats.MPEG4Audio); ok { - enc, err := mpeg4audioTrack.Config.Marshal() - if err != nil { - return err - } - - err = c.WriteMessage(&message.MsgAudio{ - ChunkStreamID: message.MsgAudioChunkStreamID, - MessageStreamID: 0x1000000, - Codec: message.CodecMPEG4Audio, - Rate: flvio.SOUND_44Khz, - Depth: flvio.SOUND_16BIT, - Channels: flvio.SOUND_STEREO, - AACType: message.MsgAudioAACTypeConfig, - Payload: enc, - }) - if err != nil { - return err - } - } - - return nil + return tracks.Write(c.mrw, videoTrack, audioTrack) } diff --git a/internal/rtmp/conn_test.go b/internal/rtmp/conn_test.go index 7e0cd00d..04296831 100644 --- a/internal/rtmp/conn_test.go +++ b/internal/rtmp/conn_test.go @@ -5,16 +5,11 @@ import ( "net" "net/url" "testing" - "time" - "github.com/bluenviron/gortsplib/v3/pkg/formats" - "github.com/bluenviron/mediacommon/pkg/codecs/h264" - "github.com/bluenviron/mediacommon/pkg/codecs/mpeg4audio" "github.com/notedit/rtmp/format/flv/flvio" "github.com/stretchr/testify/require" "github.com/aler9/mediamtx/internal/rtmp/bytecounter" - "github.com/aler9/mediamtx/internal/rtmp/h264conf" "github.com/aler9/mediamtx/internal/rtmp/handshake" "github.com/aler9/mediamtx/internal/rtmp/message" ) @@ -41,26 +36,26 @@ func TestInitializeClient(t *testing.T) { msg, err := mrw.Read() require.NoError(t, err) - require.Equal(t, &message.MsgSetWindowAckSize{ + require.Equal(t, &message.SetWindowAckSize{ Value: 2500000, }, msg) msg, err = mrw.Read() require.NoError(t, err) - require.Equal(t, &message.MsgSetPeerBandwidth{ + require.Equal(t, &message.SetPeerBandwidth{ Value: 2500000, Type: 2, }, msg) msg, err = mrw.Read() require.NoError(t, err) - require.Equal(t, &message.MsgSetChunkSize{ + require.Equal(t, &message.SetChunkSize{ Value: 65536, }, msg) msg, err = mrw.Read() require.NoError(t, err) - require.Equal(t, &message.MsgCommandAMF0{ + require.Equal(t, &message.CommandAMF0{ ChunkStreamID: 3, Name: "connect", CommandID: 1, @@ -78,7 +73,7 @@ func TestInitializeClient(t *testing.T) { }, }, msg) - err = mrw.Write(&message.MsgCommandAMF0{ + err = mrw.Write(&message.CommandAMF0{ ChunkStreamID: 3, Name: "_result", CommandID: 1, @@ -100,7 +95,7 @@ func TestInitializeClient(t *testing.T) { if ca == "read" { msg, err = mrw.Read() require.NoError(t, err) - require.Equal(t, &message.MsgCommandAMF0{ + require.Equal(t, &message.CommandAMF0{ ChunkStreamID: 3, Name: "createStream", CommandID: 2, @@ -109,7 +104,7 @@ func TestInitializeClient(t *testing.T) { }, }, msg) - err = mrw.Write(&message.MsgCommandAMF0{ + err = mrw.Write(&message.CommandAMF0{ ChunkStreamID: 3, Name: "_result", CommandID: 2, @@ -122,13 +117,13 @@ func TestInitializeClient(t *testing.T) { msg, err = mrw.Read() require.NoError(t, err) - require.Equal(t, &message.MsgUserControlSetBufferLength{ + require.Equal(t, &message.UserControlSetBufferLength{ BufferLength: 0x64, }, msg) msg, err = mrw.Read() require.NoError(t, err) - require.Equal(t, &message.MsgCommandAMF0{ + require.Equal(t, &message.CommandAMF0{ ChunkStreamID: 4, MessageStreamID: 0x1000000, Name: "play", @@ -139,7 +134,7 @@ func TestInitializeClient(t *testing.T) { }, }, msg) - err = mrw.Write(&message.MsgCommandAMF0{ + err = mrw.Write(&message.CommandAMF0{ ChunkStreamID: 5, MessageStreamID: 0x1000000, Name: "onStatus", @@ -157,7 +152,7 @@ func TestInitializeClient(t *testing.T) { } else { msg, err = mrw.Read() require.NoError(t, err) - require.Equal(t, &message.MsgCommandAMF0{ + require.Equal(t, &message.CommandAMF0{ ChunkStreamID: 3, Name: "releaseStream", CommandID: 2, @@ -169,7 +164,7 @@ func TestInitializeClient(t *testing.T) { msg, err = mrw.Read() require.NoError(t, err) - require.Equal(t, &message.MsgCommandAMF0{ + require.Equal(t, &message.CommandAMF0{ ChunkStreamID: 3, Name: "FCPublish", CommandID: 3, @@ -181,7 +176,7 @@ func TestInitializeClient(t *testing.T) { msg, err = mrw.Read() require.NoError(t, err) - require.Equal(t, &message.MsgCommandAMF0{ + require.Equal(t, &message.CommandAMF0{ ChunkStreamID: 3, Name: "createStream", CommandID: 4, @@ -190,7 +185,7 @@ func TestInitializeClient(t *testing.T) { }, }, msg) - err = mrw.Write(&message.MsgCommandAMF0{ + err = mrw.Write(&message.CommandAMF0{ ChunkStreamID: 3, Name: "_result", CommandID: 4, @@ -203,7 +198,7 @@ func TestInitializeClient(t *testing.T) { msg, err = mrw.Read() require.NoError(t, err) - require.Equal(t, &message.MsgCommandAMF0{ + require.Equal(t, &message.CommandAMF0{ ChunkStreamID: 4, MessageStreamID: 0x1000000, Name: "publish", @@ -215,7 +210,7 @@ func TestInitializeClient(t *testing.T) { }, }, msg) - err = mrw.Write(&message.MsgCommandAMF0{ + err = mrw.Write(&message.CommandAMF0{ ChunkStreamID: 5, MessageStreamID: 0x1000000, Name: "onStatus", @@ -305,7 +300,7 @@ func TestInitializeServer(t *testing.T) { tcURL = "'rtmp://127.0.0.1:9121/stream" } - err = mrw.Write(&message.MsgCommandAMF0{ + err = mrw.Write(&message.CommandAMF0{ ChunkStreamID: 3, Name: "connect", CommandID: 1, @@ -326,26 +321,26 @@ func TestInitializeServer(t *testing.T) { msg, err := mrw.Read() require.NoError(t, err) - require.Equal(t, &message.MsgSetWindowAckSize{ + require.Equal(t, &message.SetWindowAckSize{ Value: 2500000, }, msg) msg, err = mrw.Read() require.NoError(t, err) - require.Equal(t, &message.MsgSetPeerBandwidth{ + require.Equal(t, &message.SetPeerBandwidth{ Value: 2500000, Type: 2, }, msg) msg, err = mrw.Read() require.NoError(t, err) - require.Equal(t, &message.MsgSetChunkSize{ + require.Equal(t, &message.SetChunkSize{ Value: 65536, }, msg) msg, err = mrw.Read() require.NoError(t, err) - require.Equal(t, &message.MsgCommandAMF0{ + require.Equal(t, &message.CommandAMF0{ ChunkStreamID: 3, Name: "_result", CommandID: 1, @@ -363,13 +358,13 @@ func TestInitializeServer(t *testing.T) { }, }, msg) - err = mrw.Write(&message.MsgSetChunkSize{ + err = mrw.Write(&message.SetChunkSize{ Value: 65536, }) require.NoError(t, err) if ca == "read" { - err = mrw.Write(&message.MsgCommandAMF0{ + err = mrw.Write(&message.CommandAMF0{ ChunkStreamID: 3, Name: "createStream", CommandID: 2, @@ -381,7 +376,7 @@ func TestInitializeServer(t *testing.T) { msg, err = mrw.Read() require.NoError(t, err) - require.Equal(t, &message.MsgCommandAMF0{ + require.Equal(t, &message.CommandAMF0{ ChunkStreamID: 3, Name: "_result", CommandID: 2, @@ -391,12 +386,12 @@ func TestInitializeServer(t *testing.T) { }, }, msg) - err = mrw.Write(&message.MsgUserControlSetBufferLength{ + err = mrw.Write(&message.UserControlSetBufferLength{ BufferLength: 0x64, }) require.NoError(t, err) - err = mrw.Write(&message.MsgCommandAMF0{ + err = mrw.Write(&message.CommandAMF0{ ChunkStreamID: 4, MessageStreamID: 0x1000000, Name: "play", @@ -408,7 +403,7 @@ func TestInitializeServer(t *testing.T) { }) require.NoError(t, err) } else { - err = mrw.Write(&message.MsgCommandAMF0{ + err = mrw.Write(&message.CommandAMF0{ ChunkStreamID: 3, Name: "releaseStream", CommandID: 2, @@ -419,7 +414,7 @@ func TestInitializeServer(t *testing.T) { }) require.NoError(t, err) - err = mrw.Write(&message.MsgCommandAMF0{ + err = mrw.Write(&message.CommandAMF0{ ChunkStreamID: 3, Name: "FCPublish", CommandID: 3, @@ -430,7 +425,7 @@ func TestInitializeServer(t *testing.T) { }) require.NoError(t, err) - err = mrw.Write(&message.MsgCommandAMF0{ + err = mrw.Write(&message.CommandAMF0{ ChunkStreamID: 3, Name: "createStream", CommandID: 4, @@ -442,7 +437,7 @@ func TestInitializeServer(t *testing.T) { msg, err = mrw.Read() require.NoError(t, err) - require.Equal(t, &message.MsgCommandAMF0{ + require.Equal(t, &message.CommandAMF0{ ChunkStreamID: 3, Name: "_result", CommandID: 4, @@ -452,7 +447,7 @@ func TestInitializeServer(t *testing.T) { }, }, msg) - err = mrw.Write(&message.MsgCommandAMF0{ + err = mrw.Write(&message.CommandAMF0{ ChunkStreamID: 4, MessageStreamID: 0x1000000, Name: "publish", @@ -471,931 +466,6 @@ func TestInitializeServer(t *testing.T) { } } -func TestReadTracks(t *testing.T) { - sps := []byte{ - 0x67, 0x64, 0x00, 0x0c, 0xac, 0x3b, 0x50, 0xb0, - 0x4b, 0x42, 0x00, 0x00, 0x03, 0x00, 0x02, 0x00, - 0x00, 0x03, 0x00, 0x3d, 0x08, - } - - pps := []byte{ - 0x68, 0xee, 0x3c, 0x80, - } - - for _, ca := range []struct { - name string - videoTrack formats.Format - audioTrack formats.Format - }{ - { - "video+audio", - &formats.H264{ - PayloadTyp: 96, - SPS: sps, - PPS: pps, - PacketizationMode: 1, - }, - &formats.MPEG4Audio{ - PayloadTyp: 96, - Config: &mpeg4audio.Config{ - Type: 2, - SampleRate: 44100, - ChannelCount: 2, - }, - SizeLength: 13, - IndexLength: 3, - IndexDeltaLength: 3, - }, - }, - { - "video", - &formats.H264{ - PayloadTyp: 96, - SPS: sps, - PPS: pps, - PacketizationMode: 1, - }, - nil, - }, - { - "metadata without codec id", - &formats.H264{ - PayloadTyp: 96, - SPS: sps, - PPS: pps, - PacketizationMode: 1, - }, - &formats.MPEG4Audio{ - PayloadTyp: 96, - Config: &mpeg4audio.Config{ - Type: 2, - SampleRate: 44100, - ChannelCount: 2, - }, - SizeLength: 13, - IndexLength: 3, - IndexDeltaLength: 3, - }, - }, - { - "missing metadata, video+audio", - &formats.H264{ - PayloadTyp: 96, - SPS: sps, - PPS: pps, - PacketizationMode: 1, - }, - &formats.MPEG4Audio{ - PayloadTyp: 96, - Config: &mpeg4audio.Config{ - Type: 2, - SampleRate: 44100, - ChannelCount: 2, - }, - SizeLength: 13, - IndexLength: 3, - IndexDeltaLength: 3, - }, - }, - { - "missing metadata, audio", - nil, - &formats.MPEG4Audio{ - PayloadTyp: 96, - Config: &mpeg4audio.Config{ - Type: 2, - SampleRate: 44100, - ChannelCount: 2, - }, - SizeLength: 13, - IndexLength: 3, - IndexDeltaLength: 3, - }, - }, - { - "obs studio h265", - &formats.H265{ - PayloadTyp: 96, - VPS: []byte{ - 0x40, 0x01, 0x0c, 0x01, 0xff, 0xff, 0x01, 0x40, - 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, - 0x03, 0x00, 0x00, 0x03, 0x00, 0x7b, 0xac, 0x09, - }, - SPS: []byte{ - 0x42, 0x01, 0x01, 0x01, 0x40, 0x00, 0x00, 0x03, - 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, - 0x03, 0x00, 0x7b, 0xa0, 0x03, 0xc0, 0x80, 0x11, - 0x07, 0xcb, 0x96, 0xb4, 0xa4, 0x25, 0x92, 0xe3, - 0x01, 0x6a, 0x02, 0x02, 0x02, 0x08, 0x00, 0x00, - 0x03, 0x00, 0x08, 0x00, 0x00, 0x03, 0x01, 0xe3, - 0x00, 0x2e, 0xf2, 0x88, 0x00, 0x09, 0x89, 0x60, - 0x00, 0x04, 0xc4, 0xb4, 0x20, - }, - PPS: []byte{ - 0x44, 0x01, 0xc0, 0xf7, 0xc0, 0xcc, 0x90, - }, - }, - &formats.MPEG4Audio{ - PayloadTyp: 96, - Config: &mpeg4audio.Config{ - Type: 2, - SampleRate: 44100, - ChannelCount: 2, - }, - SizeLength: 13, - IndexLength: 3, - IndexDeltaLength: 3, - }, - }, - } { - t.Run(ca.name, func(t *testing.T) { - ln, err := net.Listen("tcp", "127.0.0.1:9121") - require.NoError(t, err) - defer ln.Close() - - done := make(chan struct{}) - - go func() { - conn, err := ln.Accept() - require.NoError(t, err) - defer conn.Close() - - rconn := NewConn(conn) - _, _, err = rconn.InitializeServer() - require.NoError(t, err) - - videoTrack, audioTrack, err := rconn.ReadTracks() - require.NoError(t, err) - require.Equal(t, ca.videoTrack, videoTrack) - require.Equal(t, ca.audioTrack, audioTrack) - close(done) - }() - - conn, err := net.Dial("tcp", "127.0.0.1:9121") - require.NoError(t, err) - defer conn.Close() - bc := bytecounter.NewReadWriter(conn) - - err = handshake.DoClient(bc, true) - require.NoError(t, err) - - mrw := message.NewReadWriter(bc, true) - - err = mrw.Write(&message.MsgCommandAMF0{ - ChunkStreamID: 3, - Name: "connect", - CommandID: 1, - Arguments: []interface{}{ - flvio.AMFMap{ - {K: "app", V: "/stream"}, - {K: "flashVer", V: "LNX 9,0,124,2"}, - {K: "tcUrl", V: "rtmp://127.0.0.1:9121/stream"}, - {K: "fpad", V: false}, - {K: "capabilities", V: 15}, - {K: "audioCodecs", V: 4071}, - {K: "videoCodecs", V: 252}, - {K: "videoFunction", V: 1}, - }, - }, - }) - require.NoError(t, err) - - msg, err := mrw.Read() - require.NoError(t, err) - require.Equal(t, &message.MsgSetWindowAckSize{ - Value: 2500000, - }, msg) - - msg, err = mrw.Read() - require.NoError(t, err) - require.Equal(t, &message.MsgSetPeerBandwidth{ - Value: 2500000, - Type: 2, - }, msg) - - msg, err = mrw.Read() - require.NoError(t, err) - require.Equal(t, &message.MsgSetChunkSize{ - Value: 65536, - }, msg) - - msg, err = mrw.Read() - require.NoError(t, err) - require.Equal(t, &message.MsgCommandAMF0{ - ChunkStreamID: 3, - Name: "_result", - CommandID: 1, - Arguments: []interface{}{ - flvio.AMFMap{ - {K: "fmsVer", V: "LNX 9,0,124,2"}, - {K: "capabilities", V: float64(31)}, - }, - flvio.AMFMap{ - {K: "level", V: "status"}, - {K: "code", V: "NetConnection.Connect.Success"}, - {K: "description", V: "Connection succeeded."}, - {K: "objectEncoding", V: float64(0)}, - }, - }, - }, msg) - - err = mrw.Write(&message.MsgSetChunkSize{ - Value: 65536, - }) - require.NoError(t, err) - - err = mrw.Write(&message.MsgCommandAMF0{ - ChunkStreamID: 3, - Name: "releaseStream", - CommandID: 2, - Arguments: []interface{}{ - nil, - "", - }, - }) - require.NoError(t, err) - - err = mrw.Write(&message.MsgCommandAMF0{ - ChunkStreamID: 3, - Name: "FCPublish", - CommandID: 3, - Arguments: []interface{}{ - nil, - "", - }, - }) - require.NoError(t, err) - - err = mrw.Write(&message.MsgCommandAMF0{ - ChunkStreamID: 3, - Name: "createStream", - CommandID: 4, - Arguments: []interface{}{ - nil, - }, - }) - require.NoError(t, err) - - msg, err = mrw.Read() - require.NoError(t, err) - require.Equal(t, &message.MsgCommandAMF0{ - ChunkStreamID: 3, - Name: "_result", - CommandID: 4, - Arguments: []interface{}{ - nil, - float64(1), - }, - }, msg) - - err = mrw.Write(&message.MsgCommandAMF0{ - ChunkStreamID: 8, - MessageStreamID: 1, - Name: "publish", - CommandID: 5, - Arguments: []interface{}{ - nil, - "", - "live", - }, - }) - require.NoError(t, err) - - msg, err = mrw.Read() - require.NoError(t, err) - require.Equal(t, &message.MsgCommandAMF0{ - ChunkStreamID: 5, - MessageStreamID: 0x1000000, - Name: "onStatus", - CommandID: 5, - Arguments: []interface{}{ - nil, - flvio.AMFMap{ - {K: "level", V: "status"}, - {K: "code", V: "NetStream.Publish.Start"}, - {K: "description", V: "publish start"}, - }, - }, - }, msg) - - switch ca.name { - case "video+audio": - err = mrw.Write(&message.MsgDataAMF0{ - ChunkStreamID: 4, - MessageStreamID: 1, - Payload: []interface{}{ - "@setDataFrame", - "onMetaData", - flvio.AMFMap{ - { - K: "videodatarate", - V: float64(0), - }, - { - K: "videocodecid", - V: float64(message.CodecH264), - }, - { - K: "audiodatarate", - V: float64(0), - }, - { - K: "audiocodecid", - V: float64(message.CodecMPEG4Audio), - }, - }, - }, - }) - require.NoError(t, err) - - buf, _ := h264conf.Conf{ - SPS: sps, - PPS: pps, - }.Marshal() - - err = mrw.Write(&message.MsgVideo{ - ChunkStreamID: message.MsgVideoChunkStreamID, - MessageStreamID: 0x1000000, - Codec: message.CodecH264, - IsKeyFrame: true, - Type: message.MsgVideoTypeConfig, - Payload: buf, - }) - require.NoError(t, err) - - enc, err := mpeg4audio.Config{ - Type: 2, - SampleRate: 44100, - ChannelCount: 2, - }.Marshal() - require.NoError(t, err) - - err = mrw.Write(&message.MsgAudio{ - ChunkStreamID: message.MsgAudioChunkStreamID, - MessageStreamID: 0x1000000, - Codec: message.CodecMPEG4Audio, - Rate: flvio.SOUND_44Khz, - Depth: flvio.SOUND_16BIT, - Channels: flvio.SOUND_STEREO, - AACType: message.MsgAudioAACTypeConfig, - Payload: enc, - }) - require.NoError(t, err) - - case "video": - err = mrw.Write(&message.MsgDataAMF0{ - ChunkStreamID: 4, - MessageStreamID: 1, - Payload: []interface{}{ - "@setDataFrame", - "onMetaData", - flvio.AMFMap{ - { - K: "videodatarate", - V: float64(0), - }, - { - K: "videocodecid", - V: float64(message.CodecH264), - }, - { - K: "audiodatarate", - V: float64(0), - }, - { - K: "audiocodecid", - V: float64(0), - }, - }, - }, - }) - require.NoError(t, err) - - buf, _ := h264conf.Conf{ - SPS: sps, - PPS: pps, - }.Marshal() - - err = mrw.Write(&message.MsgVideo{ - ChunkStreamID: message.MsgVideoChunkStreamID, - MessageStreamID: 0x1000000, - Codec: message.CodecH264, - IsKeyFrame: true, - Type: message.MsgVideoTypeConfig, - Payload: buf, - }) - require.NoError(t, err) - - case "metadata without codec id": - err = mrw.Write(&message.MsgDataAMF0{ - ChunkStreamID: 4, - MessageStreamID: 1, - Payload: []interface{}{ - "@setDataFrame", - "onMetaData", - flvio.AMFMap{ - { - K: "width", - V: float64(2688), - }, - { - K: "height", - V: float64(1520), - }, - { - K: "framerate", - V: float64(0o25), - }, - }, - }, - }) - require.NoError(t, err) - - buf, _ := h264conf.Conf{ - SPS: sps, - PPS: pps, - }.Marshal() - - err = mrw.Write(&message.MsgVideo{ - ChunkStreamID: message.MsgVideoChunkStreamID, - MessageStreamID: 0x1000000, - Codec: message.CodecH264, - IsKeyFrame: true, - Type: message.MsgVideoTypeConfig, - Payload: buf, - }) - require.NoError(t, err) - - enc, err := mpeg4audio.Config{ - Type: 2, - SampleRate: 44100, - ChannelCount: 2, - }.Marshal() - require.NoError(t, err) - - err = mrw.Write(&message.MsgAudio{ - ChunkStreamID: message.MsgAudioChunkStreamID, - MessageStreamID: 0x1000000, - Codec: message.CodecMPEG4Audio, - Rate: flvio.SOUND_44Khz, - Depth: flvio.SOUND_16BIT, - Channels: flvio.SOUND_STEREO, - AACType: message.MsgAudioAACTypeConfig, - Payload: enc, - }) - require.NoError(t, err) - - case "missing metadata, video+audio": - buf, _ := h264conf.Conf{ - SPS: sps, - PPS: pps, - }.Marshal() - - err = mrw.Write(&message.MsgVideo{ - ChunkStreamID: message.MsgVideoChunkStreamID, - MessageStreamID: 0x1000000, - Codec: message.CodecH264, - IsKeyFrame: true, - Type: message.MsgVideoTypeConfig, - Payload: buf, - }) - require.NoError(t, err) - - enc, err := mpeg4audio.Config{ - Type: 2, - SampleRate: 44100, - ChannelCount: 2, - }.Marshal() - require.NoError(t, err) - - err = mrw.Write(&message.MsgAudio{ - ChunkStreamID: message.MsgAudioChunkStreamID, - MessageStreamID: 0x1000000, - Codec: message.CodecMPEG4Audio, - Rate: flvio.SOUND_44Khz, - Depth: flvio.SOUND_16BIT, - Channels: flvio.SOUND_STEREO, - AACType: message.MsgAudioAACTypeConfig, - Payload: enc, - }) - require.NoError(t, err) - - case "missing metadata, audio": - enc, err := mpeg4audio.Config{ - Type: 2, - SampleRate: 44100, - ChannelCount: 2, - }.Marshal() - require.NoError(t, err) - - err = mrw.Write(&message.MsgAudio{ - ChunkStreamID: message.MsgAudioChunkStreamID, - MessageStreamID: 0x1000000, - Codec: message.CodecMPEG4Audio, - Rate: flvio.SOUND_44Khz, - Depth: flvio.SOUND_16BIT, - Channels: flvio.SOUND_STEREO, - AACType: message.MsgAudioAACTypeConfig, - Payload: enc, - }) - require.NoError(t, err) - - err = mrw.Write(&message.MsgAudio{ - ChunkStreamID: message.MsgAudioChunkStreamID, - MessageStreamID: 0x1000000, - Codec: message.CodecMPEG4Audio, - Rate: flvio.SOUND_44Khz, - Depth: flvio.SOUND_16BIT, - Channels: flvio.SOUND_STEREO, - AACType: message.MsgAudioAACTypeConfig, - Payload: enc, - DTS: 1 * time.Second, - }) - require.NoError(t, err) - - case "obs studio h265": - err = mrw.Write(&message.MsgDataAMF0{ - ChunkStreamID: 4, - MessageStreamID: 1, - Payload: []interface{}{ - "@setDataFrame", - "onMetaData", - flvio.AMFMap{ - { - K: "videodatarate", - V: float64(0), - }, - { - K: "videocodecid", - V: float64(message.CodecH264), - }, - { - K: "audiodatarate", - V: float64(0), - }, - { - K: "audiocodecid", - V: float64(message.CodecMPEG4Audio), - }, - }, - }, - }) - require.NoError(t, err) - - avcc, err := h264.AVCCMarshal([][]byte{ - { // VPS - 0x40, 0x01, 0x0c, 0x01, 0xff, 0xff, 0x01, 0x40, - 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, - 0x03, 0x00, 0x00, 0x03, 0x00, 0x7b, 0xac, 0x09, - }, - { // SPS - 0x42, 0x01, 0x01, 0x01, 0x40, 0x00, 0x00, 0x03, - 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, - 0x03, 0x00, 0x7b, 0xa0, 0x03, 0xc0, 0x80, 0x11, - 0x07, 0xcb, 0x96, 0xb4, 0xa4, 0x25, 0x92, 0xe3, - 0x01, 0x6a, 0x02, 0x02, 0x02, 0x08, 0x00, 0x00, - 0x03, 0x00, 0x08, 0x00, 0x00, 0x03, 0x01, 0xe3, - 0x00, 0x2e, 0xf2, 0x88, 0x00, 0x09, 0x89, 0x60, - 0x00, 0x04, 0xc4, 0xb4, 0x20, - }, - { - // PPS - 0x44, 0x01, 0xc0, 0xf7, 0xc0, 0xcc, 0x90, - }, - }) - require.NoError(t, err) - - err = mrw.Write(&message.MsgVideo{ - ChunkStreamID: message.MsgVideoChunkStreamID, - MessageStreamID: 0x1000000, - Codec: message.CodecH264, - IsKeyFrame: true, - Type: message.MsgVideoTypeAU, - Payload: avcc, - }) - require.NoError(t, err) - - enc, err := mpeg4audio.Config{ - Type: 2, - SampleRate: 44100, - ChannelCount: 2, - }.Marshal() - require.NoError(t, err) - - err = mrw.Write(&message.MsgAudio{ - ChunkStreamID: message.MsgAudioChunkStreamID, - MessageStreamID: 0x1000000, - Codec: message.CodecMPEG4Audio, - Rate: flvio.SOUND_44Khz, - Depth: flvio.SOUND_16BIT, - Channels: flvio.SOUND_STEREO, - AACType: message.MsgAudioAACTypeConfig, - Payload: enc, - }) - require.NoError(t, err) - } - - <-done - }) - } -} - -func TestWriteTracks(t *testing.T) { - ln, err := net.Listen("tcp", "127.0.0.1:9121") - require.NoError(t, err) - defer ln.Close() - - go func() { - conn, err := ln.Accept() - require.NoError(t, err) - defer conn.Close() - - rconn := NewConn(conn) - _, _, err = rconn.InitializeServer() - require.NoError(t, err) - - videoTrack := &formats.H264{ - PayloadTyp: 96, - SPS: []byte{ - 0x67, 0x64, 0x00, 0x0c, 0xac, 0x3b, 0x50, 0xb0, - 0x4b, 0x42, 0x00, 0x00, 0x03, 0x00, 0x02, 0x00, - 0x00, 0x03, 0x00, 0x3d, 0x08, - }, - PPS: []byte{ - 0x68, 0xee, 0x3c, 0x80, - }, - PacketizationMode: 1, - } - - audioTrack := &formats.MPEG4Audio{ - PayloadTyp: 96, - Config: &mpeg4audio.Config{ - Type: 2, - SampleRate: 44100, - ChannelCount: 2, - }, - SizeLength: 13, - IndexLength: 3, - IndexDeltaLength: 3, - } - - err = rconn.WriteTracks(videoTrack, audioTrack) - require.NoError(t, err) - }() - - conn, err := net.Dial("tcp", "127.0.0.1:9121") - require.NoError(t, err) - defer conn.Close() - bc := bytecounter.NewReadWriter(conn) - - err = handshake.DoClient(bc, true) - require.NoError(t, err) - - mrw := message.NewReadWriter(bc, true) - - err = mrw.Write(&message.MsgCommandAMF0{ - ChunkStreamID: 3, - Name: "connect", - CommandID: 1, - Arguments: []interface{}{ - flvio.AMFMap{ - {K: "app", V: "/stream"}, - {K: "flashVer", V: "LNX 9,0,124,2"}, - {K: "tcUrl", V: "rtmp://127.0.0.1:9121/stream"}, - {K: "fpad", V: false}, - {K: "capabilities", V: 15}, - {K: "audioCodecs", V: 4071}, - {K: "videoCodecs", V: 252}, - {K: "videoFunction", V: 1}, - }, - }, - }) - require.NoError(t, err) - - msg, err := mrw.Read() - require.NoError(t, err) - require.Equal(t, &message.MsgSetWindowAckSize{ - Value: 2500000, - }, msg) - - msg, err = mrw.Read() - require.NoError(t, err) - require.Equal(t, &message.MsgSetPeerBandwidth{ - Value: 2500000, - Type: 2, - }, msg) - - msg, err = mrw.Read() - require.NoError(t, err) - require.Equal(t, &message.MsgSetChunkSize{ - Value: 65536, - }, msg) - - msg, err = mrw.Read() - require.NoError(t, err) - require.Equal(t, &message.MsgCommandAMF0{ - ChunkStreamID: 3, - Name: "_result", - CommandID: 1, - Arguments: []interface{}{ - flvio.AMFMap{ - {K: "fmsVer", V: "LNX 9,0,124,2"}, - {K: "capabilities", V: float64(31)}, - }, - flvio.AMFMap{ - {K: "level", V: "status"}, - {K: "code", V: "NetConnection.Connect.Success"}, - {K: "description", V: "Connection succeeded."}, - {K: "objectEncoding", V: float64(0)}, - }, - }, - }, msg) - - err = mrw.Write(&message.MsgSetWindowAckSize{ - Value: 2500000, - }) - require.NoError(t, err) - - err = mrw.Write(&message.MsgSetChunkSize{ - Value: 65536, - }) - require.NoError(t, err) - - err = mrw.Write(&message.MsgCommandAMF0{ - ChunkStreamID: 3, - Name: "createStream", - CommandID: 2, - Arguments: []interface{}{ - nil, - }, - }) - require.NoError(t, err) - - msg, err = mrw.Read() - require.NoError(t, err) - require.Equal(t, &message.MsgCommandAMF0{ - ChunkStreamID: 3, - Name: "_result", - CommandID: 2, - Arguments: []interface{}{ - nil, - float64(1), - }, - }, msg) - - err = mrw.Write(&message.MsgCommandAMF0{ - ChunkStreamID: 8, - Name: "getStreamLength", - CommandID: 3, - Arguments: []interface{}{ - nil, - "", - }, - }) - require.NoError(t, err) - - err = mrw.Write(&message.MsgCommandAMF0{ - ChunkStreamID: 8, - MessageStreamID: 0x1000000, - Name: "play", - CommandID: 4, - Arguments: []interface{}{ - nil, - "", - float64(-2000), - }, - }) - require.NoError(t, err) - - msg, err = mrw.Read() - require.NoError(t, err) - require.Equal(t, &message.MsgUserControlStreamIsRecorded{ - StreamID: 1, - }, msg) - - msg, err = mrw.Read() - require.NoError(t, err) - require.Equal(t, &message.MsgUserControlStreamBegin{ - StreamID: 1, - }, msg) - - msg, err = mrw.Read() - require.NoError(t, err) - require.Equal(t, &message.MsgCommandAMF0{ - ChunkStreamID: 5, - MessageStreamID: 0x1000000, - Name: "onStatus", - CommandID: 4, - Arguments: []interface{}{ - nil, - flvio.AMFMap{ - {K: "level", V: "status"}, - {K: "code", V: "NetStream.Play.Reset"}, - {K: "description", V: "play reset"}, - }, - }, - }, msg) - - msg, err = mrw.Read() - require.NoError(t, err) - require.Equal(t, &message.MsgCommandAMF0{ - ChunkStreamID: 5, - MessageStreamID: 0x1000000, - Name: "onStatus", - CommandID: 4, - Arguments: []interface{}{ - nil, - flvio.AMFMap{ - {K: "level", V: "status"}, - {K: "code", V: "NetStream.Play.Start"}, - {K: "description", V: "play start"}, - }, - }, - }, msg) - - msg, err = mrw.Read() - require.NoError(t, err) - require.Equal(t, &message.MsgCommandAMF0{ - ChunkStreamID: 5, - MessageStreamID: 0x1000000, - Name: "onStatus", - CommandID: 4, - Arguments: []interface{}{ - nil, - flvio.AMFMap{ - {K: "level", V: "status"}, - {K: "code", V: "NetStream.Data.Start"}, - {K: "description", V: "data start"}, - }, - }, - }, msg) - - msg, err = mrw.Read() - require.NoError(t, err) - require.Equal(t, &message.MsgCommandAMF0{ - ChunkStreamID: 5, - MessageStreamID: 0x1000000, - Name: "onStatus", - CommandID: 4, - Arguments: []interface{}{ - nil, - flvio.AMFMap{ - {K: "level", V: "status"}, - {K: "code", V: "NetStream.Play.PublishNotify"}, - {K: "description", V: "publish notify"}, - }, - }, - }, msg) - - msg, err = mrw.Read() - require.NoError(t, err) - require.Equal(t, &message.MsgDataAMF0{ - ChunkStreamID: 4, - MessageStreamID: 0x1000000, - Payload: []interface{}{ - "@setDataFrame", - "onMetaData", - flvio.AMFMap{ - {K: "videodatarate", V: float64(0)}, - {K: "videocodecid", V: float64(7)}, - {K: "audiodatarate", V: float64(0)}, - {K: "audiocodecid", V: float64(10)}, - }, - }, - }, msg) - - msg, err = mrw.Read() - require.NoError(t, err) - require.Equal(t, &message.MsgVideo{ - ChunkStreamID: message.MsgVideoChunkStreamID, - MessageStreamID: 0x1000000, - Codec: message.CodecH264, - IsKeyFrame: true, - Type: message.MsgVideoTypeConfig, - Payload: []byte{ - 0x1, 0x64, 0x0, - 0xc, 0xff, 0xe1, 0x0, 0x15, 0x67, 0x64, 0x0, - 0xc, 0xac, 0x3b, 0x50, 0xb0, 0x4b, 0x42, 0x0, - 0x0, 0x3, 0x0, 0x2, 0x0, 0x0, 0x3, 0x0, - 0x3d, 0x8, 0x1, 0x0, 0x4, 0x68, 0xee, 0x3c, - 0x80, - }, - }, msg) - - msg, err = mrw.Read() - require.NoError(t, err) - require.Equal(t, &message.MsgAudio{ - ChunkStreamID: message.MsgAudioChunkStreamID, - MessageStreamID: 0x1000000, - Codec: message.CodecMPEG4Audio, - Rate: flvio.SOUND_44Khz, - Depth: flvio.SOUND_16BIT, - Channels: flvio.SOUND_STEREO, - AACType: message.MsgAudioAACTypeConfig, - Payload: []byte{0x12, 0x10}, - }, msg) -} - func BenchmarkRead(b *testing.B) { var buf bytes.Buffer diff --git a/internal/rtmp/message/msg_acknowledge.go b/internal/rtmp/message/acknowledge.go similarity index 70% rename from internal/rtmp/message/msg_acknowledge.go rename to internal/rtmp/message/acknowledge.go index 11f5db10..cae52527 100644 --- a/internal/rtmp/message/msg_acknowledge.go +++ b/internal/rtmp/message/acknowledge.go @@ -3,17 +3,16 @@ package message //nolint:dupl import ( "fmt" - "github.com/aler9/mediamtx/internal/rtmp/chunk" "github.com/aler9/mediamtx/internal/rtmp/rawmessage" ) -// MsgAcknowledge is an acknowledgement message. -type MsgAcknowledge struct { +// Acknowledge is an acknowledgement message. +type Acknowledge struct { Value uint32 } // Unmarshal implements Message. -func (m *MsgAcknowledge) Unmarshal(raw *rawmessage.Message) error { +func (m *Acknowledge) Unmarshal(raw *rawmessage.Message) error { if raw.ChunkStreamID != ControlChunkStreamID { return fmt.Errorf("unexpected chunk stream ID") } @@ -28,7 +27,7 @@ func (m *MsgAcknowledge) Unmarshal(raw *rawmessage.Message) error { } // Marshal implements Message. -func (m *MsgAcknowledge) Marshal() (*rawmessage.Message, error) { +func (m *Acknowledge) Marshal() (*rawmessage.Message, error) { buf := make([]byte, 4) buf[0] = byte(m.Value >> 24) @@ -38,7 +37,7 @@ func (m *MsgAcknowledge) Marshal() (*rawmessage.Message, error) { return &rawmessage.Message{ ChunkStreamID: ControlChunkStreamID, - Type: chunk.MessageTypeAcknowledge, + Type: uint8(TypeAcknowledge), Body: buf, }, nil } diff --git a/internal/rtmp/message/msg_audio.go b/internal/rtmp/message/audio.go similarity index 68% rename from internal/rtmp/message/msg_audio.go rename to internal/rtmp/message/audio.go index b5c8bc45..91bcd462 100644 --- a/internal/rtmp/message/msg_audio.go +++ b/internal/rtmp/message/audio.go @@ -4,13 +4,12 @@ import ( "fmt" "time" - "github.com/aler9/mediamtx/internal/rtmp/chunk" "github.com/aler9/mediamtx/internal/rtmp/rawmessage" ) const ( - // MsgAudioChunkStreamID is the chunk stream ID that is usually used to send MsgAudio{} - MsgAudioChunkStreamID = 4 + // AudioChunkStreamID is the chunk stream ID that is usually used to send Audio{} + AudioChunkStreamID = 4 ) // supported audio codecs @@ -19,17 +18,17 @@ const ( CodecMPEG4Audio = 10 ) -// MsgAudioAACType is the AAC type of a MsgAudio. -type MsgAudioAACType uint8 +// AudioAACType is the AAC type of a Audio. +type AudioAACType uint8 -// MsgAudioAACType values. +// AudioAACType values. const ( - MsgAudioAACTypeConfig MsgAudioAACType = 0 - MsgAudioAACTypeAU MsgAudioAACType = 1 + AudioAACTypeConfig AudioAACType = 0 + AudioAACTypeAU AudioAACType = 1 ) -// MsgAudio is an audio message. -type MsgAudio struct { +// Audio is an audio message. +type Audio struct { ChunkStreamID byte DTS time.Duration MessageStreamID uint32 @@ -37,12 +36,12 @@ type MsgAudio struct { Rate uint8 Depth uint8 Channels uint8 - AACType MsgAudioAACType // only for CodecMPEG4Audio + AACType AudioAACType // only for CodecMPEG4Audio Payload []byte } // Unmarshal implements Message. -func (m *MsgAudio) Unmarshal(raw *rawmessage.Message) error { +func (m *Audio) Unmarshal(raw *rawmessage.Message) error { m.ChunkStreamID = raw.ChunkStreamID m.DTS = raw.Timestamp m.MessageStreamID = raw.MessageStreamID @@ -65,9 +64,9 @@ func (m *MsgAudio) Unmarshal(raw *rawmessage.Message) error { if m.Codec == CodecMPEG2Audio { m.Payload = raw.Body[1:] } else { - m.AACType = MsgAudioAACType(raw.Body[1]) + m.AACType = AudioAACType(raw.Body[1]) switch m.AACType { - case MsgAudioAACTypeConfig, MsgAudioAACTypeAU: + case AudioAACTypeConfig, AudioAACTypeAU: default: return fmt.Errorf("unsupported audio message type: %d", m.AACType) } @@ -79,7 +78,7 @@ func (m *MsgAudio) Unmarshal(raw *rawmessage.Message) error { } // Marshal implements Message. -func (m MsgAudio) Marshal() (*rawmessage.Message, error) { +func (m Audio) Marshal() (*rawmessage.Message, error) { var l int if m.Codec == CodecMPEG2Audio { l = 1 + len(m.Payload) @@ -100,7 +99,7 @@ func (m MsgAudio) Marshal() (*rawmessage.Message, error) { return &rawmessage.Message{ ChunkStreamID: m.ChunkStreamID, Timestamp: m.DTS, - Type: chunk.MessageTypeAudio, + Type: uint8(TypeAudio), MessageStreamID: m.MessageStreamID, Body: body, }, nil diff --git a/internal/rtmp/message/msg_command_amf0.go b/internal/rtmp/message/command_amf0.go similarity index 77% rename from internal/rtmp/message/msg_command_amf0.go rename to internal/rtmp/message/command_amf0.go index 65422d09..6e3663a0 100644 --- a/internal/rtmp/message/msg_command_amf0.go +++ b/internal/rtmp/message/command_amf0.go @@ -5,12 +5,11 @@ import ( "github.com/notedit/rtmp/format/flv/flvio" - "github.com/aler9/mediamtx/internal/rtmp/chunk" "github.com/aler9/mediamtx/internal/rtmp/rawmessage" ) -// MsgCommandAMF0 is a AMF0 command message. -type MsgCommandAMF0 struct { +// CommandAMF0 is a AMF0 command message. +type CommandAMF0 struct { ChunkStreamID byte MessageStreamID uint32 Name string @@ -19,7 +18,7 @@ type MsgCommandAMF0 struct { } // Unmarshal implements Message. -func (m *MsgCommandAMF0) Unmarshal(raw *rawmessage.Message) error { +func (m *CommandAMF0) Unmarshal(raw *rawmessage.Message) error { m.ChunkStreamID = raw.ChunkStreamID m.MessageStreamID = raw.MessageStreamID @@ -50,10 +49,10 @@ func (m *MsgCommandAMF0) Unmarshal(raw *rawmessage.Message) error { } // Marshal implements Message. -func (m MsgCommandAMF0) Marshal() (*rawmessage.Message, error) { +func (m CommandAMF0) Marshal() (*rawmessage.Message, error) { return &rawmessage.Message{ ChunkStreamID: m.ChunkStreamID, - Type: chunk.MessageTypeCommandAMF0, + Type: uint8(TypeCommandAMF0), MessageStreamID: m.MessageStreamID, Body: flvio.FillAMF0ValsMalloc(append([]interface{}{ m.Name, diff --git a/internal/rtmp/message/msg_data_amf0.go b/internal/rtmp/message/data_amf0.go similarity index 69% rename from internal/rtmp/message/msg_data_amf0.go rename to internal/rtmp/message/data_amf0.go index 16c13abc..ec438da7 100644 --- a/internal/rtmp/message/msg_data_amf0.go +++ b/internal/rtmp/message/data_amf0.go @@ -3,19 +3,18 @@ package message import ( "github.com/notedit/rtmp/format/flv/flvio" - "github.com/aler9/mediamtx/internal/rtmp/chunk" "github.com/aler9/mediamtx/internal/rtmp/rawmessage" ) -// MsgDataAMF0 is a AMF0 data message. -type MsgDataAMF0 struct { +// DataAMF0 is a AMF0 data message. +type DataAMF0 struct { ChunkStreamID byte MessageStreamID uint32 Payload []interface{} } // Unmarshal implements Message. -func (m *MsgDataAMF0) Unmarshal(raw *rawmessage.Message) error { +func (m *DataAMF0) Unmarshal(raw *rawmessage.Message) error { m.ChunkStreamID = raw.ChunkStreamID m.MessageStreamID = raw.MessageStreamID @@ -29,10 +28,10 @@ func (m *MsgDataAMF0) Unmarshal(raw *rawmessage.Message) error { } // Marshal implements Message. -func (m MsgDataAMF0) Marshal() (*rawmessage.Message, error) { +func (m DataAMF0) Marshal() (*rawmessage.Message, error) { return &rawmessage.Message{ ChunkStreamID: m.ChunkStreamID, - Type: chunk.MessageTypeDataAMF0, + Type: uint8(TypeDataAMF0), MessageStreamID: m.MessageStreamID, Body: flvio.FillAMF0ValsMalloc(m.Payload), }, nil diff --git a/internal/rtmp/message/extended_coded_frames.go b/internal/rtmp/message/extended_coded_frames.go new file mode 100644 index 00000000..d492ec12 --- /dev/null +++ b/internal/rtmp/message/extended_coded_frames.go @@ -0,0 +1,71 @@ +package message + +import ( + "fmt" + "time" + + "github.com/aler9/mediamtx/internal/rtmp/rawmessage" +) + +// ExtendedCodedFrames is a CodedFrames extended message. +type ExtendedCodedFrames struct { + ChunkStreamID byte + DTS time.Duration + MessageStreamID uint32 + FourCC [4]byte + PTSDelta time.Duration + Payload []byte +} + +// Unmarshal implements Message. +func (m *ExtendedCodedFrames) Unmarshal(raw *rawmessage.Message) error { + if len(raw.Body) < 8 { + return fmt.Errorf("not enough bytes") + } + + m.ChunkStreamID = raw.ChunkStreamID + m.DTS = raw.Timestamp + m.MessageStreamID = raw.MessageStreamID + copy(m.FourCC[:], raw.Body[1:5]) + + if m.FourCC == FourCCHEVC { + m.PTSDelta = time.Duration(uint32(raw.Body[5])<<16|uint32(raw.Body[6])<<8|uint32(raw.Body[7])) * time.Millisecond + m.Payload = raw.Body[8:] + } else { + m.Payload = raw.Body[5:] + } + + return nil +} + +// Marshal implements Message. +func (m ExtendedCodedFrames) Marshal() (*rawmessage.Message, error) { + var l int + if m.FourCC == FourCCHEVC { + l = 8 + len(m.Payload) + } else { + l = 5 + len(m.Payload) + } + body := make([]byte, l) + + body[0] = 0b10000000 | byte(ExtendedTypeCodedFrames) + copy(body[1:5], m.FourCC[:]) + + if m.FourCC == FourCCHEVC { + tmp := uint32(m.PTSDelta / time.Millisecond) + body[5] = uint8(tmp >> 16) + body[6] = uint8(tmp >> 8) + body[7] = uint8(tmp) + copy(body[8:], m.Payload) + } else { + copy(body[5:], m.Payload) + } + + return &rawmessage.Message{ + ChunkStreamID: m.ChunkStreamID, + Timestamp: m.DTS, + Type: uint8(TypeVideo), + MessageStreamID: m.MessageStreamID, + Body: body, + }, nil +} diff --git a/internal/rtmp/message/extended_frames_x.go b/internal/rtmp/message/extended_frames_x.go new file mode 100644 index 00000000..c2fdd20c --- /dev/null +++ b/internal/rtmp/message/extended_frames_x.go @@ -0,0 +1,44 @@ +package message + +import ( + "time" + + "github.com/aler9/mediamtx/internal/rtmp/rawmessage" +) + +// ExtendedFramesX is a FramesX extended message. +type ExtendedFramesX struct { + ChunkStreamID byte + DTS time.Duration + MessageStreamID uint32 + FourCC [4]byte + Payload []byte +} + +// Unmarshal implements Message. +func (m *ExtendedFramesX) Unmarshal(raw *rawmessage.Message) error { + m.ChunkStreamID = raw.ChunkStreamID + m.DTS = raw.Timestamp + m.MessageStreamID = raw.MessageStreamID + copy(m.FourCC[:], raw.Body[1:5]) + m.Payload = raw.Body[5:] + + return nil +} + +// Marshal implements Message. +func (m ExtendedFramesX) Marshal() (*rawmessage.Message, error) { + body := make([]byte, 5+len(m.Payload)) + + body[0] = 0b10000000 | byte(ExtendedTypeFramesX) + copy(body[1:5], m.FourCC[:]) + copy(body[5:], m.Payload) + + return &rawmessage.Message{ + ChunkStreamID: m.ChunkStreamID, + Timestamp: m.DTS, + Type: uint8(TypeVideo), + MessageStreamID: m.MessageStreamID, + Body: body, + }, nil +} diff --git a/internal/rtmp/message/extended_metadata.go b/internal/rtmp/message/extended_metadata.go new file mode 100644 index 00000000..bee56796 --- /dev/null +++ b/internal/rtmp/message/extended_metadata.go @@ -0,0 +1,24 @@ +package message + +import ( + "fmt" + + "github.com/aler9/mediamtx/internal/rtmp/rawmessage" +) + +// ExtendedMetadata is a metadata extended message. +type ExtendedMetadata struct { + FourCC [4]byte +} + +// Unmarshal implements Message. +func (m *ExtendedMetadata) Unmarshal(raw *rawmessage.Message) error { + copy(m.FourCC[:], raw.Body[1:5]) + + return fmt.Errorf("ExtendedMetadata is not implemented yet") +} + +// Marshal implements Message. +func (m ExtendedMetadata) Marshal() (*rawmessage.Message, error) { + return nil, fmt.Errorf("TODO") +} diff --git a/internal/rtmp/message/extended_mpeg2ts_sequence_start.go b/internal/rtmp/message/extended_mpeg2ts_sequence_start.go new file mode 100644 index 00000000..24af8f43 --- /dev/null +++ b/internal/rtmp/message/extended_mpeg2ts_sequence_start.go @@ -0,0 +1,24 @@ +package message + +import ( + "fmt" + + "github.com/aler9/mediamtx/internal/rtmp/rawmessage" +) + +// ExtendedMPEG2TSSequenceStart is a MPEG2-TS sequence start extended message. +type ExtendedMPEG2TSSequenceStart struct { + FourCC [4]byte +} + +// Unmarshal implements Message. +func (m *ExtendedMPEG2TSSequenceStart) Unmarshal(raw *rawmessage.Message) error { + copy(m.FourCC[:], raw.Body[1:5]) + + return fmt.Errorf("ExtendedMPEG2TSSequenceStart is not implemented yet") +} + +// Marshal implements Message. +func (m ExtendedMPEG2TSSequenceStart) Marshal() (*rawmessage.Message, error) { + return nil, fmt.Errorf("TODO") +} diff --git a/internal/rtmp/message/extended_sequence_end.go b/internal/rtmp/message/extended_sequence_end.go new file mode 100644 index 00000000..7360c030 --- /dev/null +++ b/internal/rtmp/message/extended_sequence_end.go @@ -0,0 +1,28 @@ +package message + +import ( + "fmt" + + "github.com/aler9/mediamtx/internal/rtmp/rawmessage" +) + +// ExtendedSequenceEnd is a sequence end extended message. +type ExtendedSequenceEnd struct { + FourCC [4]byte +} + +// Unmarshal implements Message. +func (m *ExtendedSequenceEnd) Unmarshal(raw *rawmessage.Message) error { + if len(raw.Body) != 5 { + return fmt.Errorf("invalid body size") + } + + copy(m.FourCC[:], raw.Body[1:5]) + + return nil +} + +// Marshal implements Message. +func (m ExtendedSequenceEnd) Marshal() (*rawmessage.Message, error) { + return nil, fmt.Errorf("TODO") +} diff --git a/internal/rtmp/message/extended_sequence_start.go b/internal/rtmp/message/extended_sequence_start.go new file mode 100644 index 00000000..c85b339c --- /dev/null +++ b/internal/rtmp/message/extended_sequence_start.go @@ -0,0 +1,26 @@ +package message + +import ( + "fmt" + + "github.com/aler9/mediamtx/internal/rtmp/rawmessage" +) + +// ExtendedSequenceStart is a sequence start extended message. +type ExtendedSequenceStart struct { + FourCC [4]byte + Config []byte +} + +// Unmarshal implements Message. +func (m *ExtendedSequenceStart) Unmarshal(raw *rawmessage.Message) error { + copy(m.FourCC[:], raw.Body[1:5]) + m.Config = raw.Body[5:] + + return nil +} + +// Marshal implements Message. +func (m ExtendedSequenceStart) Marshal() (*rawmessage.Message, error) { + return nil, fmt.Errorf("TODO") +} diff --git a/internal/rtmp/message/message.go b/internal/rtmp/message/message.go index 98e23164..16bfbebf 100644 --- a/internal/rtmp/message/message.go +++ b/internal/rtmp/message/message.go @@ -10,6 +10,66 @@ const ( ControlChunkStreamID = 2 ) +// Type is a message type. +type Type byte + +// message types. +const ( + TypeSetChunkSize Type = 1 + TypeAbortMessage Type = 2 + TypeAcknowledge Type = 3 + 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 +) + +// UserControlType is a user control type. +type UserControlType uint16 + +// user control types. +const ( + UserControlTypeStreamBegin UserControlType = 0 + UserControlTypeStreamEOF UserControlType = 1 + UserControlTypeStreamDry UserControlType = 2 + UserControlTypeSetBufferLength UserControlType = 3 + UserControlTypeStreamIsRecorded UserControlType = 4 + UserControlTypePingRequest UserControlType = 6 + UserControlTypePingResponse UserControlType = 7 +) + +// ExtendedType is a message extended type. +type ExtendedType uint8 + +// message extended types. +const ( + ExtendedTypeSequenceStart ExtendedType = 0 + ExtendedTypeCodedFrames ExtendedType = 1 + ExtendedTypeSequenceEnd ExtendedType = 2 + ExtendedTypeFramesX ExtendedType = 3 + ExtendedTypeMetadata ExtendedType = 4 + ExtendedTypeMPEG2TSSequenceStart ExtendedType = 5 +) + +// FourCC is an identifier of a video codec. +type FourCC [4]byte + +// video codec identifiers. +var ( + FourCCAV1 FourCC = [4]byte{'a', 'v', '0', '1'} + FourCCVP9 FourCC = [4]byte{'v', 'p', '0', '9'} + FourCCHEVC FourCC = [4]byte{'h', 'v', 'c', '1'} +) + // Message is a message. type Message interface { Unmarshal(*rawmessage.Message) error diff --git a/internal/rtmp/message/msg_usercontrol.go b/internal/rtmp/message/msg_usercontrol.go deleted file mode 100644 index 08fc903a..00000000 --- a/internal/rtmp/message/msg_usercontrol.go +++ /dev/null @@ -1,12 +0,0 @@ -package message - -// user control types. -const ( - UserControlTypeStreamBegin = 0 - UserControlTypeStreamEOF = 1 - UserControlTypeStreamDry = 2 - UserControlTypeSetBufferLength = 3 - UserControlTypeStreamIsRecorded = 4 - UserControlTypePingRequest = 6 - UserControlTypePingResponse = 7 -) diff --git a/internal/rtmp/message/reader.go b/internal/rtmp/message/reader.go index 498113e9..ab88633b 100644 --- a/internal/rtmp/message/reader.go +++ b/internal/rtmp/message/reader.go @@ -4,70 +4,109 @@ import ( "fmt" "github.com/aler9/mediamtx/internal/rtmp/bytecounter" - "github.com/aler9/mediamtx/internal/rtmp/chunk" "github.com/aler9/mediamtx/internal/rtmp/rawmessage" ) func allocateMessage(raw *rawmessage.Message) (Message, error) { - switch raw.Type { - case chunk.MessageTypeSetChunkSize: - return &MsgSetChunkSize{}, nil + switch Type(raw.Type) { + case TypeSetChunkSize: + return &SetChunkSize{}, nil - case chunk.MessageTypeAcknowledge: - return &MsgAcknowledge{}, nil + case TypeAcknowledge: + return &Acknowledge{}, nil - case chunk.MessageTypeSetWindowAckSize: - return &MsgSetWindowAckSize{}, nil + case TypeSetWindowAckSize: + return &SetWindowAckSize{}, nil - case chunk.MessageTypeSetPeerBandwidth: - return &MsgSetPeerBandwidth{}, nil + case TypeSetPeerBandwidth: + return &SetPeerBandwidth{}, nil - case chunk.MessageTypeUserControl: + case TypeUserControl: if len(raw.Body) < 2 { - return nil, fmt.Errorf("invalid body size") + return nil, fmt.Errorf("not enough bytes") } - subType := uint16(raw.Body[0])<<8 | uint16(raw.Body[1]) - switch subType { + userControlType := UserControlType(uint16(raw.Body[0])<<8 | uint16(raw.Body[1])) + + switch userControlType { case UserControlTypeStreamBegin: - return &MsgUserControlStreamBegin{}, nil + return &UserControlStreamBegin{}, nil case UserControlTypeStreamEOF: - return &MsgUserControlStreamEOF{}, nil + return &UserControlStreamEOF{}, nil case UserControlTypeStreamDry: - return &MsgUserControlStreamDry{}, nil + return &UserControlStreamDry{}, nil case UserControlTypeSetBufferLength: - return &MsgUserControlSetBufferLength{}, nil + return &UserControlSetBufferLength{}, nil case UserControlTypeStreamIsRecorded: - return &MsgUserControlStreamIsRecorded{}, nil + return &UserControlStreamIsRecorded{}, nil case UserControlTypePingRequest: - return &MsgUserControlPingRequest{}, nil + return &UserControlPingRequest{}, nil case UserControlTypePingResponse: - return &MsgUserControlPingResponse{}, nil + return &UserControlPingResponse{}, nil default: - return nil, fmt.Errorf("invalid user control type") + return nil, fmt.Errorf("invalid user control type: %v", userControlType) } - case chunk.MessageTypeCommandAMF0: - return &MsgCommandAMF0{}, nil + case TypeCommandAMF0: + return &CommandAMF0{}, nil - case chunk.MessageTypeDataAMF0: - return &MsgDataAMF0{}, nil + case TypeDataAMF0: + return &DataAMF0{}, nil - case chunk.MessageTypeAudio: - return &MsgAudio{}, nil + case TypeAudio: + return &Audio{}, nil - case chunk.MessageTypeVideo: - return &MsgVideo{}, nil + case TypeVideo: + if len(raw.Body) < 5 { + return nil, fmt.Errorf("not enough bytes") + } + + if (raw.Body[0] & 0b10000000) != 0 { + var fourCC [4]byte + copy(fourCC[:], raw.Body[1:5]) + + switch fourCC { + case FourCCAV1, FourCCVP9, FourCCHEVC: + default: + return nil, fmt.Errorf("invalid fourCC: %v", fourCC) + } + + extendedType := ExtendedType(raw.Body[0] & 0x0F) + + switch extendedType { + case ExtendedTypeSequenceStart: + return &ExtendedSequenceStart{}, nil + + case ExtendedTypeCodedFrames: + return &ExtendedCodedFrames{}, nil + + case ExtendedTypeSequenceEnd: + return &ExtendedSequenceEnd{}, nil + + case ExtendedTypeFramesX: + return &ExtendedFramesX{}, nil + + case ExtendedTypeMetadata: + return &ExtendedMetadata{}, nil + + case ExtendedTypeMPEG2TSSequenceStart: + return &ExtendedMPEG2TSSequenceStart{}, nil + + default: + return nil, fmt.Errorf("invalid extended type: %v", extendedType) + } + } + return &Video{}, nil default: - return nil, fmt.Errorf("unhandled message type (%v)", raw.Type) + return nil, fmt.Errorf("invalid message type: %v", raw.Type) } } @@ -101,10 +140,10 @@ func (r *Reader) Read() (Message, error) { } switch tmsg := msg.(type) { - case *MsgSetChunkSize: + case *SetChunkSize: r.r.SetChunkSize(tmsg.Value) - case *MsgSetWindowAckSize: + case *SetWindowAckSize: r.r.SetWindowAckSize(tmsg.Value) } diff --git a/internal/rtmp/message/reader_test.go b/internal/rtmp/message/reader_test.go index b067552f..4aeab493 100644 --- a/internal/rtmp/message/reader_test.go +++ b/internal/rtmp/message/reader_test.go @@ -18,7 +18,7 @@ var readWriterCases = []struct { }{ { "acknowledge", - &MsgAcknowledge{ + &Acknowledge{ Value: 45953968, }, []byte{ @@ -28,7 +28,7 @@ var readWriterCases = []struct { }, { "audio mpeg2", - &MsgAudio{ + &Audio{ ChunkStreamID: 7, DTS: 6013806 * time.Millisecond, MessageStreamID: 4534543, @@ -45,7 +45,7 @@ var readWriterCases = []struct { }, { "audio mpeg4", - &MsgAudio{ + &Audio{ ChunkStreamID: 7, DTS: 6013806 * time.Millisecond, MessageStreamID: 4534543, @@ -53,7 +53,7 @@ var readWriterCases = []struct { Rate: flvio.SOUND_44Khz, Depth: flvio.SOUND_16BIT, Channels: flvio.SOUND_STEREO, - AACType: MsgAudioAACTypeAU, + AACType: AudioAACTypeAU, Payload: []byte{0x5A, 0xC0, 0x77, 0x40}, }, []byte{ @@ -64,7 +64,7 @@ var readWriterCases = []struct { }, { "command amf0", - &MsgCommandAMF0{ + &CommandAMF0{ ChunkStreamID: 3, MessageStreamID: 345243, Name: "i8yythrergre", @@ -90,7 +90,7 @@ var readWriterCases = []struct { }, { "data amf0", - &MsgDataAMF0{ + &DataAMF0{ ChunkStreamID: 3, MessageStreamID: 345243, Payload: []interface{}{ @@ -108,7 +108,7 @@ var readWriterCases = []struct { }, { "set chunk size", - &MsgSetChunkSize{ + &SetChunkSize{ Value: 10000, }, []byte{ @@ -118,7 +118,7 @@ var readWriterCases = []struct { }, { "set peer bandwidth", - &MsgSetChunkSize{ + &SetChunkSize{ Value: 10000, }, []byte{ @@ -128,7 +128,7 @@ var readWriterCases = []struct { }, { "set window ack size", - &MsgSetChunkSize{ + &SetChunkSize{ Value: 10000, }, []byte{ @@ -138,7 +138,7 @@ var readWriterCases = []struct { }, { "user control ping request", - &MsgUserControlPingRequest{ + &UserControlPingRequest{ ServerTime: 569834435, }, []byte{ @@ -149,7 +149,7 @@ var readWriterCases = []struct { }, { "user control ping response", - &MsgUserControlPingResponse{ + &UserControlPingResponse{ ServerTime: 569834435, }, []byte{ @@ -160,7 +160,7 @@ var readWriterCases = []struct { }, { "user control set buffer length", - &MsgUserControlSetBufferLength{ + &UserControlSetBufferLength{ StreamID: 35534, BufferLength: 235345, }, @@ -172,7 +172,7 @@ var readWriterCases = []struct { }, { "user control stream begin", - &MsgUserControlStreamBegin{ + &UserControlStreamBegin{ StreamID: 35534, }, []byte{ @@ -183,7 +183,7 @@ var readWriterCases = []struct { }, { "user control stream dry", - &MsgUserControlStreamDry{ + &UserControlStreamDry{ StreamID: 35534, }, []byte{ @@ -194,7 +194,7 @@ var readWriterCases = []struct { }, { "user control stream eof", - &MsgUserControlStreamEOF{ + &UserControlStreamEOF{ StreamID: 35534, }, []byte{ @@ -205,7 +205,7 @@ var readWriterCases = []struct { }, { "user control stream is recorded", - &MsgUserControlStreamIsRecorded{ + &UserControlStreamIsRecorded{ StreamID: 35534, }, []byte{ @@ -216,20 +216,51 @@ var readWriterCases = []struct { }, { "video", - &MsgVideo{ + &Video{ ChunkStreamID: 6, DTS: 2543534 * time.Millisecond, MessageStreamID: 0x1000000, Codec: CodecH264, IsKeyFrame: true, - Type: MsgVideoTypeConfig, + Type: VideoTypeConfig, PTSDelta: 10 * time.Millisecond, Payload: []byte{0x01, 0x02, 0x03}, }, []byte{ - 0x6, 0x26, 0xcf, 0xae, 0x0, 0x0, 0x8, 0x9, - 0x1, 0x0, 0x0, 0x0, 0x17, 0x0, 0x0, 0x0, - 0xa, 0x1, 0x2, 0x3, + 0x06, 0x26, 0xcf, 0xae, 0x00, 0x00, 0x08, 0x09, + 0x01, 0x00, 0x00, 0x00, 0x17, 0x00, 0x00, 0x00, + 0x0a, 0x01, 0x02, 0x03, + }, + }, + { + "extended coded frames", + &ExtendedCodedFrames{ + ChunkStreamID: 4, + DTS: 15100 * time.Millisecond, + MessageStreamID: 0x1000000, + FourCC: FourCCHEVC, + PTSDelta: 30 * time.Millisecond, + Payload: []byte{0x01, 0x02, 0x03}, + }, + []byte{ + 0x04, 0x00, 0x3a, 0xfc, 0x00, 0x00, 0x0b, 0x09, + 0x01, 0x00, 0x00, 0x00, 0x81, 0x68, 0x76, 0x63, + 0x31, 0x00, 0x00, 0x1e, 0x01, 0x02, 0x03, + }, + }, + { + "extended frames x", + &ExtendedFramesX{ + ChunkStreamID: 4, + DTS: 15100 * time.Millisecond, + MessageStreamID: 0x1000000, + FourCC: FourCCHEVC, + Payload: []byte{0x01, 0x02, 0x03}, + }, + []byte{ + 0x04, 0x00, 0x3a, 0xfc, 0x00, 0x00, 0x08, 0x09, + 0x01, 0x00, 0x00, 0x00, 0x83, 0x68, 0x76, 0x63, + 0x31, 0x01, 0x02, 0x03, }, }, } diff --git a/internal/rtmp/message/readwriter.go b/internal/rtmp/message/readwriter.go index 890cd235..cd6b082f 100644 --- a/internal/rtmp/message/readwriter.go +++ b/internal/rtmp/message/readwriter.go @@ -15,7 +15,7 @@ func NewReadWriter(bc *bytecounter.ReadWriter, checkAcknowledge bool) *ReadWrite w := NewWriter(bc.Writer, checkAcknowledge) r := NewReader(bc.Reader, func(count uint32) error { - return w.Write(&MsgAcknowledge{ + return w.Write(&Acknowledge{ Value: count, }) }) @@ -34,11 +34,11 @@ func (rw *ReadWriter) Read() (Message, error) { } switch tmsg := msg.(type) { - case *MsgAcknowledge: + case *Acknowledge: rw.w.SetAcknowledgeValue(tmsg.Value) - case *MsgUserControlPingRequest: - rw.w.Write(&MsgUserControlPingResponse{ + case *UserControlPingRequest: + rw.w.Write(&UserControlPingResponse{ ServerTime: tmsg.ServerTime, }) } diff --git a/internal/rtmp/message/readwriter_test.go b/internal/rtmp/message/readwriter_test.go index fb481f7f..99c4ae39 100644 --- a/internal/rtmp/message/readwriter_test.go +++ b/internal/rtmp/message/readwriter_test.go @@ -31,7 +31,7 @@ func TestReadWriterAcknowledge(t *testing.T) { Reader: &buf2, Writer: &buf1, }), true) - err := rw1.Write(&MsgAcknowledge{ + err := rw1.Write(&Acknowledge{ Value: 7863534, }) require.NoError(t, err) @@ -52,7 +52,7 @@ func TestReadWriterPing(t *testing.T) { Reader: &buf2, Writer: &buf1, }), true) - err := rw1.Write(&MsgUserControlPingRequest{ + err := rw1.Write(&UserControlPingRequest{ ServerTime: 143424312, }) require.NoError(t, err) @@ -66,7 +66,7 @@ func TestReadWriterPing(t *testing.T) { msg, err := rw1.Read() require.NoError(t, err) - require.Equal(t, &MsgUserControlPingResponse{ + require.Equal(t, &UserControlPingResponse{ ServerTime: 143424312, }, msg) } diff --git a/internal/rtmp/message/msg_setchunksize.go b/internal/rtmp/message/set_chunk_size.go similarity index 65% rename from internal/rtmp/message/msg_setchunksize.go rename to internal/rtmp/message/set_chunk_size.go index 96354734..53e5f499 100644 --- a/internal/rtmp/message/msg_setchunksize.go +++ b/internal/rtmp/message/set_chunk_size.go @@ -3,23 +3,22 @@ package message //nolint:dupl import ( "fmt" - "github.com/aler9/mediamtx/internal/rtmp/chunk" "github.com/aler9/mediamtx/internal/rtmp/rawmessage" ) -// MsgSetChunkSize is a set chunk size message. -type MsgSetChunkSize struct { +// SetChunkSize is a set chunk size message. +type SetChunkSize struct { Value uint32 } // Unmarshal implements Message. -func (m *MsgSetChunkSize) Unmarshal(raw *rawmessage.Message) error { +func (m *SetChunkSize) Unmarshal(raw *rawmessage.Message) error { if raw.ChunkStreamID != ControlChunkStreamID { return fmt.Errorf("unexpected chunk stream ID") } if len(raw.Body) != 4 { - return fmt.Errorf("unexpected body size") + return fmt.Errorf("invalid body size") } m.Value = uint32(raw.Body[0])<<24 | uint32(raw.Body[1])<<16 | uint32(raw.Body[2])<<8 | uint32(raw.Body[3]) @@ -28,7 +27,7 @@ func (m *MsgSetChunkSize) Unmarshal(raw *rawmessage.Message) error { } // Marshal implements Message. -func (m *MsgSetChunkSize) Marshal() (*rawmessage.Message, error) { +func (m *SetChunkSize) Marshal() (*rawmessage.Message, error) { buf := make([]byte, 4) buf[0] = byte(m.Value >> 24) @@ -38,7 +37,7 @@ func (m *MsgSetChunkSize) Marshal() (*rawmessage.Message, error) { return &rawmessage.Message{ ChunkStreamID: ControlChunkStreamID, - Type: chunk.MessageTypeSetChunkSize, + Type: uint8(TypeSetChunkSize), Body: buf, }, nil } diff --git a/internal/rtmp/message/msg_setpeerbandwidth.go b/internal/rtmp/message/set_peer_bandwidth.go similarity index 65% rename from internal/rtmp/message/msg_setpeerbandwidth.go rename to internal/rtmp/message/set_peer_bandwidth.go index a5e4c1c5..fcb3337b 100644 --- a/internal/rtmp/message/msg_setpeerbandwidth.go +++ b/internal/rtmp/message/set_peer_bandwidth.go @@ -3,24 +3,23 @@ package message //nolint:dupl import ( "fmt" - "github.com/aler9/mediamtx/internal/rtmp/chunk" "github.com/aler9/mediamtx/internal/rtmp/rawmessage" ) -// MsgSetPeerBandwidth is a set peer bandwidth message. -type MsgSetPeerBandwidth struct { +// SetPeerBandwidth is a set peer bandwidth message. +type SetPeerBandwidth struct { Value uint32 Type byte } // Unmarshal implements Message. -func (m *MsgSetPeerBandwidth) Unmarshal(raw *rawmessage.Message) error { +func (m *SetPeerBandwidth) Unmarshal(raw *rawmessage.Message) error { if raw.ChunkStreamID != ControlChunkStreamID { return fmt.Errorf("unexpected chunk stream ID") } if len(raw.Body) != 5 { - return fmt.Errorf("unexpected body size") + return fmt.Errorf("invalid body size") } m.Value = uint32(raw.Body[0])<<24 | uint32(raw.Body[1])<<16 | uint32(raw.Body[2])<<8 | uint32(raw.Body[3]) @@ -30,7 +29,7 @@ func (m *MsgSetPeerBandwidth) Unmarshal(raw *rawmessage.Message) error { } // Marshal implements Message. -func (m *MsgSetPeerBandwidth) Marshal() (*rawmessage.Message, error) { +func (m *SetPeerBandwidth) Marshal() (*rawmessage.Message, error) { buf := make([]byte, 5) buf[0] = byte(m.Value >> 24) @@ -41,7 +40,7 @@ func (m *MsgSetPeerBandwidth) Marshal() (*rawmessage.Message, error) { return &rawmessage.Message{ ChunkStreamID: ControlChunkStreamID, - Type: chunk.MessageTypeSetPeerBandwidth, + Type: uint8(TypeSetPeerBandwidth), Body: buf, }, nil } diff --git a/internal/rtmp/message/msg_setwindowacksize.go b/internal/rtmp/message/set_window_ack_size.go similarity index 63% rename from internal/rtmp/message/msg_setwindowacksize.go rename to internal/rtmp/message/set_window_ack_size.go index 47f6cb6d..583cff86 100644 --- a/internal/rtmp/message/msg_setwindowacksize.go +++ b/internal/rtmp/message/set_window_ack_size.go @@ -3,23 +3,22 @@ package message //nolint:dupl import ( "fmt" - "github.com/aler9/mediamtx/internal/rtmp/chunk" "github.com/aler9/mediamtx/internal/rtmp/rawmessage" ) -// MsgSetWindowAckSize is a set window acknowledgement message. -type MsgSetWindowAckSize struct { +// SetWindowAckSize is a set window acknowledgement message. +type SetWindowAckSize struct { Value uint32 } // Unmarshal implements Message. -func (m *MsgSetWindowAckSize) Unmarshal(raw *rawmessage.Message) error { +func (m *SetWindowAckSize) Unmarshal(raw *rawmessage.Message) error { if raw.ChunkStreamID != ControlChunkStreamID { return fmt.Errorf("unexpected chunk stream ID") } if len(raw.Body) != 4 { - return fmt.Errorf("unexpected body size") + return fmt.Errorf("invalid body size") } m.Value = uint32(raw.Body[0])<<24 | uint32(raw.Body[1])<<16 | uint32(raw.Body[2])<<8 | uint32(raw.Body[3]) @@ -28,7 +27,7 @@ func (m *MsgSetWindowAckSize) Unmarshal(raw *rawmessage.Message) error { } // Marshal implements Message. -func (m *MsgSetWindowAckSize) Marshal() (*rawmessage.Message, error) { +func (m *SetWindowAckSize) Marshal() (*rawmessage.Message, error) { buf := make([]byte, 4) buf[0] = byte(m.Value >> 24) @@ -38,7 +37,7 @@ func (m *MsgSetWindowAckSize) Marshal() (*rawmessage.Message, error) { return &rawmessage.Message{ ChunkStreamID: ControlChunkStreamID, - Type: chunk.MessageTypeSetWindowAckSize, + Type: uint8(TypeSetWindowAckSize), Body: buf, }, nil } diff --git a/internal/rtmp/message/msg_usercontrol_pingrequest.go b/internal/rtmp/message/user_control_ping_request.go similarity index 70% rename from internal/rtmp/message/msg_usercontrol_pingrequest.go rename to internal/rtmp/message/user_control_ping_request.go index 25fc7c14..2059470b 100644 --- a/internal/rtmp/message/msg_usercontrol_pingrequest.go +++ b/internal/rtmp/message/user_control_ping_request.go @@ -3,17 +3,16 @@ package message //nolint:dupl import ( "fmt" - "github.com/aler9/mediamtx/internal/rtmp/chunk" "github.com/aler9/mediamtx/internal/rtmp/rawmessage" ) -// MsgUserControlPingRequest is a user control message. -type MsgUserControlPingRequest struct { +// UserControlPingRequest is a user control message. +type UserControlPingRequest struct { ServerTime uint32 } // Unmarshal implements Message. -func (m *MsgUserControlPingRequest) Unmarshal(raw *rawmessage.Message) error { +func (m *UserControlPingRequest) Unmarshal(raw *rawmessage.Message) error { if raw.ChunkStreamID != ControlChunkStreamID { return fmt.Errorf("unexpected chunk stream ID") } @@ -28,7 +27,7 @@ func (m *MsgUserControlPingRequest) Unmarshal(raw *rawmessage.Message) error { } // Marshal implements Message. -func (m MsgUserControlPingRequest) Marshal() (*rawmessage.Message, error) { +func (m UserControlPingRequest) Marshal() (*rawmessage.Message, error) { buf := make([]byte, 6) buf[0] = byte(UserControlTypePingRequest >> 8) @@ -40,7 +39,7 @@ func (m MsgUserControlPingRequest) Marshal() (*rawmessage.Message, error) { return &rawmessage.Message{ ChunkStreamID: ControlChunkStreamID, - Type: chunk.MessageTypeUserControl, + Type: uint8(TypeUserControl), Body: buf, }, nil } diff --git a/internal/rtmp/message/msg_usercontrol_pingresponse.go b/internal/rtmp/message/user_control_ping_response.go similarity index 70% rename from internal/rtmp/message/msg_usercontrol_pingresponse.go rename to internal/rtmp/message/user_control_ping_response.go index 83d2b61f..7adfc86e 100644 --- a/internal/rtmp/message/msg_usercontrol_pingresponse.go +++ b/internal/rtmp/message/user_control_ping_response.go @@ -3,17 +3,16 @@ package message //nolint:dupl import ( "fmt" - "github.com/aler9/mediamtx/internal/rtmp/chunk" "github.com/aler9/mediamtx/internal/rtmp/rawmessage" ) -// MsgUserControlPingResponse is a user control message. -type MsgUserControlPingResponse struct { +// UserControlPingResponse is a user control message. +type UserControlPingResponse struct { ServerTime uint32 } // Unmarshal implements Message. -func (m *MsgUserControlPingResponse) Unmarshal(raw *rawmessage.Message) error { +func (m *UserControlPingResponse) Unmarshal(raw *rawmessage.Message) error { if raw.ChunkStreamID != ControlChunkStreamID { return fmt.Errorf("unexpected chunk stream ID") } @@ -28,7 +27,7 @@ func (m *MsgUserControlPingResponse) Unmarshal(raw *rawmessage.Message) error { } // Marshal implements Message. -func (m MsgUserControlPingResponse) Marshal() (*rawmessage.Message, error) { +func (m UserControlPingResponse) Marshal() (*rawmessage.Message, error) { buf := make([]byte, 6) buf[0] = byte(UserControlTypePingResponse >> 8) @@ -40,7 +39,7 @@ func (m MsgUserControlPingResponse) Marshal() (*rawmessage.Message, error) { return &rawmessage.Message{ ChunkStreamID: ControlChunkStreamID, - Type: chunk.MessageTypeUserControl, + Type: uint8(TypeUserControl), Body: buf, }, nil } diff --git a/internal/rtmp/message/msg_usercontrol_setbufferlength.go b/internal/rtmp/message/user_control_set_buffer_length.go similarity index 75% rename from internal/rtmp/message/msg_usercontrol_setbufferlength.go rename to internal/rtmp/message/user_control_set_buffer_length.go index e90f385c..9e1df04b 100644 --- a/internal/rtmp/message/msg_usercontrol_setbufferlength.go +++ b/internal/rtmp/message/user_control_set_buffer_length.go @@ -3,18 +3,17 @@ package message //nolint:dupl import ( "fmt" - "github.com/aler9/mediamtx/internal/rtmp/chunk" "github.com/aler9/mediamtx/internal/rtmp/rawmessage" ) -// MsgUserControlSetBufferLength is a user control message. -type MsgUserControlSetBufferLength struct { +// UserControlSetBufferLength is a user control message. +type UserControlSetBufferLength struct { StreamID uint32 BufferLength uint32 } // Unmarshal implements Message. -func (m *MsgUserControlSetBufferLength) Unmarshal(raw *rawmessage.Message) error { +func (m *UserControlSetBufferLength) Unmarshal(raw *rawmessage.Message) error { if raw.ChunkStreamID != ControlChunkStreamID { return fmt.Errorf("unexpected chunk stream ID") } @@ -30,7 +29,7 @@ func (m *MsgUserControlSetBufferLength) Unmarshal(raw *rawmessage.Message) error } // Marshal implements Message. -func (m MsgUserControlSetBufferLength) Marshal() (*rawmessage.Message, error) { +func (m UserControlSetBufferLength) Marshal() (*rawmessage.Message, error) { buf := make([]byte, 10) buf[0] = byte(UserControlTypeSetBufferLength >> 8) @@ -46,7 +45,7 @@ func (m MsgUserControlSetBufferLength) Marshal() (*rawmessage.Message, error) { return &rawmessage.Message{ ChunkStreamID: ControlChunkStreamID, - Type: chunk.MessageTypeUserControl, + Type: uint8(TypeUserControl), Body: buf, }, nil } diff --git a/internal/rtmp/message/msg_usercontrol_streambegin.go b/internal/rtmp/message/user_control_stream_begin.go similarity index 70% rename from internal/rtmp/message/msg_usercontrol_streambegin.go rename to internal/rtmp/message/user_control_stream_begin.go index 7f2b134b..07fd1774 100644 --- a/internal/rtmp/message/msg_usercontrol_streambegin.go +++ b/internal/rtmp/message/user_control_stream_begin.go @@ -3,17 +3,16 @@ package message //nolint:dupl import ( "fmt" - "github.com/aler9/mediamtx/internal/rtmp/chunk" "github.com/aler9/mediamtx/internal/rtmp/rawmessage" ) -// MsgUserControlStreamBegin is a user control message. -type MsgUserControlStreamBegin struct { +// UserControlStreamBegin is a user control message. +type UserControlStreamBegin struct { StreamID uint32 } // Unmarshal implements Message. -func (m *MsgUserControlStreamBegin) Unmarshal(raw *rawmessage.Message) error { +func (m *UserControlStreamBegin) Unmarshal(raw *rawmessage.Message) error { if raw.ChunkStreamID != ControlChunkStreamID { return fmt.Errorf("unexpected chunk stream ID") } @@ -28,7 +27,7 @@ func (m *MsgUserControlStreamBegin) Unmarshal(raw *rawmessage.Message) error { } // Marshal implements Message. -func (m MsgUserControlStreamBegin) Marshal() (*rawmessage.Message, error) { +func (m UserControlStreamBegin) Marshal() (*rawmessage.Message, error) { buf := make([]byte, 6) buf[0] = byte(UserControlTypeStreamBegin >> 8) @@ -40,7 +39,7 @@ func (m MsgUserControlStreamBegin) Marshal() (*rawmessage.Message, error) { return &rawmessage.Message{ ChunkStreamID: ControlChunkStreamID, - Type: chunk.MessageTypeUserControl, + Type: uint8(TypeUserControl), Body: buf, }, nil } diff --git a/internal/rtmp/message/msg_usercontrol_streamdry.go b/internal/rtmp/message/user_control_stream_dry.go similarity index 70% rename from internal/rtmp/message/msg_usercontrol_streamdry.go rename to internal/rtmp/message/user_control_stream_dry.go index d5e5dc05..2d06cd0b 100644 --- a/internal/rtmp/message/msg_usercontrol_streamdry.go +++ b/internal/rtmp/message/user_control_stream_dry.go @@ -3,17 +3,16 @@ package message //nolint:dupl import ( "fmt" - "github.com/aler9/mediamtx/internal/rtmp/chunk" "github.com/aler9/mediamtx/internal/rtmp/rawmessage" ) -// MsgUserControlStreamDry is a user control message. -type MsgUserControlStreamDry struct { +// UserControlStreamDry is a user control message. +type UserControlStreamDry struct { StreamID uint32 } // Unmarshal implements Message. -func (m *MsgUserControlStreamDry) Unmarshal(raw *rawmessage.Message) error { +func (m *UserControlStreamDry) Unmarshal(raw *rawmessage.Message) error { if raw.ChunkStreamID != ControlChunkStreamID { return fmt.Errorf("unexpected chunk stream ID") } @@ -28,7 +27,7 @@ func (m *MsgUserControlStreamDry) Unmarshal(raw *rawmessage.Message) error { } // Marshal implements Message. -func (m MsgUserControlStreamDry) Marshal() (*rawmessage.Message, error) { +func (m UserControlStreamDry) Marshal() (*rawmessage.Message, error) { buf := make([]byte, 6) buf[0] = byte(UserControlTypeStreamDry >> 8) @@ -40,7 +39,7 @@ func (m MsgUserControlStreamDry) Marshal() (*rawmessage.Message, error) { return &rawmessage.Message{ ChunkStreamID: ControlChunkStreamID, - Type: chunk.MessageTypeUserControl, + Type: uint8(TypeUserControl), Body: buf, }, nil } diff --git a/internal/rtmp/message/msg_usercontrol_streameof.go b/internal/rtmp/message/user_control_stream_eof.go similarity index 70% rename from internal/rtmp/message/msg_usercontrol_streameof.go rename to internal/rtmp/message/user_control_stream_eof.go index 58313e7c..287ed2dd 100644 --- a/internal/rtmp/message/msg_usercontrol_streameof.go +++ b/internal/rtmp/message/user_control_stream_eof.go @@ -3,17 +3,16 @@ package message //nolint:dupl import ( "fmt" - "github.com/aler9/mediamtx/internal/rtmp/chunk" "github.com/aler9/mediamtx/internal/rtmp/rawmessage" ) -// MsgUserControlStreamEOF is a user control message. -type MsgUserControlStreamEOF struct { +// UserControlStreamEOF is a user control message. +type UserControlStreamEOF struct { StreamID uint32 } // Unmarshal implements Message. -func (m *MsgUserControlStreamEOF) Unmarshal(raw *rawmessage.Message) error { +func (m *UserControlStreamEOF) Unmarshal(raw *rawmessage.Message) error { if raw.ChunkStreamID != ControlChunkStreamID { return fmt.Errorf("unexpected chunk stream ID") } @@ -28,7 +27,7 @@ func (m *MsgUserControlStreamEOF) Unmarshal(raw *rawmessage.Message) error { } // Marshal implements Message. -func (m MsgUserControlStreamEOF) Marshal() (*rawmessage.Message, error) { +func (m UserControlStreamEOF) Marshal() (*rawmessage.Message, error) { buf := make([]byte, 6) buf[0] = byte(UserControlTypeStreamEOF >> 8) @@ -40,7 +39,7 @@ func (m MsgUserControlStreamEOF) Marshal() (*rawmessage.Message, error) { return &rawmessage.Message{ ChunkStreamID: ControlChunkStreamID, - Type: chunk.MessageTypeUserControl, + Type: uint8(TypeUserControl), Body: buf, }, nil } diff --git a/internal/rtmp/message/msg_usercontrol_streamisrecorded.go b/internal/rtmp/message/user_control_stream_is_recorded.go similarity index 69% rename from internal/rtmp/message/msg_usercontrol_streamisrecorded.go rename to internal/rtmp/message/user_control_stream_is_recorded.go index a5a93425..a9a5da0f 100644 --- a/internal/rtmp/message/msg_usercontrol_streamisrecorded.go +++ b/internal/rtmp/message/user_control_stream_is_recorded.go @@ -3,17 +3,16 @@ package message //nolint:dupl import ( "fmt" - "github.com/aler9/mediamtx/internal/rtmp/chunk" "github.com/aler9/mediamtx/internal/rtmp/rawmessage" ) -// MsgUserControlStreamIsRecorded is a user control message. -type MsgUserControlStreamIsRecorded struct { +// UserControlStreamIsRecorded is a user control message. +type UserControlStreamIsRecorded struct { StreamID uint32 } // Unmarshal implements Message. -func (m *MsgUserControlStreamIsRecorded) Unmarshal(raw *rawmessage.Message) error { +func (m *UserControlStreamIsRecorded) Unmarshal(raw *rawmessage.Message) error { if raw.ChunkStreamID != ControlChunkStreamID { return fmt.Errorf("unexpected chunk stream ID") } @@ -28,7 +27,7 @@ func (m *MsgUserControlStreamIsRecorded) Unmarshal(raw *rawmessage.Message) erro } // Marshal implements Message. -func (m MsgUserControlStreamIsRecorded) Marshal() (*rawmessage.Message, error) { +func (m UserControlStreamIsRecorded) Marshal() (*rawmessage.Message, error) { buf := make([]byte, 6) buf[0] = byte(UserControlTypeStreamIsRecorded >> 8) @@ -40,7 +39,7 @@ func (m MsgUserControlStreamIsRecorded) Marshal() (*rawmessage.Message, error) { return &rawmessage.Message{ ChunkStreamID: ControlChunkStreamID, - Type: chunk.MessageTypeUserControl, + Type: uint8(TypeUserControl), Body: buf, }, nil } diff --git a/internal/rtmp/message/msg_video.go b/internal/rtmp/message/video.go similarity index 63% rename from internal/rtmp/message/msg_video.go rename to internal/rtmp/message/video.go index c08447a5..9b38d69a 100644 --- a/internal/rtmp/message/msg_video.go +++ b/internal/rtmp/message/video.go @@ -6,13 +6,12 @@ import ( "github.com/notedit/rtmp/format/flv/flvio" - "github.com/aler9/mediamtx/internal/rtmp/chunk" "github.com/aler9/mediamtx/internal/rtmp/rawmessage" ) const ( - // MsgVideoChunkStreamID is the chunk stream ID that is usually used to send MsgVideo{} - MsgVideoChunkStreamID = 6 + // VideoChunkStreamID is the chunk stream ID that is usually used to send Video{} + VideoChunkStreamID = 6 ) // supported video codecs @@ -20,30 +19,30 @@ const ( CodecH264 = 7 ) -// MsgVideoType is the type of a video message. -type MsgVideoType uint8 +// VideoType is the type of a video message. +type VideoType uint8 -// MsgVideoType values. +// VideoType values. const ( - MsgVideoTypeConfig MsgVideoType = 0 - MsgVideoTypeAU MsgVideoType = 1 - MsgVideoTypeEOS MsgVideoType = 2 + VideoTypeConfig VideoType = 0 + VideoTypeAU VideoType = 1 + VideoTypeEOS VideoType = 2 ) -// MsgVideo is a video message. -type MsgVideo struct { +// Video is a video message. +type Video struct { ChunkStreamID byte DTS time.Duration MessageStreamID uint32 Codec uint8 IsKeyFrame bool - Type MsgVideoType + Type VideoType PTSDelta time.Duration Payload []byte } // Unmarshal implements Message. -func (m *MsgVideo) Unmarshal(raw *rawmessage.Message) error { +func (m *Video) Unmarshal(raw *rawmessage.Message) error { m.ChunkStreamID = raw.ChunkStreamID m.DTS = raw.Timestamp m.MessageStreamID = raw.MessageStreamID @@ -61,15 +60,14 @@ func (m *MsgVideo) Unmarshal(raw *rawmessage.Message) error { return fmt.Errorf("unsupported video codec: %d", m.Codec) } - m.Type = MsgVideoType(raw.Body[1]) + m.Type = VideoType(raw.Body[1]) switch m.Type { - case MsgVideoTypeConfig, MsgVideoTypeAU, MsgVideoTypeEOS: + case VideoTypeConfig, VideoTypeAU, VideoTypeEOS: default: return fmt.Errorf("unsupported video message type: %d", m.Type) } - tmp := uint32(raw.Body[2])<<16 | uint32(raw.Body[3])<<8 | uint32(raw.Body[4]) - m.PTSDelta = time.Duration(tmp) * time.Millisecond + m.PTSDelta = time.Duration(uint32(raw.Body[2])<<16|uint32(raw.Body[3])<<8|uint32(raw.Body[4])) * time.Millisecond m.Payload = raw.Body[5:] @@ -77,7 +75,7 @@ func (m *MsgVideo) Unmarshal(raw *rawmessage.Message) error { } // Marshal implements Message. -func (m MsgVideo) Marshal() (*rawmessage.Message, error) { +func (m Video) Marshal() (*rawmessage.Message, error) { body := make([]byte, 5+len(m.Payload)) if m.IsKeyFrame { @@ -98,7 +96,7 @@ func (m MsgVideo) Marshal() (*rawmessage.Message, error) { return &rawmessage.Message{ ChunkStreamID: m.ChunkStreamID, Timestamp: m.DTS, - Type: chunk.MessageTypeVideo, + Type: uint8(TypeVideo), MessageStreamID: m.MessageStreamID, Body: body, }, nil diff --git a/internal/rtmp/message/writer.go b/internal/rtmp/message/writer.go index 8b071405..17da0e65 100644 --- a/internal/rtmp/message/writer.go +++ b/internal/rtmp/message/writer.go @@ -35,10 +35,10 @@ func (w *Writer) Write(msg Message) error { } switch tmsg := msg.(type) { - case *MsgSetChunkSize: + case *SetChunkSize: w.w.SetChunkSize(tmsg.Value) - case *MsgSetWindowAckSize: + case *SetWindowAckSize: w.w.SetWindowAckSize(tmsg.Value) } diff --git a/internal/rtmp/rawmessage/message.go b/internal/rtmp/rawmessage/message.go index 2f00350f..c5212d25 100644 --- a/internal/rtmp/rawmessage/message.go +++ b/internal/rtmp/rawmessage/message.go @@ -3,15 +3,13 @@ package rawmessage import ( "time" - - "github.com/aler9/mediamtx/internal/rtmp/chunk" ) // Message is a raw message. type Message struct { ChunkStreamID byte Timestamp time.Duration - Type chunk.MessageType + Type uint8 MessageStreamID uint32 Body []byte } diff --git a/internal/rtmp/rawmessage/reader.go b/internal/rtmp/rawmessage/reader.go index bfd9cfbb..af8c9631 100644 --- a/internal/rtmp/rawmessage/reader.go +++ b/internal/rtmp/rawmessage/reader.go @@ -15,7 +15,7 @@ var errMoreChunksNeeded = errors.New("more chunks are needed") type readerChunkStream struct { mr *Reader curTimestamp *uint32 - curType *chunk.MessageType + curType *uint8 curMessageStreamID *uint32 curBodyLen *uint32 curBody []byte diff --git a/internal/rtmp/rawmessage/reader_test.go b/internal/rtmp/rawmessage/reader_test.go index 9450c58c..7c5062fd 100644 --- a/internal/rtmp/rawmessage/reader_test.go +++ b/internal/rtmp/rawmessage/reader_test.go @@ -23,14 +23,14 @@ var cases = []struct { { ChunkStreamID: 27, Timestamp: 18576 * time.Millisecond, - Type: chunk.MessageTypeSetPeerBandwidth, + Type: 6, MessageStreamID: 3123, Body: bytes.Repeat([]byte{0x03}, 64), }, { ChunkStreamID: 27, Timestamp: (18576 + 15) * time.Millisecond, - Type: chunk.MessageTypeSetWindowAckSize, + Type: 5, MessageStreamID: 3123, Body: bytes.Repeat([]byte{0x04}, 64), }, @@ -39,7 +39,7 @@ var cases = []struct { &chunk.Chunk0{ ChunkStreamID: 27, Timestamp: 18576, - Type: chunk.MessageTypeSetPeerBandwidth, + Type: 6, MessageStreamID: 3123, BodyLen: 64, Body: bytes.Repeat([]byte{0x03}, 64), @@ -47,7 +47,7 @@ var cases = []struct { &chunk.Chunk1{ ChunkStreamID: 27, TimestampDelta: 15, - Type: chunk.MessageTypeSetWindowAckSize, + Type: 5, BodyLen: 64, Body: bytes.Repeat([]byte{0x04}, 64), }, @@ -63,21 +63,21 @@ var cases = []struct { { ChunkStreamID: 27, Timestamp: 18576 * time.Millisecond, - Type: chunk.MessageTypeSetPeerBandwidth, + Type: 6, MessageStreamID: 3123, Body: bytes.Repeat([]byte{0x03}, 64), }, { ChunkStreamID: 27, Timestamp: (18576 + 15) * time.Millisecond, - Type: chunk.MessageTypeSetPeerBandwidth, + Type: 6, MessageStreamID: 3123, Body: bytes.Repeat([]byte{0x04}, 64), }, { ChunkStreamID: 27, Timestamp: (18576 + 15 + 15) * time.Millisecond, - Type: chunk.MessageTypeSetPeerBandwidth, + Type: 6, MessageStreamID: 3123, Body: bytes.Repeat([]byte{0x05}, 64), }, @@ -86,7 +86,7 @@ var cases = []struct { &chunk.Chunk0{ ChunkStreamID: 27, Timestamp: 18576, - Type: chunk.MessageTypeSetPeerBandwidth, + Type: 6, MessageStreamID: 3123, BodyLen: 64, Body: bytes.Repeat([]byte{0x03}, 64), @@ -113,28 +113,28 @@ var cases = []struct { { ChunkStreamID: 27, Timestamp: 18576 * time.Millisecond, - Type: chunk.MessageTypeSetPeerBandwidth, + Type: 6, MessageStreamID: 3123, Body: bytes.Repeat([]byte{0x03}, 190), }, { ChunkStreamID: 27, Timestamp: 18576 * time.Millisecond, - Type: chunk.MessageTypeSetPeerBandwidth, + Type: 6, MessageStreamID: 3123, Body: bytes.Repeat([]byte{0x04}, 192), }, { ChunkStreamID: 27, Timestamp: (18576 + 15) * time.Millisecond, - Type: chunk.MessageTypeSetPeerBandwidth, + Type: 6, MessageStreamID: 3123, Body: bytes.Repeat([]byte{0x05}, 192), }, { ChunkStreamID: 27, Timestamp: (18576 + 15 + 15) * time.Millisecond, - Type: chunk.MessageTypeSetPeerBandwidth, + Type: 6, MessageStreamID: 3123, Body: bytes.Repeat([]byte{0x06}, 192), }, @@ -143,7 +143,7 @@ var cases = []struct { &chunk.Chunk0{ ChunkStreamID: 27, Timestamp: 18576, - Type: chunk.MessageTypeSetPeerBandwidth, + Type: 6, MessageStreamID: 3123, BodyLen: 190, Body: bytes.Repeat([]byte{0x03}, 128), @@ -155,7 +155,7 @@ var cases = []struct { &chunk.Chunk1{ ChunkStreamID: 27, TimestampDelta: 0, - Type: chunk.MessageTypeSetPeerBandwidth, + Type: 6, BodyLen: 192, Body: bytes.Repeat([]byte{0x04}, 128), }, @@ -240,7 +240,7 @@ func TestReaderAcknowledge(t *testing.T) { buf2, err := chunk.Chunk0{ ChunkStreamID: 27, Timestamp: 18576, - Type: chunk.MessageTypeSetPeerBandwidth, + Type: 6, MessageStreamID: 3123, BodyLen: 200, Body: bytes.Repeat([]byte{0x03}, 200), diff --git a/internal/rtmp/rawmessage/writer.go b/internal/rtmp/rawmessage/writer.go index 58627605..31dae62c 100644 --- a/internal/rtmp/rawmessage/writer.go +++ b/internal/rtmp/rawmessage/writer.go @@ -12,7 +12,7 @@ import ( type writerChunkStream struct { mw *Writer lastMessageStreamID *uint32 - lastType *chunk.MessageType + lastType *uint8 lastBodyLen *uint32 lastTimestamp *time.Duration lastTimestampDelta *time.Duration diff --git a/internal/rtmp/rawmessage/writer_test.go b/internal/rtmp/rawmessage/writer_test.go index fcff2f04..0540efdf 100644 --- a/internal/rtmp/rawmessage/writer_test.go +++ b/internal/rtmp/rawmessage/writer_test.go @@ -50,7 +50,7 @@ func TestWriterAcknowledge(t *testing.T) { err := w.Write(&Message{ ChunkStreamID: 27, Timestamp: 18576 * time.Millisecond, - Type: chunk.MessageTypeSetPeerBandwidth, + Type: 6, MessageStreamID: 3123, Body: bytes.Repeat([]byte{0x03}, 200), }) @@ -59,7 +59,7 @@ func TestWriterAcknowledge(t *testing.T) { err = w.Write(&Message{ ChunkStreamID: 27, Timestamp: 18576 * time.Millisecond, - Type: chunk.MessageTypeSetPeerBandwidth, + Type: 6, MessageStreamID: 3123, Body: bytes.Repeat([]byte{0x03}, 200), }) diff --git a/internal/rtmp/tracks/boxes_av1.go b/internal/rtmp/tracks/boxes_av1.go new file mode 100644 index 00000000..f75b07e8 --- /dev/null +++ b/internal/rtmp/tracks/boxes_av1.go @@ -0,0 +1,37 @@ +package tracks + +import ( + gomp4 "github.com/abema/go-mp4" +) + +// BoxTypeAv1C returns the box type. +func BoxTypeAv1C() gomp4.BoxType { return gomp4.StrToBoxType("av1C") } + +func init() { //nolint:gochecknoinits + gomp4.AddBoxDef(&Av1C{}) +} + +// Av1C is a Av1C ISO-BMFF box. +type Av1C struct { + gomp4.Box + Marker uint8 `mp4:"0,size=1,const=1"` + Version uint8 `mp4:"1,size=7,const=1"` + SeqProfile uint8 `mp4:"2,size=3"` + SeqLevelIdx0 uint8 `mp4:"3,size=5"` + SeqTier0 uint8 `mp4:"4,size=1"` + HighBitdepth uint8 `mp4:"5,size=1"` + TwelveBit uint8 `mp4:"6,size=1"` + Monochrome uint8 `mp4:"7,size=1"` + ChromaSubsamplingX uint8 `mp4:"8,size=1"` + ChromaSubsamplingY uint8 `mp4:"9,size=1"` + ChromaSamplePosition uint8 `mp4:"10,size=2"` + Reserved uint8 `mp4:"11,size=3,const=0"` + InitialPresentationDelayPresent uint8 `mp4:"12,size=1"` + InitialPresentationDelayMinusOne uint8 `mp4:"13,size=4"` + ConfigOBUs []uint8 `mp4:"14,size=8"` +} + +// GetType returns the box type. +func (Av1C) GetType() gomp4.BoxType { + return BoxTypeAv1C() +} diff --git a/internal/rtmp/tracks/read.go b/internal/rtmp/tracks/read.go new file mode 100644 index 00000000..deb633b7 --- /dev/null +++ b/internal/rtmp/tracks/read.go @@ -0,0 +1,396 @@ +// Package tracks contains functions to read and write track metadata. +package tracks + +import ( + "bytes" + "errors" + "fmt" + "time" + + gomp4 "github.com/abema/go-mp4" + "github.com/bluenviron/gortsplib/v3/pkg/formats" + "github.com/bluenviron/mediacommon/pkg/codecs/av1" + "github.com/bluenviron/mediacommon/pkg/codecs/h264" + "github.com/bluenviron/mediacommon/pkg/codecs/h265" + "github.com/bluenviron/mediacommon/pkg/codecs/mpeg4audio" + "github.com/notedit/rtmp/format/flv/flvio" + + "github.com/aler9/mediamtx/internal/rtmp/h264conf" + "github.com/aler9/mediamtx/internal/rtmp/message" +) + +func h265FindNALU(array []gomp4.HEVCNaluArray, typ h265.NALUType) []byte { + for _, entry := range array { + if entry.NaluType == byte(typ) && entry.NumNalus == 1 && + h265.NALUType((entry.Nalus[0].NALUnit[0]>>1)&0b111111) == typ { + return entry.Nalus[0].NALUnit + } + } + return nil +} + +func trackFromH264DecoderConfig(data []byte) (formats.Format, error) { + var conf h264conf.Conf + err := conf.Unmarshal(data) + if err != nil { + return nil, fmt.Errorf("unable to parse H264 config: %v", err) + } + + return &formats.H264{ + PayloadTyp: 96, + SPS: conf.SPS, + PPS: conf.PPS, + PacketizationMode: 1, + }, nil +} + +func trackFromAACDecoderConfig(data []byte) (*formats.MPEG4Audio, error) { + var mpegConf mpeg4audio.Config + err := mpegConf.Unmarshal(data) + if err != nil { + return nil, err + } + + return &formats.MPEG4Audio{ + PayloadTyp: 96, + Config: &mpegConf, + SizeLength: 13, + IndexLength: 3, + IndexDeltaLength: 3, + }, nil +} + +var errEmptyMetadata = errors.New("metadata is empty") + +func readTracksFromMetadata(r *message.ReadWriter, payload []interface{}) (formats.Format, formats.Format, error) { + if len(payload) != 1 { + return nil, nil, fmt.Errorf("invalid metadata") + } + + md, ok := payload[0].(flvio.AMFMap) + if !ok { + return nil, nil, fmt.Errorf("invalid metadata") + } + + var videoTrack formats.Format + var audioTrack formats.Format + + hasVideo, err := func() (bool, error) { + v, ok := md.GetV("videocodecid") + if !ok { + return false, nil + } + + switch vt := v.(type) { + case float64: + switch vt { + case 0: + return false, nil + + case message.CodecH264: + return true, nil + } + + case string: + if vt == "avc1" { + return true, nil + } + } + + return false, fmt.Errorf("unsupported video codec: %v", v) + }() + if err != nil { + return nil, nil, err + } + + hasAudio, err := func() (bool, error) { + v, ok := md.GetV("audiocodecid") + if !ok { + return false, nil + } + + switch vt := v.(type) { + case float64: + switch vt { + case 0: + return false, nil + + case message.CodecMPEG2Audio: + audioTrack = &formats.MPEG2Audio{} + return true, nil + + case message.CodecMPEG4Audio: + return true, nil + } + + case string: + if vt == "mp4a" { + return true, nil + } + } + + return false, fmt.Errorf("unsupported audio codec %v", v) + }() + if err != nil { + return nil, nil, err + } + + if !hasVideo && !hasAudio { + return nil, nil, errEmptyMetadata + } + + for { + if (!hasVideo || videoTrack != nil) && + (!hasAudio || audioTrack != nil) { + return videoTrack, audioTrack, nil + } + + msg, err := r.Read() + if err != nil { + return nil, nil, err + } + + switch tmsg := msg.(type) { + case *message.Video: + if !hasVideo { + return nil, nil, fmt.Errorf("unexpected video packet") + } + + if videoTrack == nil { + if tmsg.Type == message.VideoTypeConfig { + videoTrack, err = trackFromH264DecoderConfig(tmsg.Payload) + if err != nil { + return nil, nil, err + } + + // format used by OBS < 29.1 to publish H265 + } else if tmsg.Type == message.VideoTypeAU && tmsg.IsKeyFrame { + nalus, err := h264.AVCCUnmarshal(tmsg.Payload) + if err != nil { + return nil, nil, err + } + + var vps []byte + var sps []byte + var pps []byte + + for _, nalu := range nalus { + typ := h265.NALUType((nalu[0] >> 1) & 0b111111) + + switch typ { + case h265.NALUType_VPS_NUT: + vps = nalu + + case h265.NALUType_SPS_NUT: + sps = nalu + + case h265.NALUType_PPS_NUT: + pps = nalu + } + } + + if vps != nil && sps != nil && pps != nil { + videoTrack = &formats.H265{ + PayloadTyp: 96, + VPS: vps, + SPS: sps, + PPS: pps, + } + } + } + } + + case *message.ExtendedSequenceStart: + if videoTrack == nil { + switch tmsg.FourCC { + case message.FourCCHEVC: + var hvcc gomp4.HvcC + _, err := gomp4.Unmarshal(bytes.NewReader(tmsg.Config), uint64(len(tmsg.Config)), &hvcc, gomp4.Context{}) + if err != nil { + return nil, nil, fmt.Errorf("invalid H265 configuration: %v", err) + } + + vps := h265FindNALU(hvcc.NaluArrays, h265.NALUType_VPS_NUT) + sps := h265FindNALU(hvcc.NaluArrays, h265.NALUType_SPS_NUT) + pps := h265FindNALU(hvcc.NaluArrays, h265.NALUType_PPS_NUT) + if vps == nil || sps == nil || pps == nil { + return nil, nil, fmt.Errorf("H265 parameters are missing") + } + + videoTrack = &formats.H265{ + PayloadTyp: 96, + VPS: vps, + SPS: sps, + PPS: pps, + } + + case message.FourCCAV1: + var av1c Av1C + _, err := gomp4.Unmarshal(bytes.NewReader(tmsg.Config), uint64(len(tmsg.Config)), &av1c, gomp4.Context{}) + if err != nil { + return nil, nil, fmt.Errorf("invalid AV1 configuration: %v", err) + } + + // parse sequence header and metadata contained in ConfigOBUs, but do not use them + _, err = av1.BitstreamUnmarshal(av1c.ConfigOBUs, false) + if err != nil { + return nil, nil, fmt.Errorf("invalid AV1 configuration: %v", err) + } + + videoTrack = &formats.AV1{} + + default: // VP9 + return nil, nil, fmt.Errorf("VP9 is not supported yet") + } + } + + case *message.Audio: + if !hasAudio { + return nil, nil, fmt.Errorf("unexpected audio packet") + } + + if audioTrack == nil && + tmsg.Codec == message.CodecMPEG4Audio && + tmsg.AACType == message.AudioAACTypeConfig { + audioTrack, err = trackFromAACDecoderConfig(tmsg.Payload) + if err != nil { + return nil, nil, err + } + } + } + } +} + +func readTracksFromMessages(r *message.ReadWriter, msg message.Message) (formats.Format, *formats.MPEG4Audio, error) { + var startTime *time.Duration + var videoTrack formats.Format + var audioTrack *formats.MPEG4Audio + + // analyze 1 second of packets +outer: + for { + switch tmsg := msg.(type) { + case *message.Video: + if startTime == nil { + v := tmsg.DTS + startTime = &v + } + + if tmsg.Type == message.VideoTypeConfig { + if videoTrack == nil { + var err error + videoTrack, err = trackFromH264DecoderConfig(tmsg.Payload) + if err != nil { + return nil, nil, err + } + + // stop the analysis if both tracks are found + if videoTrack != nil && audioTrack != nil { + return videoTrack, audioTrack, nil + } + } + } + + if (tmsg.DTS - *startTime) >= 1*time.Second { + break outer + } + + case *message.Audio: + if startTime == nil { + v := tmsg.DTS + startTime = &v + } + + if tmsg.AACType == message.AudioAACTypeConfig { + if audioTrack == nil { + var err error + audioTrack, err = trackFromAACDecoderConfig(tmsg.Payload) + if err != nil { + return nil, nil, err + } + + // stop the analysis if both tracks are found + if videoTrack != nil && audioTrack != nil { + return videoTrack, audioTrack, nil + } + } + } + + if (tmsg.DTS - *startTime) >= 1*time.Second { + break outer + } + } + + var err error + msg, err = r.Read() + if err != nil { + return nil, nil, err + } + } + + if videoTrack == nil && audioTrack == nil { + return nil, nil, fmt.Errorf("no tracks found") + } + + return videoTrack, audioTrack, nil +} + +// Read reads track informations. +// It returns the video track and the audio track. +func Read(r *message.ReadWriter) (formats.Format, formats.Format, error) { + msg, err := func() (message.Message, error) { + for { + msg, err := r.Read() + if err != nil { + return nil, err + } + + // skip play start and data start + if cmd, ok := msg.(*message.CommandAMF0); ok && cmd.Name == "onStatus" { + continue + } + + // skip RtmpSampleAccess + if data, ok := msg.(*message.DataAMF0); ok && len(data.Payload) >= 1 { + if s, ok := data.Payload[0].(string); ok && s == "|RtmpSampleAccess" { + continue + } + } + + return msg, nil + } + }() + if err != nil { + return nil, nil, err + } + + if data, ok := msg.(*message.DataAMF0); ok && len(data.Payload) >= 1 { + payload := data.Payload + + if s, ok := payload[0].(string); ok && s == "@setDataFrame" { + payload = payload[1:] + } + + if len(payload) >= 1 { + if s, ok := payload[0].(string); ok && s == "onMetaData" { + videoTrack, audioTrack, err := readTracksFromMetadata(r, payload[1:]) + if err != nil { + if err == errEmptyMetadata { + msg, err := r.Read() + if err != nil { + return nil, nil, err + } + + return readTracksFromMessages(r, msg) + } + + return nil, nil, err + } + + return videoTrack, audioTrack, nil + } + } + } + + return readTracksFromMessages(r, msg) +} diff --git a/internal/rtmp/tracks/read_test.go b/internal/rtmp/tracks/read_test.go new file mode 100644 index 00000000..0a00433b --- /dev/null +++ b/internal/rtmp/tracks/read_test.go @@ -0,0 +1,484 @@ +package tracks + +import ( + "bytes" + "testing" + "time" + + "github.com/bluenviron/gortsplib/v3/pkg/formats" + "github.com/bluenviron/mediacommon/pkg/codecs/h264" + "github.com/bluenviron/mediacommon/pkg/codecs/mpeg4audio" + "github.com/notedit/rtmp/format/flv/flvio" + "github.com/stretchr/testify/require" + + "github.com/aler9/mediamtx/internal/rtmp/bytecounter" + "github.com/aler9/mediamtx/internal/rtmp/h264conf" + "github.com/aler9/mediamtx/internal/rtmp/message" +) + +func TestRead(t *testing.T) { + sps := []byte{ + 0x67, 0x64, 0x00, 0x0c, 0xac, 0x3b, 0x50, 0xb0, + 0x4b, 0x42, 0x00, 0x00, 0x03, 0x00, 0x02, 0x00, + 0x00, 0x03, 0x00, 0x3d, 0x08, + } + + pps := []byte{ + 0x68, 0xee, 0x3c, 0x80, + } + + for _, ca := range []struct { + name string + videoTrack formats.Format + audioTrack formats.Format + }{ + { + "video+audio", + &formats.H264{ + PayloadTyp: 96, + SPS: sps, + PPS: pps, + PacketizationMode: 1, + }, + &formats.MPEG4Audio{ + PayloadTyp: 96, + Config: &mpeg4audio.Config{ + Type: 2, + SampleRate: 44100, + ChannelCount: 2, + }, + SizeLength: 13, + IndexLength: 3, + IndexDeltaLength: 3, + }, + }, + { + "video", + &formats.H264{ + PayloadTyp: 96, + SPS: sps, + PPS: pps, + PacketizationMode: 1, + }, + nil, + }, + { + "metadata without codec id", + &formats.H264{ + PayloadTyp: 96, + SPS: sps, + PPS: pps, + PacketizationMode: 1, + }, + &formats.MPEG4Audio{ + PayloadTyp: 96, + Config: &mpeg4audio.Config{ + Type: 2, + SampleRate: 44100, + ChannelCount: 2, + }, + SizeLength: 13, + IndexLength: 3, + IndexDeltaLength: 3, + }, + }, + { + "missing metadata, video+audio", + &formats.H264{ + PayloadTyp: 96, + SPS: sps, + PPS: pps, + PacketizationMode: 1, + }, + &formats.MPEG4Audio{ + PayloadTyp: 96, + Config: &mpeg4audio.Config{ + Type: 2, + SampleRate: 44100, + ChannelCount: 2, + }, + SizeLength: 13, + IndexLength: 3, + IndexDeltaLength: 3, + }, + }, + { + "missing metadata, audio", + nil, + &formats.MPEG4Audio{ + PayloadTyp: 96, + Config: &mpeg4audio.Config{ + Type: 2, + SampleRate: 44100, + ChannelCount: 2, + }, + SizeLength: 13, + IndexLength: 3, + IndexDeltaLength: 3, + }, + }, + { + "obs studio pre 29.1 h265", + &formats.H265{ + PayloadTyp: 96, + VPS: []byte{ + 0x40, 0x01, 0x0c, 0x01, 0xff, 0xff, 0x01, 0x40, + 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, + 0x03, 0x00, 0x00, 0x03, 0x00, 0x7b, 0xac, 0x09, + }, + SPS: []byte{ + 0x42, 0x01, 0x01, 0x01, 0x40, 0x00, 0x00, 0x03, + 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, + 0x03, 0x00, 0x7b, 0xa0, 0x03, 0xc0, 0x80, 0x11, + 0x07, 0xcb, 0x96, 0xb4, 0xa4, 0x25, 0x92, 0xe3, + 0x01, 0x6a, 0x02, 0x02, 0x02, 0x08, 0x00, 0x00, + 0x03, 0x00, 0x08, 0x00, 0x00, 0x03, 0x01, 0xe3, + 0x00, 0x2e, 0xf2, 0x88, 0x00, 0x09, 0x89, 0x60, + 0x00, 0x04, 0xc4, 0xb4, 0x20, + }, + PPS: []byte{ + 0x44, 0x01, 0xc0, 0xf7, 0xc0, 0xcc, 0x90, + }, + }, + &formats.MPEG4Audio{ + PayloadTyp: 96, + Config: &mpeg4audio.Config{ + Type: 2, + SampleRate: 44100, + ChannelCount: 2, + }, + SizeLength: 13, + IndexLength: 3, + IndexDeltaLength: 3, + }, + }, + } { + t.Run(ca.name, func(t *testing.T) { + var buf bytes.Buffer + bc := bytecounter.NewReadWriter(&buf) + mrw := message.NewReadWriter(bc, true) + + switch ca.name { + case "video+audio": + err := mrw.Write(&message.DataAMF0{ + ChunkStreamID: 4, + MessageStreamID: 1, + Payload: []interface{}{ + "@setDataFrame", + "onMetaData", + flvio.AMFMap{ + { + K: "videodatarate", + V: float64(0), + }, + { + K: "videocodecid", + V: float64(message.CodecH264), + }, + { + K: "audiodatarate", + V: float64(0), + }, + { + K: "audiocodecid", + V: float64(message.CodecMPEG4Audio), + }, + }, + }, + }) + require.NoError(t, err) + + buf, _ := h264conf.Conf{ + SPS: sps, + PPS: pps, + }.Marshal() + + err = mrw.Write(&message.Video{ + ChunkStreamID: message.VideoChunkStreamID, + MessageStreamID: 0x1000000, + Codec: message.CodecH264, + IsKeyFrame: true, + Type: message.VideoTypeConfig, + Payload: buf, + }) + require.NoError(t, err) + + enc, err := mpeg4audio.Config{ + Type: 2, + SampleRate: 44100, + ChannelCount: 2, + }.Marshal() + require.NoError(t, err) + + err = mrw.Write(&message.Audio{ + ChunkStreamID: message.AudioChunkStreamID, + MessageStreamID: 0x1000000, + Codec: message.CodecMPEG4Audio, + Rate: flvio.SOUND_44Khz, + Depth: flvio.SOUND_16BIT, + Channels: flvio.SOUND_STEREO, + AACType: message.AudioAACTypeConfig, + Payload: enc, + }) + require.NoError(t, err) + + case "video": + err := mrw.Write(&message.DataAMF0{ + ChunkStreamID: 4, + MessageStreamID: 1, + Payload: []interface{}{ + "@setDataFrame", + "onMetaData", + flvio.AMFMap{ + { + K: "videodatarate", + V: float64(0), + }, + { + K: "videocodecid", + V: float64(message.CodecH264), + }, + { + K: "audiodatarate", + V: float64(0), + }, + { + K: "audiocodecid", + V: float64(0), + }, + }, + }, + }) + require.NoError(t, err) + + buf, _ := h264conf.Conf{ + SPS: sps, + PPS: pps, + }.Marshal() + + err = mrw.Write(&message.Video{ + ChunkStreamID: message.VideoChunkStreamID, + MessageStreamID: 0x1000000, + Codec: message.CodecH264, + IsKeyFrame: true, + Type: message.VideoTypeConfig, + Payload: buf, + }) + require.NoError(t, err) + + case "metadata without codec id": + err := mrw.Write(&message.DataAMF0{ + ChunkStreamID: 4, + MessageStreamID: 1, + Payload: []interface{}{ + "@setDataFrame", + "onMetaData", + flvio.AMFMap{ + { + K: "width", + V: float64(2688), + }, + { + K: "height", + V: float64(1520), + }, + { + K: "framerate", + V: float64(0o25), + }, + }, + }, + }) + require.NoError(t, err) + + buf, _ := h264conf.Conf{ + SPS: sps, + PPS: pps, + }.Marshal() + + err = mrw.Write(&message.Video{ + ChunkStreamID: message.VideoChunkStreamID, + MessageStreamID: 0x1000000, + Codec: message.CodecH264, + IsKeyFrame: true, + Type: message.VideoTypeConfig, + Payload: buf, + }) + require.NoError(t, err) + + enc, err := mpeg4audio.Config{ + Type: 2, + SampleRate: 44100, + ChannelCount: 2, + }.Marshal() + require.NoError(t, err) + + err = mrw.Write(&message.Audio{ + ChunkStreamID: message.AudioChunkStreamID, + MessageStreamID: 0x1000000, + Codec: message.CodecMPEG4Audio, + Rate: flvio.SOUND_44Khz, + Depth: flvio.SOUND_16BIT, + Channels: flvio.SOUND_STEREO, + AACType: message.AudioAACTypeConfig, + Payload: enc, + }) + require.NoError(t, err) + + case "missing metadata, video+audio": + buf, _ := h264conf.Conf{ + SPS: sps, + PPS: pps, + }.Marshal() + + err := mrw.Write(&message.Video{ + ChunkStreamID: message.VideoChunkStreamID, + MessageStreamID: 0x1000000, + Codec: message.CodecH264, + IsKeyFrame: true, + Type: message.VideoTypeConfig, + Payload: buf, + }) + require.NoError(t, err) + + enc, err := mpeg4audio.Config{ + Type: 2, + SampleRate: 44100, + ChannelCount: 2, + }.Marshal() + require.NoError(t, err) + + err = mrw.Write(&message.Audio{ + ChunkStreamID: message.AudioChunkStreamID, + MessageStreamID: 0x1000000, + Codec: message.CodecMPEG4Audio, + Rate: flvio.SOUND_44Khz, + Depth: flvio.SOUND_16BIT, + Channels: flvio.SOUND_STEREO, + AACType: message.AudioAACTypeConfig, + Payload: enc, + }) + require.NoError(t, err) + + case "missing metadata, audio": + enc, err := mpeg4audio.Config{ + Type: 2, + SampleRate: 44100, + ChannelCount: 2, + }.Marshal() + require.NoError(t, err) + + err = mrw.Write(&message.Audio{ + ChunkStreamID: message.AudioChunkStreamID, + MessageStreamID: 0x1000000, + Codec: message.CodecMPEG4Audio, + Rate: flvio.SOUND_44Khz, + Depth: flvio.SOUND_16BIT, + Channels: flvio.SOUND_STEREO, + AACType: message.AudioAACTypeConfig, + Payload: enc, + }) + require.NoError(t, err) + + err = mrw.Write(&message.Audio{ + ChunkStreamID: message.AudioChunkStreamID, + MessageStreamID: 0x1000000, + Codec: message.CodecMPEG4Audio, + Rate: flvio.SOUND_44Khz, + Depth: flvio.SOUND_16BIT, + Channels: flvio.SOUND_STEREO, + AACType: message.AudioAACTypeConfig, + Payload: enc, + DTS: 1 * time.Second, + }) + require.NoError(t, err) + + case "obs studio pre 29.1 h265": + err := mrw.Write(&message.DataAMF0{ + ChunkStreamID: 4, + MessageStreamID: 1, + Payload: []interface{}{ + "@setDataFrame", + "onMetaData", + flvio.AMFMap{ + { + K: "videodatarate", + V: float64(0), + }, + { + K: "videocodecid", + V: float64(message.CodecH264), + }, + { + K: "audiodatarate", + V: float64(0), + }, + { + K: "audiocodecid", + V: float64(message.CodecMPEG4Audio), + }, + }, + }, + }) + require.NoError(t, err) + + avcc, err := h264.AVCCMarshal([][]byte{ + { // VPS + 0x40, 0x01, 0x0c, 0x01, 0xff, 0xff, 0x01, 0x40, + 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, + 0x03, 0x00, 0x00, 0x03, 0x00, 0x7b, 0xac, 0x09, + }, + { // SPS + 0x42, 0x01, 0x01, 0x01, 0x40, 0x00, 0x00, 0x03, + 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, + 0x03, 0x00, 0x7b, 0xa0, 0x03, 0xc0, 0x80, 0x11, + 0x07, 0xcb, 0x96, 0xb4, 0xa4, 0x25, 0x92, 0xe3, + 0x01, 0x6a, 0x02, 0x02, 0x02, 0x08, 0x00, 0x00, + 0x03, 0x00, 0x08, 0x00, 0x00, 0x03, 0x01, 0xe3, + 0x00, 0x2e, 0xf2, 0x88, 0x00, 0x09, 0x89, 0x60, + 0x00, 0x04, 0xc4, 0xb4, 0x20, + }, + { + // PPS + 0x44, 0x01, 0xc0, 0xf7, 0xc0, 0xcc, 0x90, + }, + }) + require.NoError(t, err) + + err = mrw.Write(&message.Video{ + ChunkStreamID: message.VideoChunkStreamID, + MessageStreamID: 0x1000000, + Codec: message.CodecH264, + IsKeyFrame: true, + Type: message.VideoTypeAU, + Payload: avcc, + }) + require.NoError(t, err) + + enc, err := mpeg4audio.Config{ + Type: 2, + SampleRate: 44100, + ChannelCount: 2, + }.Marshal() + require.NoError(t, err) + + err = mrw.Write(&message.Audio{ + ChunkStreamID: message.AudioChunkStreamID, + MessageStreamID: 0x1000000, + Codec: message.CodecMPEG4Audio, + Rate: flvio.SOUND_44Khz, + Depth: flvio.SOUND_16BIT, + Channels: flvio.SOUND_STEREO, + AACType: message.AudioAACTypeConfig, + Payload: enc, + }) + require.NoError(t, err) + } + + videoTrack, audioTrack, err := Read(mrw) + require.NoError(t, err) + require.Equal(t, ca.videoTrack, videoTrack) + require.Equal(t, ca.audioTrack, audioTrack) + }) + } +} diff --git a/internal/rtmp/tracks/write.go b/internal/rtmp/tracks/write.go new file mode 100644 index 00000000..5d488936 --- /dev/null +++ b/internal/rtmp/tracks/write.go @@ -0,0 +1,107 @@ +package tracks + +import ( + "github.com/bluenviron/gortsplib/v3/pkg/formats" + "github.com/notedit/rtmp/format/flv/flvio" + + "github.com/aler9/mediamtx/internal/rtmp/h264conf" + "github.com/aler9/mediamtx/internal/rtmp/message" +) + +// Write writes track informations. +func Write(w *message.ReadWriter, videoTrack formats.Format, audioTrack formats.Format) error { + err := w.Write(&message.DataAMF0{ + ChunkStreamID: 4, + MessageStreamID: 0x1000000, + Payload: []interface{}{ + "@setDataFrame", + "onMetaData", + flvio.AMFMap{ + { + K: "videodatarate", + V: float64(0), + }, + { + K: "videocodecid", + V: func() float64 { + switch videoTrack.(type) { + case *formats.H264: + return message.CodecH264 + + default: + return 0 + } + }(), + }, + { + K: "audiodatarate", + V: float64(0), + }, + { + K: "audiocodecid", + V: func() float64 { + switch audioTrack.(type) { + case *formats.MPEG2Audio: + return message.CodecMPEG2Audio + + case *formats.MPEG4Audio: + return message.CodecMPEG4Audio + + default: + return 0 + } + }(), + }, + }, + }, + }) + if err != nil { + return err + } + + if videoTrack, ok := videoTrack.(*formats.H264); ok { + // write decoder config only if SPS and PPS are available. + // if they're not available yet, they're sent later. + if sps, pps := videoTrack.SafeParams(); sps != nil && pps != nil { + buf, _ := h264conf.Conf{ + SPS: sps, + PPS: pps, + }.Marshal() + + err = w.Write(&message.Video{ + ChunkStreamID: message.VideoChunkStreamID, + MessageStreamID: 0x1000000, + Codec: message.CodecH264, + IsKeyFrame: true, + Type: message.VideoTypeConfig, + Payload: buf, + }) + if err != nil { + return err + } + } + } + + if mpeg4audioTrack, ok := audioTrack.(*formats.MPEG4Audio); ok { + enc, err := mpeg4audioTrack.Config.Marshal() + if err != nil { + return err + } + + err = w.Write(&message.Audio{ + ChunkStreamID: message.AudioChunkStreamID, + MessageStreamID: 0x1000000, + Codec: message.CodecMPEG4Audio, + Rate: flvio.SOUND_44Khz, + Depth: flvio.SOUND_16BIT, + Channels: flvio.SOUND_STEREO, + AACType: message.AudioAACTypeConfig, + Payload: enc, + }) + if err != nil { + return err + } + } + + return nil +} diff --git a/internal/rtmp/tracks/write_test.go b/internal/rtmp/tracks/write_test.go new file mode 100644 index 00000000..68cddb64 --- /dev/null +++ b/internal/rtmp/tracks/write_test.go @@ -0,0 +1,96 @@ +package tracks + +import ( + "bytes" + "testing" + + "github.com/bluenviron/gortsplib/v3/pkg/formats" + "github.com/bluenviron/mediacommon/pkg/codecs/mpeg4audio" + "github.com/notedit/rtmp/format/flv/flvio" + "github.com/stretchr/testify/require" + + "github.com/aler9/mediamtx/internal/rtmp/bytecounter" + "github.com/aler9/mediamtx/internal/rtmp/message" +) + +func TestWrite(t *testing.T) { + var buf bytes.Buffer + bc := bytecounter.NewReadWriter(&buf) + mrw := message.NewReadWriter(bc, true) + + videoTrack := &formats.H264{ + PayloadTyp: 96, + SPS: []byte{ + 0x67, 0x64, 0x00, 0x0c, 0xac, 0x3b, 0x50, 0xb0, + 0x4b, 0x42, 0x00, 0x00, 0x03, 0x00, 0x02, 0x00, + 0x00, 0x03, 0x00, 0x3d, 0x08, + }, + PPS: []byte{ + 0x68, 0xee, 0x3c, 0x80, + }, + PacketizationMode: 1, + } + + audioTrack := &formats.MPEG4Audio{ + PayloadTyp: 96, + Config: &mpeg4audio.Config{ + Type: 2, + SampleRate: 44100, + ChannelCount: 2, + }, + SizeLength: 13, + IndexLength: 3, + IndexDeltaLength: 3, + } + + err := Write(mrw, videoTrack, audioTrack) + require.NoError(t, err) + + msg, err := mrw.Read() + require.NoError(t, err) + require.Equal(t, &message.DataAMF0{ + ChunkStreamID: 4, + MessageStreamID: 0x1000000, + Payload: []interface{}{ + "@setDataFrame", + "onMetaData", + flvio.AMFMap{ + {K: "videodatarate", V: float64(0)}, + {K: "videocodecid", V: float64(7)}, + {K: "audiodatarate", V: float64(0)}, + {K: "audiocodecid", V: float64(10)}, + }, + }, + }, msg) + + msg, err = mrw.Read() + require.NoError(t, err) + require.Equal(t, &message.Video{ + ChunkStreamID: message.VideoChunkStreamID, + MessageStreamID: 0x1000000, + Codec: message.CodecH264, + IsKeyFrame: true, + Type: message.VideoTypeConfig, + Payload: []byte{ + 0x1, 0x64, 0x0, + 0xc, 0xff, 0xe1, 0x0, 0x15, 0x67, 0x64, 0x0, + 0xc, 0xac, 0x3b, 0x50, 0xb0, 0x4b, 0x42, 0x0, + 0x0, 0x3, 0x0, 0x2, 0x0, 0x0, 0x3, 0x0, + 0x3d, 0x8, 0x1, 0x0, 0x4, 0x68, 0xee, 0x3c, + 0x80, + }, + }, msg) + + msg, err = mrw.Read() + require.NoError(t, err) + require.Equal(t, &message.Audio{ + ChunkStreamID: message.AudioChunkStreamID, + MessageStreamID: 0x1000000, + Codec: message.CodecMPEG4Audio, + Rate: flvio.SOUND_44Khz, + Depth: flvio.SOUND_16BIT, + Channels: flvio.SOUND_STEREO, + AACType: message.AudioAACTypeConfig, + Payload: []byte{0x12, 0x10}, + }, msg) +}