rtmp: support additional enhanced RTMP features (#4168) (#4321) (#4954)

* support reading AV1, VP9, H265, Opus, AC-3, G711, LPCM
* support reading multiple video or audio tracks at once
This commit is contained in:
Alessandro Ros
2025-09-11 23:18:46 +02:00
committed by GitHub
parent 2be164acd6
commit 9318107779
31 changed files with 3029 additions and 649 deletions

View File

@@ -9,7 +9,7 @@ Live streams can be read from the server with the following protocols and codecs
| [SRT](#srt) | | H265, H264, MPEG-4 Video (H263, Xvid), MPEG-1/2 Video | Opus, MPEG-4 Audio (AAC), MPEG-1/2 Audio (MP3), AC-3 |
| [WebRTC](#webrtc) | WHEP | AV1, VP9, VP8, H265, H264 | Opus, G722, G711 (PCMA, PCMU) |
| [RTSP](#rtsp) | UDP, UDP-Multicast, TCP, RTSPS | AV1, VP9, VP8, H265, H264, MPEG-4 Video (H263, Xvid), MPEG-1/2 Video, M-JPEG and any RTP-compatible codec | Opus, MPEG-4 Audio (AAC), MPEG-1/2 Audio (MP3), AC-3, G726, G722, G711 (PCMA, PCMU), LPCM and any RTP-compatible codec |
| [RTMP](#rtmp) | RTMP, RTMPS, Enhanced RTMP | H264 | MPEG-4 Audio (AAC), MPEG-1/2 Audio (MP3) |
| [RTMP](#rtmp) | RTMP, RTMPS, Enhanced RTMP | AV1, VP9, H265, H264 | Opus, MPEG-4 Audio (AAC), MPEG-1/2 Audio (MP3), AC-3, G711 (PCMA, PCMU), LPCM |
| [HLS](#hls) | Low-Latency HLS, MP4-based HLS, legacy HLS | AV1, VP9, H265, H264 | Opus, MPEG-4 Audio (AAC) |
We provide instructions for reading with the following software:
@@ -182,6 +182,18 @@ The RTSP protocol supports several underlying transport protocols, each with its
ffmpeg -rtsp_transport tcp -i rtsp://localhost:8554/mystream -c copy output.mp4
```
FFmpeg can also read a stream with RTMP:
```sh
ffmpeg -i rtmp://localhost/mystream -c copy output.mp4
```
In order to read AV1, VP9, H265, Opus, AC3 tracks and in order to read multiple video or audio tracks, the `-rtmp_enhanced_codecs` flag must be present:
```sh
ffmpeg -rtmp_enhanced_codecs ac-3,av01,avc1,ec-3,fLaC,hvc1,.mp3,mp4a,Opus,vp09 -i rtmp://localhost/mystream -c copy output.mp4
```
### GStreamer
GStreamer can read a stream from the server in several ways (RTSP, RTMP, HLS, WebRTC with WHEP, SRT). The recommended one consists in reading with [RTSP](#rtsp):

2
go.mod
View File

@@ -11,7 +11,7 @@ require (
github.com/asticode/go-astits v1.13.0
github.com/bluenviron/gohlslib/v2 v2.2.2
github.com/bluenviron/gortsplib/v4 v4.16.2
github.com/bluenviron/mediacommon/v2 v2.4.1
github.com/bluenviron/mediacommon/v2 v2.4.2-0.20250909112826-017d0bbe41db
github.com/datarhei/gosrt v0.9.0
github.com/fsnotify/fsnotify v1.9.0
github.com/gin-contrib/pprof v1.5.3

4
go.sum
View File

@@ -35,8 +35,8 @@ github.com/bluenviron/gohlslib/v2 v2.2.2 h1:Q86VloPjwONKF8pu6jSEh9ENm4UzdMl5SzYv
github.com/bluenviron/gohlslib/v2 v2.2.2/go.mod h1:3Lby/VMDD/cN0B3uJPd3bEEiJZ34LqXs71FEvN/fq2k=
github.com/bluenviron/gortsplib/v4 v4.16.2 h1:10HaMsorjW13gscLp3R7Oj41ck2i1EHIUYCNWD2wpkI=
github.com/bluenviron/gortsplib/v4 v4.16.2/go.mod h1:Vm07yUMys9XKnuZJLfTT8zluAN2n9ZOtz40Xb8RKh+8=
github.com/bluenviron/mediacommon/v2 v2.4.1 h1:PsKrO/c7hDjXxiOGRUBsYtMGNb4lKWIFea6zcOchoVs=
github.com/bluenviron/mediacommon/v2 v2.4.1/go.mod h1:a6MbPmXtYda9mKibKVMZlW20GYLLrX2R7ZkUE+1pwV0=
github.com/bluenviron/mediacommon/v2 v2.4.2-0.20250909112826-017d0bbe41db h1:yBxx462HsYC14/vKr5BF/Hlpso6WmyHzjwoE/W0td5s=
github.com/bluenviron/mediacommon/v2 v2.4.2-0.20250909112826-017d0bbe41db/go.mod h1:zy1fODPuS/kBd93ftgJS1Jhvjq7LFWfAo32KP7By9AE=
github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ=
github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=

View File

@@ -16,6 +16,7 @@ import (
"github.com/bluenviron/gortsplib/v4"
"github.com/bluenviron/gortsplib/v4/pkg/description"
"github.com/bluenviron/gortsplib/v4/pkg/format"
"github.com/bluenviron/mediacommon/v2/pkg/formats/mpegts"
srt "github.com/datarhei/gosrt"
"github.com/google/uuid"
@@ -441,13 +442,13 @@ func TestAPIProtocolListGet(t *testing.T) {
defer conn.Close()
w := &rtmp.Writer{
Conn: conn,
VideoTrack: test.FormatH264,
Conn: conn,
Tracks: []format.Format{test.FormatH264},
}
err = w.Initialize()
require.NoError(t, err)
err = w.WriteH264(2*time.Second, 2*time.Second, [][]byte{{5, 2, 3, 4}})
err = w.WriteH264(test.FormatH264, 2*time.Second, 2*time.Second, [][]byte{{5, 2, 3, 4}})
require.NoError(t, err)
time.Sleep(500 * time.Millisecond)
@@ -1034,13 +1035,13 @@ func TestAPIProtocolKick(t *testing.T) {
defer conn.Close()
w := &rtmp.Writer{
Conn: conn,
VideoTrack: test.FormatH264,
Conn: conn,
Tracks: []format.Format{test.FormatH264},
}
err = w.Initialize()
require.NoError(t, err)
err = w.WriteH264(2*time.Second, 2*time.Second, [][]byte{{5, 2, 3, 4}})
err = w.WriteH264(test.FormatH264, 2*time.Second, 2*time.Second, [][]byte{{5, 2, 3, 4}})
require.NoError(t, err)
case "webrtc":

View File

@@ -14,6 +14,7 @@ import (
"github.com/bluenviron/gortsplib/v4"
"github.com/bluenviron/gortsplib/v4/pkg/description"
"github.com/bluenviron/gortsplib/v4/pkg/format"
"github.com/bluenviron/mediacommon/v2/pkg/formats/mpegts"
srt "github.com/datarhei/gosrt"
"github.com/pion/rtp"
@@ -217,13 +218,13 @@ webrtc_sessions_rtcp_packets_sent 0
defer conn.Close()
w := &rtmp.Writer{
Conn: conn,
VideoTrack: test.FormatH264,
Conn: conn,
Tracks: []format.Format{test.FormatH264},
}
err2 = w.Initialize()
require.NoError(t, err2)
err2 = w.WriteH264(2*time.Second, 2*time.Second, [][]byte{{5, 2, 3, 4}})
err2 = w.WriteH264(test.FormatH264, 2*time.Second, 2*time.Second, [][]byte{{5, 2, 3, 4}})
require.NoError(t, err2)
<-terminate
@@ -245,13 +246,13 @@ webrtc_sessions_rtcp_packets_sent 0
defer conn.Close()
w := &rtmp.Writer{
Conn: conn,
VideoTrack: test.FormatH264,
Conn: conn,
Tracks: []format.Format{test.FormatH264},
}
err2 = w.Initialize()
require.NoError(t, err2)
err2 = w.WriteH264(2*time.Second, 2*time.Second, [][]byte{{5, 2, 3, 4}})
err2 = w.WriteH264(test.FormatH264, 2*time.Second, 2*time.Second, [][]byte{{5, 2, 3, 4}})
require.NoError(t, err2)
<-terminate

View File

@@ -7,6 +7,7 @@ import (
"github.com/bluenviron/gortsplib/v4/pkg/format"
"github.com/bluenviron/gortsplib/v4/pkg/format/rtpav1"
mcav1 "github.com/bluenviron/mediacommon/v2/pkg/codecs/av1"
"github.com/pion/rtp"
"github.com/bluenviron/mediamtx/internal/logger"
@@ -48,9 +49,44 @@ func (t *av1) createEncoder() error {
return t.encoder.Init()
}
func (t *av1) remuxTemporalUnit(tu [][]byte) [][]byte {
n := 0
for _, obu := range tu {
typ := mcav1.OBUType((obu[0] >> 3) & 0b1111)
if typ == mcav1.OBUTypeTemporalDelimiter {
continue
}
n++
}
if n == 0 {
return nil
}
filteredTU := make([][]byte, n)
i := 0
for _, obu := range tu {
typ := mcav1.OBUType((obu[0] >> 3) & 0b1111)
if typ == mcav1.OBUTypeTemporalDelimiter {
continue
}
filteredTU[i] = obu
i++
}
return filteredTU
}
func (t *av1) ProcessUnit(uu unit.Unit) error { //nolint:dupl
u := uu.(*unit.AV1)
u.TU = t.remuxTemporalUnit(u.TU)
pkts, err := t.encoder.Encode(u.TU)
if err != nil {
return err
@@ -106,7 +142,7 @@ func (t *av1) ProcessRTPPacket( //nolint:dupl
return nil, err
}
u.TU = tu
u.TU = t.remuxTemporalUnit(tu)
}
// route packet as is

View File

@@ -0,0 +1,34 @@
package formatprocessor
import (
"testing"
"github.com/bluenviron/gortsplib/v4/pkg/format"
mcav1 "github.com/bluenviron/mediacommon/v2/pkg/codecs/av1"
"github.com/bluenviron/mediamtx/internal/unit"
"github.com/stretchr/testify/require"
)
func TestAV1RemoveTUD(t *testing.T) {
forma := &format.AV1{}
p, err := New(1450, forma, true, nil)
require.NoError(t, err)
u := &unit.AV1{
Base: unit.Base{
PTS: 30000,
},
TU: [][]byte{
{byte(mcav1.OBUTypeTemporalDelimiter) << 3},
{5},
},
}
err = p.ProcessUnit(u)
require.NoError(t, err)
require.Equal(t, [][]byte{
{5},
}, u.TU)
}

View File

@@ -174,13 +174,13 @@ func (t *h264) remuxAccessUnit(au [][]byte) [][]byte {
typ := mch264.NALUType(nalu[0] & 0x1F)
switch typ {
case mch264.NALUTypeSPS, mch264.NALUTypePPS: // parameters: remove
case mch264.NALUTypeSPS, mch264.NALUTypePPS:
continue
case mch264.NALUTypeAccessUnitDelimiter: // AUD: remove
case mch264.NALUTypeAccessUnitDelimiter:
continue
case mch264.NALUTypeIDR: // key frame
case mch264.NALUTypeIDR:
if !isKeyFrame {
isKeyFrame = true
@@ -197,12 +197,12 @@ func (t *h264) remuxAccessUnit(au [][]byte) [][]byte {
return nil
}
filteredNALUs := make([][]byte, n)
filteredAU := make([][]byte, n)
i := 0
if isKeyFrame && t.Format.SPS != nil && t.Format.PPS != nil {
filteredNALUs[0] = t.Format.SPS
filteredNALUs[1] = t.Format.PPS
filteredAU[0] = t.Format.SPS
filteredAU[1] = t.Format.PPS
i = 2
}
@@ -217,11 +217,11 @@ func (t *h264) remuxAccessUnit(au [][]byte) [][]byte {
continue
}
filteredNALUs[i] = nalu
filteredAU[i] = nalu
i++
}
return filteredNALUs
return filteredAU
}
func (t *h264) ProcessUnit(uu unit.Unit) error {

View File

@@ -28,7 +28,31 @@ func Logger(cb func(logger.Level, string, ...interface{})) logger.Writer {
return &testLogger{cb: cb}
}
func TestH264ProcessUnit(t *testing.T) {
func TestH264RemoveAUD(t *testing.T) {
forma := &format.H264{}
p, err := New(1450, forma, true, nil)
require.NoError(t, err)
u := &unit.H264{
Base: unit.Base{
PTS: 30000,
},
AU: [][]byte{
{9, 24}, // AUD
{5, 1}, // IDR
},
}
err = p.ProcessUnit(u)
require.NoError(t, err)
require.Equal(t, [][]byte{
{5, 1}, // IDR
}, u.AU)
}
func TestH264AddParams(t *testing.T) {
forma := &format.H264{}
p, err := New(1450, forma, true, nil)
@@ -77,11 +101,11 @@ func TestH264ProcessUnit(t *testing.T) {
{5, 2}, // IDR
}, u2.AU)
// test that timestamp had increased
// test that timestamp has increased
require.Equal(t, u1.RTPPackets[0].Timestamp+30000, u2.RTPPackets[0].Timestamp)
}
func TestH264ProcessUnitEmpty(t *testing.T) {
func TestH264ProcessEmptyUnit(t *testing.T) {
forma := &format.H264{
PayloadTyp: 96,
PacketizationMode: 1,
@@ -104,7 +128,7 @@ func TestH264ProcessUnitEmpty(t *testing.T) {
require.Equal(t, []*rtp.Packet(nil), unit.RTPPackets)
}
func TestH264ProcessRTPPacketUpdateParams(t *testing.T) {
func TestH264RTPExtractParams(t *testing.T) {
for _, ca := range []string{"standard", "aggregated"} {
t.Run(ca, func(t *testing.T) {
forma := &format.H264{
@@ -169,7 +193,7 @@ func TestH264ProcessRTPPacketUpdateParams(t *testing.T) {
}
}
func TestH264ProcessRTPPacketOversized(t *testing.T) {
func TestH264RTPOversized(t *testing.T) {
forma := &format.H264{
PayloadTyp: 96,
SPS: []byte{0x01, 0x02, 0x03, 0x04},

View File

@@ -205,13 +205,13 @@ func (t *h265) remuxAccessUnit(au [][]byte) [][]byte {
typ := mch265.NALUType((nalu[0] >> 1) & 0b111111)
switch typ {
case mch265.NALUType_VPS_NUT, mch265.NALUType_SPS_NUT, mch265.NALUType_PPS_NUT: // parameters: remove
case mch265.NALUType_VPS_NUT, mch265.NALUType_SPS_NUT, mch265.NALUType_PPS_NUT:
continue
case mch265.NALUType_AUD_NUT: // AUD: remove
case mch265.NALUType_AUD_NUT:
continue
case mch265.NALUType_IDR_W_RADL, mch265.NALUType_IDR_N_LP, mch265.NALUType_CRA_NUT: // key frame
case mch265.NALUType_IDR_W_RADL, mch265.NALUType_IDR_N_LP, mch265.NALUType_CRA_NUT:
if !isKeyFrame {
isKeyFrame = true
@@ -228,13 +228,13 @@ func (t *h265) remuxAccessUnit(au [][]byte) [][]byte {
return nil
}
filteredNALUs := make([][]byte, n)
filteredAU := make([][]byte, n)
i := 0
if isKeyFrame && t.Format.VPS != nil && t.Format.SPS != nil && t.Format.PPS != nil {
filteredNALUs[0] = t.Format.VPS
filteredNALUs[1] = t.Format.SPS
filteredNALUs[2] = t.Format.PPS
filteredAU[0] = t.Format.VPS
filteredAU[1] = t.Format.SPS
filteredAU[2] = t.Format.PPS
i = 3
}
@@ -249,11 +249,11 @@ func (t *h265) remuxAccessUnit(au [][]byte) [][]byte {
continue
}
filteredNALUs[i] = nalu
filteredAU[i] = nalu
i++
}
return filteredNALUs
return filteredAU
}
func (t *h265) ProcessUnit(uu unit.Unit) error { //nolint:dupl

View File

@@ -15,7 +15,31 @@ import (
"github.com/bluenviron/mediamtx/internal/unit"
)
func TestH265ProcessUnit(t *testing.T) {
func TestH265RemoveAUD(t *testing.T) {
forma := &format.H265{}
p, err := New(1450, forma, true, nil)
require.NoError(t, err)
u := &unit.H265{
Base: unit.Base{
PTS: 30000,
},
AU: [][]byte{
{byte(mch265.NALUType_AUD_NUT) << 1, 0},
{byte(mch265.NALUType_CRA_NUT) << 1, 0},
},
}
err = p.ProcessUnit(u)
require.NoError(t, err)
require.Equal(t, [][]byte{
{byte(mch265.NALUType_CRA_NUT) << 1, 0},
}, u.AU)
}
func TestH265AddParams(t *testing.T) {
forma := &format.H265{}
p, err := New(1450, forma, true, nil)
@@ -68,11 +92,11 @@ func TestH265ProcessUnit(t *testing.T) {
{byte(mch265.NALUType_CRA_NUT) << 1, 1},
}, u2.AU)
// test that timestamp had increased
// test that timestamp has increased
require.Equal(t, u1.RTPPackets[0].Timestamp+30000, u2.RTPPackets[0].Timestamp)
}
func TestH265ProcessUnitEmpty(t *testing.T) {
func TestH265ProcessEmptyUnit(t *testing.T) {
forma := &format.H265{
PayloadTyp: 96,
}
@@ -95,7 +119,7 @@ func TestH265ProcessUnitEmpty(t *testing.T) {
require.Equal(t, []*rtp.Packet(nil), unit.RTPPackets)
}
func TestH265ProcessRTPPacketUpdateParams(t *testing.T) {
func TestH265RTPExtractParams(t *testing.T) {
for _, ca := range []string{"standard", "aggregated"} {
t.Run(ca, func(t *testing.T) {
forma := &format.H265{
@@ -168,7 +192,7 @@ func TestH265ProcessRTPPacketUpdateParams(t *testing.T) {
}
}
func TestH265ProcessRTPPacketOversized(t *testing.T) {
func TestH265RTPOversized(t *testing.T) {
forma := &format.H265{
PayloadTyp: 96,
VPS: []byte{byte(mch265.NALUType_VPS_NUT) << 1, 10, 11, 12},

View File

@@ -66,6 +66,6 @@ func TestMPEG4VideoProcessUnit(t *testing.T) {
0, 0, 1, 0xF1,
}, u2.Frame)
// test that timestamp had increased
// test that timestamp has increased
require.Equal(t, u1.RTPPackets[0].Timestamp+30000, u2.RTPPackets[0].Timestamp)
}

View File

@@ -69,10 +69,8 @@ func FromStream(
return nil
}
randomAccess := h265.IsRandomAccess(tunit.AU)
if dtsExtractor == nil {
if !randomAccess {
if !h265.IsRandomAccess(tunit.AU) {
return nil
}
dtsExtractor = &h265.DTSExtractor{}

View File

@@ -17,8 +17,25 @@ import (
"github.com/google/uuid"
)
// RTMP 1.0 spec, section 7.2.1.1
const (
supportSndNone = 0x0001
supportSndMP3 = 0x0004
supportSndG711A = 0x0080
supportSndG711U = 0x0100
supportSndAAV = 0x0400
supportVidH264 = 0x0080
encodingAMF0 = 0
)
var errAuth = errors.New("auth")
func fourCCToString(c message.FourCC) string {
return string([]byte{byte(c >> 24), byte(c >> 16), byte(c >> 8), byte(c)})
}
func resultIsOK1(res *message.CommandAMF0) bool {
if len(res.Arguments) < 2 {
return false
@@ -252,15 +269,45 @@ func (c *Client) initialize3() error {
{Key: "app", Value: app},
{Key: "flashVer", Value: "LNX 9,0,124,2"},
{Key: "tcUrl", Value: tcURL},
{Key: "objectEncoding", Value: float64(encodingAMF0)},
}
if !c.Publish {
connectArg = append(connectArg,
amf0.ObjectEntry{Key: "fpad", Value: false},
amf0.ObjectEntry{Key: "capabilities", Value: float64(15)},
amf0.ObjectEntry{Key: "audioCodecs", Value: float64(4071)},
amf0.ObjectEntry{Key: "videoCodecs", Value: float64(252)},
amf0.ObjectEntry{Key: "videoFunction", Value: float64(1)},
amf0.ObjectEntry{
Key: "fpad",
Value: false,
},
amf0.ObjectEntry{
Key: "capabilities",
Value: float64(15),
},
amf0.ObjectEntry{
Key: "audioCodecs",
Value: float64(
supportSndNone | supportSndMP3 | supportSndG711A | supportSndG711U | supportSndAAV),
},
amf0.ObjectEntry{
Key: "videoCodecs",
Value: float64(supportVidH264),
},
amf0.ObjectEntry{
Key: "videoFunction",
Value: float64(0),
},
amf0.ObjectEntry{
Key: "fourCcList",
Value: amf0.StrictArray{
fourCCToString(message.FourCCAV1),
fourCCToString(message.FourCCVP9),
fourCCToString(message.FourCCHEVC),
fourCCToString(message.FourCCAVC),
fourCCToString(message.FourCCOpus),
fourCCToString(message.FourCCAC3),
fourCCToString(message.FourCCMP4A),
fourCCToString(message.FourCCMP3),
},
},
)
}

View File

@@ -66,8 +66,8 @@ func TestClient(t *testing.T) {
require.NoError(t, err2)
switch authState {
case 0:
require.Equal(t, &message.CommandAMF0{
case 0: //nolint:dupl
require.Equal(t, &message.CommandAMF0{ //nolint:dupl
ChunkStreamID: 3,
Name: "connect",
CommandID: 1,
@@ -76,17 +76,28 @@ func TestClient(t *testing.T) {
{Key: "app", Value: "stream"},
{Key: "flashVer", Value: "LNX 9,0,124,2"},
{Key: "tcUrl", Value: "rtmp://127.0.0.1:9121/stream"},
{Key: "objectEncoding", Value: float64(0)},
{Key: "fpad", Value: false},
{Key: "capabilities", Value: float64(15)},
{Key: "audioCodecs", Value: float64(4071)},
{Key: "videoCodecs", Value: float64(252)},
{Key: "videoFunction", Value: float64(1)},
{Key: "audioCodecs", Value: float64(1413)},
{Key: "videoCodecs", Value: float64(128)},
{Key: "videoFunction", Value: float64(0)},
{Key: "fourCcList", Value: amf0.StrictArray{
"av01",
"vp09",
"hvc1",
"avc1",
"Opus",
"ac-3",
"mp4a",
".mp3",
}},
},
},
}, msg)
case 1:
require.Equal(t, &message.CommandAMF0{
case 1: //nolint:dupl
require.Equal(t, &message.CommandAMF0{ //nolint:dupl
ChunkStreamID: 3,
Name: "connect",
CommandID: 1,
@@ -95,11 +106,22 @@ func TestClient(t *testing.T) {
{Key: "app", Value: "stream?authmod=adobe&user=myuser"},
{Key: "flashVer", Value: "LNX 9,0,124,2"},
{Key: "tcUrl", Value: "rtmp://127.0.0.1:9121/stream?authmod=adobe&user=myuser"},
{Key: "objectEncoding", Value: float64(0)},
{Key: "fpad", Value: false},
{Key: "capabilities", Value: float64(15)},
{Key: "audioCodecs", Value: float64(4071)},
{Key: "videoCodecs", Value: float64(252)},
{Key: "videoFunction", Value: float64(1)},
{Key: "audioCodecs", Value: float64(1413)},
{Key: "videoCodecs", Value: float64(128)},
{Key: "videoFunction", Value: float64(0)},
{Key: "fourCcList", Value: amf0.StrictArray{
"av01",
"vp09",
"hvc1",
"avc1",
"Opus",
"ac-3",
"mp4a",
".mp3",
}},
},
},
}, msg)
@@ -127,11 +149,22 @@ func TestClient(t *testing.T) {
Value: "rtmp://127.0.0.1:9121/stream?authmod=adobe&user=myuser&challenge=" +
clientChallenge + "&response=" + response,
},
{Key: "objectEncoding", Value: float64(0)},
{Key: "fpad", Value: false},
{Key: "capabilities", Value: float64(15)},
{Key: "audioCodecs", Value: float64(4071)},
{Key: "videoCodecs", Value: float64(252)},
{Key: "videoFunction", Value: float64(1)},
{Key: "audioCodecs", Value: float64(1413)},
{Key: "videoCodecs", Value: float64(128)},
{Key: "videoFunction", Value: float64(0)},
{Key: "fourCcList", Value: amf0.StrictArray{
"av01",
"vp09",
"hvc1",
"avc1",
"Opus",
"ac-3",
"mp4a",
".mp3",
}},
},
},
}, msg)
@@ -140,7 +173,7 @@ func TestClient(t *testing.T) {
case "read", "read nginx rtmp":
msg, err2 = mrw.Read()
require.NoError(t, err2)
require.Equal(t, &message.CommandAMF0{
require.Equal(t, &message.CommandAMF0{ //nolint:dupl
ChunkStreamID: 3,
Name: "connect",
CommandID: 1,
@@ -149,11 +182,22 @@ func TestClient(t *testing.T) {
{Key: "app", Value: "stream"},
{Key: "flashVer", Value: "LNX 9,0,124,2"},
{Key: "tcUrl", Value: "rtmp://127.0.0.1:9121/stream"},
{Key: "objectEncoding", Value: float64(0)},
{Key: "fpad", Value: false},
{Key: "capabilities", Value: float64(15)},
{Key: "audioCodecs", Value: float64(4071)},
{Key: "videoCodecs", Value: float64(252)},
{Key: "videoFunction", Value: float64(1)},
{Key: "audioCodecs", Value: float64(1413)},
{Key: "videoCodecs", Value: float64(128)},
{Key: "videoFunction", Value: float64(0)},
{Key: "fourCcList", Value: amf0.StrictArray{
"av01",
"vp09",
"hvc1",
"avc1",
"Opus",
"ac-3",
"mp4a",
".mp3",
}},
},
},
}, msg)
@@ -170,6 +214,7 @@ func TestClient(t *testing.T) {
{Key: "app", Value: "stream"},
{Key: "flashVer", Value: "LNX 9,0,124,2"},
{Key: "tcUrl", Value: "rtmp://127.0.0.1:9121/stream"},
{Key: "objectEncoding", Value: float64(0)},
},
},
}, msg)
@@ -408,11 +453,11 @@ func TestClient(t *testing.T) {
switch ca {
case "read", "read nginx rtmp":
require.Equal(t, uint64(3421), conn.BytesReceived())
require.Equal(t, uint64(3409), conn.BytesSent())
require.Equal(t, uint64(0xdb3), conn.BytesSent())
case "publish":
require.Equal(t, uint64(3427), conn.BytesReceived())
require.Equal(t, uint64(0xd27), conn.BytesSent())
require.Equal(t, uint64(0xd40), conn.BytesSent())
}
<-done

View File

@@ -1,9 +1,6 @@
package rtmp
import (
"io"
"github.com/bluenviron/mediamtx/internal/protocols/rtmp/bytecounter"
"github.com/bluenviron/mediamtx/internal/protocols/rtmp/message"
)
@@ -14,33 +11,3 @@ type Conn interface {
Read() (message.Message, error)
Write(msg message.Message) error
}
type dummyConn struct {
rw io.ReadWriter
bc *bytecounter.ReadWriter
mrw *message.ReadWriter
}
func (c *dummyConn) initialize() {
c.bc = bytecounter.NewReadWriter(c.rw)
c.mrw = message.NewReadWriter(c.bc, c.bc, false)
}
// BytesReceived returns the number of bytes received.
func (c *dummyConn) BytesReceived() uint64 {
return c.bc.Reader.Count()
}
// BytesSent returns the number of bytes sent.
func (c *dummyConn) BytesSent() uint64 {
return c.bc.Writer.Count()
}
func (c *dummyConn) Read() (message.Message, error) {
return c.mrw.Read()
}
func (c *dummyConn) Write(msg message.Message) error {
return c.mrw.Write(msg)
}

View File

@@ -4,19 +4,25 @@ import (
"errors"
"fmt"
"net"
"slices"
"time"
"github.com/bluenviron/gortsplib/v4/pkg/format"
"github.com/bluenviron/mediacommon/v2/pkg/codecs/ac3"
"github.com/bluenviron/mediacommon/v2/pkg/codecs/h264"
"github.com/bluenviron/mediacommon/v2/pkg/codecs/h265"
"github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg1audio"
"github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio"
"github.com/bluenviron/mediacommon/v2/pkg/codecs/opus"
"github.com/bluenviron/mediamtx/internal/logger"
"github.com/bluenviron/mediamtx/internal/protocols/rtmp/message"
"github.com/bluenviron/mediamtx/internal/stream"
"github.com/bluenviron/mediamtx/internal/unit"
)
var errNoSupportedCodecsFrom = errors.New(
"the stream doesn't contain any supported codec, which are currently H264, MPEG-4 Audio, MPEG-1/2 Audio")
"the stream doesn't contain any supported codec, which are currently " +
"AV1, VP9, H265, H264, Opus, MPEG-4 Audio, MPEG-1/2 Audio, AC-3, G711, LPCM")
func multiplyAndDivide2(v, m, d time.Duration) time.Duration {
secs := v / d
@@ -28,227 +34,391 @@ func timestampToDuration(t int64, clockRate int) time.Duration {
return multiplyAndDivide2(time.Duration(t), time.Second, time.Duration(clockRate))
}
func setupVideo(
str *stream.Stream,
reader stream.Reader,
w **Writer,
nconn net.Conn,
writeTimeout time.Duration,
) format.Format {
var videoFormatH264 *format.H264
videoMedia := str.Desc.FindFormat(&videoFormatH264)
if videoFormatH264 != nil {
var videoDTSExtractor *h264.DTSExtractor
str.AddReader(
reader,
videoMedia,
videoFormatH264,
func(u unit.Unit) error {
tunit := u.(*unit.H264)
if tunit.AU == nil {
return nil
}
idrPresent := false
nonIDRPresent := false
for _, nalu := range tunit.AU {
typ := h264.NALUType(nalu[0] & 0x1F)
switch typ {
case h264.NALUTypeIDR:
idrPresent = true
case h264.NALUTypeNonIDR:
nonIDRPresent = true
}
}
// wait until we receive an IDR
if videoDTSExtractor == nil {
if !idrPresent {
return nil
}
videoDTSExtractor = &h264.DTSExtractor{}
videoDTSExtractor.Initialize()
} else if !idrPresent && !nonIDRPresent {
return nil
}
dts, err := videoDTSExtractor.Extract(tunit.AU, tunit.PTS)
if err != nil {
return err
}
nconn.SetWriteDeadline(time.Now().Add(writeTimeout))
return (*w).WriteH264(
timestampToDuration(tunit.PTS, videoFormatH264.ClockRate()),
timestampToDuration(dts, videoFormatH264.ClockRate()),
tunit.AU)
})
return videoFormatH264
}
return nil
}
func setupAudio(
str *stream.Stream,
reader stream.Reader,
w **Writer,
nconn net.Conn,
writeTimeout time.Duration,
) format.Format {
var audioFormatMPEG4Audio *format.MPEG4Audio
audioMedia := str.Desc.FindFormat(&audioFormatMPEG4Audio)
if audioMedia != nil {
str.AddReader(
reader,
audioMedia,
audioFormatMPEG4Audio,
func(u unit.Unit) error {
tunit := u.(*unit.MPEG4Audio)
if tunit.AUs == nil {
return nil
}
for i, au := range tunit.AUs {
pts := tunit.PTS + int64(i)*mpeg4audio.SamplesPerAccessUnit
nconn.SetWriteDeadline(time.Now().Add(writeTimeout))
err := (*w).WriteMPEG4Audio(
timestampToDuration(pts, audioFormatMPEG4Audio.ClockRate()),
au,
)
if err != nil {
return err
}
}
return nil
})
return audioFormatMPEG4Audio
}
var audioFormatMPEG4AudioLATM *format.MPEG4AudioLATM
audioMedia = str.Desc.FindFormat(&audioFormatMPEG4AudioLATM)
if audioMedia != nil && !audioFormatMPEG4AudioLATM.CPresent {
str.AddReader(
reader,
audioMedia,
audioFormatMPEG4AudioLATM,
func(u unit.Unit) error {
tunit := u.(*unit.MPEG4AudioLATM)
if tunit.Element == nil {
return nil
}
var ame mpeg4audio.AudioMuxElement
ame.StreamMuxConfig = audioFormatMPEG4AudioLATM.StreamMuxConfig
err := ame.Unmarshal(tunit.Element)
if err != nil {
return err
}
nconn.SetWriteDeadline(time.Now().Add(writeTimeout))
return (*w).WriteMPEG4Audio(
timestampToDuration(tunit.PTS, audioFormatMPEG4AudioLATM.ClockRate()),
ame.Payloads[0][0][0],
)
})
return audioFormatMPEG4AudioLATM
}
var audioFormatMPEG1 *format.MPEG1Audio
audioMedia = str.Desc.FindFormat(&audioFormatMPEG1)
if audioMedia != nil {
str.AddReader(
reader,
audioMedia,
audioFormatMPEG1,
func(u unit.Unit) error {
tunit := u.(*unit.MPEG1Audio)
pts := tunit.PTS
for _, frame := range tunit.Frames {
var h mpeg1audio.FrameHeader
err := h.Unmarshal(frame)
if err != nil {
return err
}
if h.MPEG2 || h.Layer != 3 {
return fmt.Errorf("RTMP only supports MPEG-1 layer 3 audio")
}
nconn.SetWriteDeadline(time.Now().Add(writeTimeout))
err = (*w).WriteMPEG1Audio(
timestampToDuration(pts, audioFormatMPEG1.ClockRate()),
&h,
frame)
if err != nil {
return err
}
pts += int64(h.SampleCount()) *
int64(audioFormatMPEG1.ClockRate()) / int64(h.SampleRate)
}
return nil
})
return audioFormatMPEG1
}
return nil
}
// FromStream maps a MediaMTX stream to a RTMP stream.
func FromStream(
str *stream.Stream,
reader stream.Reader,
conn Conn,
conn *ServerConn,
nconn net.Conn,
writeTimeout time.Duration,
) error {
var tracks []format.Format
var w *Writer
videoFormat := setupVideo(
str,
reader,
&w,
nconn,
writeTimeout,
)
for _, media := range str.Desc.Medias {
for _, forma := range media.Formats {
switch forma := forma.(type) {
case *format.AV1:
if slices.Contains(conn.FourCcList, interface{}(fourCCToString(message.FourCCAV1))) {
str.AddReader(
reader,
media,
forma,
func(u unit.Unit) error {
tunit := u.(*unit.AV1)
audioFormat := setupAudio(
str,
reader,
&w,
nconn,
writeTimeout,
)
if tunit.TU == nil {
return nil
}
if videoFormat == nil && audioFormat == nil {
nconn.SetWriteDeadline(time.Now().Add(writeTimeout))
return (*w).WriteAV1(
forma,
timestampToDuration(tunit.PTS, forma.ClockRate()),
timestampToDuration(tunit.PTS, forma.ClockRate()),
tunit.TU)
})
tracks = append(tracks, forma)
}
case *format.VP9:
if slices.Contains(conn.FourCcList, interface{}(fourCCToString(message.FourCCVP9))) {
str.AddReader(
reader,
media,
forma,
func(u unit.Unit) error {
tunit := u.(*unit.VP9)
if tunit.Frame == nil {
return nil
}
nconn.SetWriteDeadline(time.Now().Add(writeTimeout))
return (*w).WriteVP9(
forma,
timestampToDuration(tunit.PTS, forma.ClockRate()),
timestampToDuration(tunit.PTS, forma.ClockRate()),
tunit.Frame)
})
tracks = append(tracks, forma)
}
case *format.H265:
if slices.Contains(conn.FourCcList, interface{}(fourCCToString(message.FourCCHEVC))) {
var videoDTSExtractor *h265.DTSExtractor
str.AddReader(
reader,
media,
forma,
func(u unit.Unit) error {
tunit := u.(*unit.H265)
if tunit.AU == nil {
return nil
}
if videoDTSExtractor == nil {
if !h265.IsRandomAccess(tunit.AU) {
return nil
}
videoDTSExtractor = &h265.DTSExtractor{}
videoDTSExtractor.Initialize()
}
dts, err := videoDTSExtractor.Extract(tunit.AU, tunit.PTS)
if err != nil {
return err
}
nconn.SetWriteDeadline(time.Now().Add(writeTimeout))
return (*w).WriteH265(
forma,
timestampToDuration(tunit.PTS, forma.ClockRate()),
timestampToDuration(dts, forma.ClockRate()),
tunit.AU)
})
tracks = append(tracks, forma)
}
case *format.H264:
var videoDTSExtractor *h264.DTSExtractor
str.AddReader(
reader,
media,
forma,
func(u unit.Unit) error {
tunit := u.(*unit.H264)
if tunit.AU == nil {
return nil
}
idrPresent := false
nonIDRPresent := false
for _, nalu := range tunit.AU {
typ := h264.NALUType(nalu[0] & 0x1F)
switch typ {
case h264.NALUTypeIDR:
idrPresent = true
case h264.NALUTypeNonIDR:
nonIDRPresent = true
}
}
// wait until we receive an IDR
if videoDTSExtractor == nil {
if !idrPresent {
return nil
}
videoDTSExtractor = &h264.DTSExtractor{}
videoDTSExtractor.Initialize()
} else if !idrPresent && !nonIDRPresent {
return nil
}
dts, err := videoDTSExtractor.Extract(tunit.AU, tunit.PTS)
if err != nil {
return err
}
nconn.SetWriteDeadline(time.Now().Add(writeTimeout))
return (*w).WriteH264(
forma,
timestampToDuration(tunit.PTS, forma.ClockRate()),
timestampToDuration(dts, forma.ClockRate()),
tunit.AU)
})
tracks = append(tracks, forma)
case *format.Opus:
if slices.Contains(conn.FourCcList, interface{}(fourCCToString(message.FourCCOpus))) {
str.AddReader(
reader,
media,
forma,
func(u unit.Unit) error {
tunit := u.(*unit.Opus)
if tunit.Packets == nil {
return nil
}
pts := tunit.PTS
for _, pkt := range tunit.Packets {
nconn.SetWriteDeadline(time.Now().Add(writeTimeout))
err := (*w).WriteOpus(
forma,
timestampToDuration(pts, forma.ClockRate()),
pkt,
)
if err != nil {
return err
}
pts += opus.PacketDuration2(pkt)
}
return nil
})
tracks = append(tracks, forma)
}
case *format.MPEG4Audio:
str.AddReader(
reader,
media,
forma,
func(u unit.Unit) error {
tunit := u.(*unit.MPEG4Audio)
if tunit.AUs == nil {
return nil
}
for i, au := range tunit.AUs {
pts := tunit.PTS + int64(i)*mpeg4audio.SamplesPerAccessUnit
nconn.SetWriteDeadline(time.Now().Add(writeTimeout))
err := (*w).WriteMPEG4Audio(
forma,
timestampToDuration(pts, forma.ClockRate()),
au,
)
if err != nil {
return err
}
}
return nil
})
tracks = append(tracks, forma)
case *format.MPEG4AudioLATM:
if !forma.CPresent {
str.AddReader(
reader,
media,
forma,
func(u unit.Unit) error {
tunit := u.(*unit.MPEG4AudioLATM)
if tunit.Element == nil {
return nil
}
var ame mpeg4audio.AudioMuxElement
ame.StreamMuxConfig = forma.StreamMuxConfig
err := ame.Unmarshal(tunit.Element)
if err != nil {
return err
}
nconn.SetWriteDeadline(time.Now().Add(writeTimeout))
return (*w).WriteMPEG4Audio(
forma,
timestampToDuration(tunit.PTS, forma.ClockRate()),
ame.Payloads[0][0][0],
)
})
tracks = append(tracks, forma)
}
case *format.MPEG1Audio:
// TODO: check sample rate and layer,
// unfortunately they are not available at this stage.
str.AddReader(
reader,
media,
forma,
func(u unit.Unit) error {
tunit := u.(*unit.MPEG1Audio)
pts := tunit.PTS
for _, frame := range tunit.Frames {
var h mpeg1audio.FrameHeader
err := h.Unmarshal(frame)
if err != nil {
return err
}
if h.MPEG2 || h.Layer != 3 {
return fmt.Errorf("RTMP only supports MPEG-1 layer 3 audio")
}
nconn.SetWriteDeadline(time.Now().Add(writeTimeout))
err = (*w).WriteMPEG1Audio(
forma,
timestampToDuration(pts, forma.ClockRate()),
&h,
frame)
if err != nil {
return err
}
pts += int64(h.SampleCount()) *
int64(forma.ClockRate()) / int64(h.SampleRate)
}
return nil
})
tracks = append(tracks, forma)
case *format.AC3:
if slices.Contains(conn.FourCcList, interface{}(fourCCToString(message.FourCCAC3))) {
str.AddReader(
reader,
media,
forma,
func(u unit.Unit) error {
tunit := u.(*unit.AC3)
for i, frame := range tunit.Frames {
pts := tunit.PTS + int64(i)*ac3.SamplesPerFrame
nconn.SetWriteDeadline(time.Now().Add(writeTimeout))
err := (*w).WriteAC3(
forma,
timestampToDuration(pts, forma.ClockRate()),
frame)
if err != nil {
return err
}
}
return nil
})
tracks = append(tracks, forma)
}
case *format.G711:
if forma.SampleRate == 8000 {
str.AddReader(
reader,
media,
forma,
func(u unit.Unit) error {
tunit := u.(*unit.G711)
if tunit.Samples == nil {
return nil
}
nconn.SetWriteDeadline(time.Now().Add(writeTimeout))
return (*w).WriteG711(
forma,
timestampToDuration(tunit.PTS, forma.ClockRate()),
tunit.Samples,
)
})
tracks = append(tracks, forma)
}
case *format.LPCM:
if (forma.ChannelCount == 1 || forma.ChannelCount == 2) &&
(forma.SampleRate == 5512 ||
forma.SampleRate == 11025 ||
forma.SampleRate == 22050 ||
forma.SampleRate == 44100) {
str.AddReader(
reader,
media,
forma,
func(u unit.Unit) error {
tunit := u.(*unit.LPCM)
if tunit.Samples == nil {
return nil
}
nconn.SetWriteDeadline(time.Now().Add(writeTimeout))
return (*w).WriteLPCM(
forma,
timestampToDuration(tunit.PTS, forma.ClockRate()),
tunit.Samples,
)
})
tracks = append(tracks, forma)
}
}
}
}
if len(tracks) == 0 {
return errNoSupportedCodecsFrom
}
w = &Writer{
Conn: conn,
VideoTrack: videoFormat,
AudioTrack: audioFormat,
Conn: conn,
Tracks: tracks,
}
err := w.Initialize()
if err != nil {
@@ -258,7 +428,7 @@ func FromStream(
n := 1
for _, media := range str.Desc.Medias {
for _, forma := range media.Formats {
if forma != videoFormat && forma != audioFormat {
if !slices.Contains(tracks, forma) {
reader.Log(logger.Warn, "skipping track %d (%s)", n, forma.Codec())
}
n++

View File

@@ -1,18 +1,628 @@
package rtmp
import (
"bytes"
"context"
"fmt"
"net"
"net/url"
"testing"
"time"
"github.com/bluenviron/gortsplib/v4/pkg/description"
"github.com/bluenviron/gortsplib/v4/pkg/format"
"github.com/bluenviron/mediamtx/internal/formatprocessor"
"github.com/bluenviron/mediamtx/internal/logger"
"github.com/bluenviron/mediamtx/internal/stream"
"github.com/bluenviron/mediamtx/internal/test"
"github.com/bluenviron/mediamtx/internal/unit"
"github.com/stretchr/testify/require"
)
func TestFromStream(t *testing.T) {
for _, ca := range []string{
"h264 + aac",
"av1",
"vp9",
"h265",
"h264",
"opus",
"aac",
"mp3",
"ac-3",
"pcma",
"pcmu",
"lpcm",
"h265 + h264 + vp9 + av1 + opus + aac",
} {
t.Run(ca, func(t *testing.T) {
var medias []*description.Media
switch ca {
case "h264 + aac":
medias = []*description.Media{
{
Formats: []format.Format{test.FormatH264},
},
{
Formats: []format.Format{test.FormatMPEG4Audio},
},
}
case "av1":
medias = []*description.Media{
{
Formats: []format.Format{&format.AV1{
PayloadTyp: 96,
}},
},
}
case "vp9":
medias = []*description.Media{
{
Formats: []format.Format{&format.VP9{
PayloadTyp: 96,
}},
},
}
case "h265":
medias = []*description.Media{
{
Formats: []format.Format{test.FormatH265},
},
}
case "h264":
medias = []*description.Media{
{
Formats: []format.Format{test.FormatH264},
},
}
case "opus":
medias = []*description.Media{
{
Formats: []format.Format{&format.Opus{
PayloadTyp: 96,
ChannelCount: 2,
}},
},
}
case "aac":
medias = []*description.Media{
{
Formats: []format.Format{test.FormatMPEG4Audio},
},
}
case "mp3":
medias = []*description.Media{
{
Formats: []format.Format{&format.MPEG1Audio{}},
},
}
case "ac-3":
medias = []*description.Media{
{
Formats: []format.Format{&format.AC3{
SampleRate: 44100,
ChannelCount: 2,
}},
},
}
case "pcma":
medias = []*description.Media{
{
Formats: []format.Format{&format.G711{
MULaw: false,
SampleRate: 8000,
ChannelCount: 1,
}},
},
}
case "pcmu":
medias = []*description.Media{
{
Formats: []format.Format{&format.G711{
MULaw: true,
SampleRate: 8000,
ChannelCount: 1,
}},
},
}
case "lpcm":
medias = []*description.Media{
{
Formats: []format.Format{&format.LPCM{
BitDepth: 16,
SampleRate: 44100,
ChannelCount: 2,
}},
},
}
case "h265 + h264 + vp9 + av1 + opus + aac":
medias = []*description.Media{
{
Formats: []format.Format{&format.H265{}},
},
{
Formats: []format.Format{&format.H264{}},
},
{
Formats: []format.Format{&format.VP9{}},
},
{
Formats: []format.Format{&format.AV1{}},
},
{
Formats: []format.Format{&format.Opus{
PayloadTyp: 96,
ChannelCount: 2,
}},
},
{
Formats: []format.Format{&format.MPEG4Audio{
PayloadTyp: 96,
Config: test.FormatMPEG4Audio.Config,
SizeLength: 13,
IndexLength: 3,
IndexDeltaLength: 3,
}},
},
}
}
strm := &stream.Stream{
WriteQueueSize: 512,
RTPMaxPayloadSize: 1450,
Desc: &description.Session{Medias: medias},
GenerateRTPPackets: true,
Parent: test.NilLogger,
}
err := strm.Initialize()
require.NoError(t, err)
ln, err := net.Listen("tcp", "127.0.0.1:9121")
require.NoError(t, err)
defer ln.Close()
done := make(chan struct{})
go func() {
u, err2 := url.Parse("rtmp://127.0.0.1:9121/stream")
require.NoError(t, err2)
c := &Client{
URL: u,
}
err2 = c.Initialize(context.Background())
require.NoError(t, err2)
r := &Reader{
Conn: c,
}
err2 = r.Initialize()
require.NoError(t, err2)
switch ca {
case "h264 + aac":
require.Equal(t, []format.Format{
&format.H264{
SPS: test.FormatH264.SPS,
PPS: test.FormatH264.PPS,
PacketizationMode: 1,
PayloadTyp: 96,
},
&format.MPEG4Audio{
PayloadTyp: 96,
Config: test.FormatMPEG4Audio.Config,
SizeLength: 13,
IndexLength: 3,
IndexDeltaLength: 3,
},
}, r.Tracks())
case "av1":
require.Equal(t, []format.Format{
&format.AV1{
PayloadTyp: 96,
},
}, r.Tracks())
case "vp9":
require.Equal(t, []format.Format{
&format.VP9{
PayloadTyp: 96,
},
}, r.Tracks())
case "h265":
require.Equal(t, []format.Format{
&format.H265{
VPS: test.FormatH265.VPS,
SPS: test.FormatH265.SPS,
PPS: test.FormatH265.PPS,
PayloadTyp: 96,
},
}, r.Tracks())
case "h264":
require.Equal(t, []format.Format{
&format.H264{
SPS: test.FormatH264.SPS,
PPS: test.FormatH264.PPS,
PacketizationMode: 1,
PayloadTyp: 96,
},
}, r.Tracks())
case "opus":
require.Equal(t, []format.Format{
&format.Opus{
PayloadTyp: 96,
ChannelCount: 2,
},
}, r.Tracks())
case "aac":
require.Equal(t, []format.Format{
&format.MPEG4Audio{
PayloadTyp: 96,
Config: test.FormatMPEG4Audio.Config,
SizeLength: 13,
IndexLength: 3,
IndexDeltaLength: 3,
},
}, r.Tracks())
case "mp3":
require.Equal(t, []format.Format{
&format.MPEG1Audio{},
}, r.Tracks())
case "ac-3":
require.Equal(t, []format.Format{
&format.AC3{
PayloadTyp: 96,
SampleRate: 48000,
ChannelCount: 1,
},
}, r.Tracks())
case "pcma":
require.Equal(t, []format.Format{
&format.G711{
PayloadTyp: 8,
MULaw: false,
ChannelCount: 1,
SampleRate: 8000,
},
}, r.Tracks())
case "pcmu":
require.Equal(t, []format.Format{
&format.G711{
MULaw: true,
ChannelCount: 1,
SampleRate: 8000,
},
}, r.Tracks())
case "lpcm":
require.Equal(t, []format.Format{
&format.LPCM{
PayloadTyp: 96,
BitDepth: 16,
SampleRate: 44100,
ChannelCount: 2,
},
}, r.Tracks())
case "h265 + h264 + vp9 + av1 + opus + aac":
require.Equal(t, []format.Format{
&format.H265{
PayloadTyp: 96,
VPS: formatprocessor.H265DefaultVPS,
SPS: formatprocessor.H265DefaultSPS,
PPS: formatprocessor.H265DefaultPPS,
},
&format.H264{
PayloadTyp: 96,
PacketizationMode: 1,
SPS: formatprocessor.H264DefaultSPS,
PPS: formatprocessor.H264DefaultPPS,
},
&format.VP9{
PayloadTyp: 96,
},
&format.AV1{
PayloadTyp: 96,
},
&format.Opus{
PayloadTyp: 96,
ChannelCount: 2,
},
&format.MPEG4Audio{
PayloadTyp: 96,
Config: test.FormatMPEG4Audio.Config,
SizeLength: 13,
IndexLength: 3,
IndexDeltaLength: 3,
},
}, r.Tracks())
}
close(done)
}()
nconn, err := ln.Accept()
require.NoError(t, err)
defer nconn.Close()
conn := &ServerConn{
RW: nconn,
}
err = conn.Initialize()
require.NoError(t, err)
err = conn.Accept()
require.NoError(t, err)
reader := test.NilLogger
err = FromStream(strm, reader, conn, nconn, 10*time.Second)
require.NoError(t, err)
defer strm.RemoveReader(reader)
strm.StartReader(reader)
switch ca {
case "h264 + aac":
strm.WriteUnit(medias[0], medias[0].Formats[0], &unit.H264{
Base: unit.Base{
PTS: 0,
},
AU: [][]byte{
{5, 2}, // IDR
},
})
strm.WriteUnit(medias[1], medias[1].Formats[0], &unit.MPEG4Audio{
Base: unit.Base{
PTS: 90000 * 5,
},
AUs: [][]byte{
{3, 4},
},
})
case "av1":
for i := range 2 {
strm.WriteUnit(medias[0], medias[0].Formats[0], &unit.AV1{
Base: unit.Base{
PTS: 90000 * 2 * int64(i),
},
TU: [][]byte{{
0x0a, 0x0e, 0x00, 0x00, 0x00, 0x4a, 0xab, 0xbf,
0xc3, 0x77, 0x6b, 0xe4, 0x40, 0x40, 0x40, 0x41,
}},
})
}
case "vp9":
for i := range 2 {
strm.WriteUnit(medias[0], medias[0].Formats[0], &unit.VP9{
Base: unit.Base{
PTS: 90000 * 2 * int64(i),
},
Frame: []byte{1, 2},
})
}
case "h265":
for i := range 2 {
strm.WriteUnit(medias[0], medias[0].Formats[0], &unit.H265{
Base: unit.Base{
PTS: 90000 * 2 * int64(i),
},
AU: [][]byte{
{0x26, 0x1, 0xaf, 0x8, 0x42, 0x23, 0x48, 0x8a, 0x43, 0xe2},
},
})
}
case "h264":
for i := range 2 {
strm.WriteUnit(medias[0], medias[0].Formats[0], &unit.H264{
Base: unit.Base{
PTS: 90000 * 2 * int64(i),
},
AU: [][]byte{
{5, 2}, // IDR
},
})
}
case "opus":
for i := range 2 {
strm.WriteUnit(medias[0], medias[0].Formats[0], &unit.Opus{
Base: unit.Base{
PTS: 90000 * 5 * int64(i),
},
Packets: [][]byte{
{3, 4},
},
})
}
case "aac":
for i := range 2 {
strm.WriteUnit(medias[0], medias[0].Formats[0], &unit.MPEG4Audio{
Base: unit.Base{
PTS: 90000 * 5 * int64(i),
},
AUs: [][]byte{
{3, 4},
},
})
}
case "mp3":
for i := range 2 {
strm.WriteUnit(medias[0], medias[0].Formats[0], &unit.MPEG1Audio{
Base: unit.Base{
PTS: 90000 * 5 * int64(i),
},
Frames: [][]byte{
{
0xff, 0xfa, 0x52, 0x04, 0x00,
},
},
})
}
case "ac-3":
for i := range 2 {
strm.WriteUnit(medias[0], medias[0].Formats[0], &unit.AC3{
Base: unit.Base{
PTS: 90000 * 5 * int64(i),
},
Frames: [][]byte{
{
0x0b, 0x77, 0x47, 0x11, 0x0c, 0x40, 0x2f, 0x84,
0x2b, 0xc1, 0x07, 0x7a, 0xb0, 0xfa, 0xbb, 0xea,
0xef, 0x9f, 0x57, 0x7c, 0xf9, 0xf3, 0xf7, 0xcf,
0x9f, 0x3e, 0x32, 0xfe, 0xd5, 0xc1, 0x50, 0xde,
0xc5, 0x1e, 0x73, 0xd2, 0x6c, 0xa6, 0x94, 0x46,
0x4e, 0x92, 0x8c, 0x0f, 0xb9, 0xcf, 0xad, 0x07,
0x54, 0x4a, 0x2e, 0xf3, 0x7d, 0x07, 0x2e, 0xa4,
0x2f, 0xba, 0xbf, 0x39, 0xb5, 0xc9, 0x92, 0xa6,
0xe1, 0xb4, 0x70, 0xc5, 0xc4, 0xb5, 0xe6, 0x5d,
0x0f, 0xa8, 0x71, 0xa4, 0xcc, 0xc5, 0xbc, 0x75,
0x67, 0x92, 0x52, 0x4f, 0x7e, 0x62, 0x1c, 0xa9,
0xd9, 0xb5, 0x19, 0x6a, 0xd7, 0xb0, 0x44, 0x92,
0x30, 0x3b, 0xf7, 0x61, 0xd6, 0x49, 0x96, 0x66,
0x98, 0x28, 0x1a, 0x95, 0xa9, 0x42, 0xad, 0xb7,
0x50, 0x90, 0xad, 0x1c, 0x34, 0x80, 0xe2, 0xef,
0xcd, 0x41, 0x0b, 0xf0, 0x9d, 0x57, 0x62, 0x78,
0xfd, 0xc6, 0xc2, 0x19, 0x9e, 0x26, 0x31, 0xca,
0x1e, 0x75, 0xb1, 0x7a, 0x8e, 0xb5, 0x51, 0x3a,
0xfe, 0xe4, 0xf1, 0x0b, 0x4f, 0x14, 0x90, 0xdb,
0x9f, 0x44, 0x50, 0xbb, 0xef, 0x74, 0x00, 0x8c,
0x1f, 0x97, 0xa1, 0xa2, 0xfa, 0x72, 0x16, 0x47,
0xc6, 0xc0, 0xe5, 0xfe, 0x67, 0x03, 0x9c, 0xfe,
0x62, 0x01, 0xa1, 0x00, 0x5d, 0xff, 0xa5, 0x03,
0x59, 0xfa, 0xa8, 0x25, 0x5f, 0x6b, 0x83, 0x51,
0xf2, 0xc0, 0x44, 0xff, 0x2d, 0x05, 0x4b, 0xee,
0xe0, 0x54, 0x9e, 0xae, 0x86, 0x45, 0xf3, 0xbd,
0x0e, 0x42, 0xf2, 0xbf, 0x0f, 0x7f, 0xc6, 0x09,
0x07, 0xdc, 0x22, 0x11, 0x77, 0xbe, 0x31, 0x27,
0x5b, 0xa4, 0x13, 0x47, 0x07, 0x32, 0x9f, 0x1f,
0xcb, 0xb0, 0xdf, 0x3e, 0x7d, 0x0d, 0xf3, 0xe7,
0xcf, 0x9f, 0x3e, 0xae, 0xf9, 0xf3, 0xe7, 0xcf,
0x9f, 0x3e, 0x85, 0x5d, 0xf3, 0xe7, 0xcf, 0x9f,
0x3e, 0x7c, 0xf9, 0xf3, 0xe7, 0xcf, 0x9f, 0x3f,
0x53, 0x5d, 0xf3, 0xe7, 0xcf, 0x9f, 0x3e, 0x7c,
0xf9, 0xf3, 0xe7, 0xcf, 0x9f, 0x3e, 0x7c, 0xf9,
0xf3, 0xe7, 0xcf, 0x9f, 0x3e, 0x7c, 0xf9, 0xf3,
0xe7, 0xcf, 0x9f, 0x3e, 0x00, 0x46, 0x28, 0x26,
0x20, 0x4a, 0x5a, 0xc0, 0x8a, 0xc5, 0xae, 0xa0,
0x55, 0x78, 0x82, 0x7a, 0x38, 0x10, 0x09, 0xc9,
0xb8, 0x0c, 0xfa, 0x5b, 0xc9, 0xd2, 0xec, 0x44,
0x25, 0xf8, 0x20, 0xf2, 0xc8, 0x8a, 0xe9, 0x40,
0x18, 0x06, 0xc6, 0x2b, 0xc8, 0xed, 0x8f, 0x33,
0x09, 0x92, 0x28, 0x1e, 0xc4, 0x24, 0xd8, 0x33,
0xa5, 0x00, 0xf5, 0xea, 0x18, 0xfa, 0x90, 0x97,
0x97, 0xe8, 0x39, 0x6a, 0xcf, 0xf1, 0xdd, 0xff,
0x9e, 0x8e, 0x04, 0x02, 0xae, 0x65, 0x87, 0x5c,
0x4e, 0x72, 0xfd, 0x3c, 0x01, 0x86, 0xfe, 0x56,
0x59, 0x74, 0x44, 0x3a, 0x40, 0x00, 0xec, 0xfc,
},
},
})
}
case "pcma", "pcmu":
for i := range 2 {
strm.WriteUnit(medias[0], medias[0].Formats[0], &unit.G711{
Base: unit.Base{
PTS: 90000 * 5 * int64(i),
},
Samples: []byte{
3, 4,
},
})
}
case "lpcm":
for i := range 2 {
strm.WriteUnit(medias[0], medias[0].Formats[0], &unit.LPCM{
Base: unit.Base{
PTS: 90000 * 5 * int64(i),
},
Samples: []byte{
3, 4, 5, 6,
},
})
}
case "h265 + h264 + vp9 + av1 + opus + aac":
strm.WriteUnit(medias[0], medias[0].Formats[0], &unit.H265{
AU: [][]byte{
formatprocessor.H265DefaultVPS,
formatprocessor.H265DefaultSPS,
formatprocessor.H265DefaultPPS,
{0x26, 0x1, 0xaf, 0x8, 0x42, 0x23, 0x48, 0x8a, 0x43, 0xe2},
},
})
strm.WriteUnit(medias[1], medias[1].Formats[0], &unit.H264{
AU: [][]byte{
formatprocessor.H264DefaultSPS,
formatprocessor.H264DefaultPPS,
{5, 2}, // IDR
},
})
strm.WriteUnit(medias[2], medias[2].Formats[0], &unit.VP9{
Frame: []byte{1, 2},
})
strm.WriteUnit(medias[3], medias[3].Formats[0], &unit.AV1{
TU: [][]byte{{
0x0a, 0x0e, 0x00, 0x00, 0x00, 0x4a, 0xab, 0xbf,
0xc3, 0x77, 0x6b, 0xe4, 0x40, 0x40, 0x40, 0x41,
}},
})
strm.WriteUnit(medias[4], medias[4].Formats[0], &unit.Opus{
Packets: [][]byte{
{3, 4},
},
})
strm.WriteUnit(medias[5], medias[5].Formats[0], &unit.MPEG4Audio{
Base: unit.Base{
PTS: 90000 * 5,
},
AUs: [][]byte{
{3, 4},
},
})
}
<-done
})
}
}
func TestFromStreamNoSupportedCodecs(t *testing.T) {
strm := &stream.Stream{
WriteQueueSize: 512,
@@ -48,10 +658,6 @@ func TestFromStreamSkipUnsupportedTracks(t *testing.T) {
Type: description.MediaTypeVideo,
Formats: []format.Format{&format.H264{}},
},
{
Type: description.MediaTypeVideo,
Formats: []format.Format{&format.H264{}},
},
}},
GenerateRTPPackets: true,
Parent: test.NilLogger,
@@ -63,24 +669,43 @@ func TestFromStreamSkipUnsupportedTracks(t *testing.T) {
l := test.Logger(func(l logger.Level, format string, args ...interface{}) {
require.Equal(t, logger.Warn, l)
switch n {
case 0:
if n == 0 {
require.Equal(t, "skipping track 1 (VP8)", fmt.Sprintf(format, args...))
case 1:
require.Equal(t, "skipping track 3 (H264)", fmt.Sprintf(format, args...))
}
n++
})
var buf bytes.Buffer
c := &dummyConn{
rw: &buf,
}
c.initialize()
ln, err := net.Listen("tcp", "127.0.0.1:9121")
require.NoError(t, err)
defer ln.Close()
err = FromStream(strm, l, c, nil, 0)
go func() {
u, err2 := url.Parse("rtmp://127.0.0.1:9121/stream")
require.NoError(t, err2)
c := &Client{
URL: u,
}
err2 = c.Initialize(context.Background())
require.NoError(t, err2)
}()
nconn, err := ln.Accept()
require.NoError(t, err)
defer nconn.Close()
conn := &ServerConn{
RW: nconn,
}
err = conn.Initialize()
require.NoError(t, err)
err = conn.Accept()
require.NoError(t, err)
err = FromStream(strm, l, conn, nil, 0)
require.NoError(t, err)
defer strm.RemoveReader(l)
require.Equal(t, 2, n)
require.Equal(t, 1, n)
}

View File

@@ -97,13 +97,10 @@ func (m *Audio) unmarshal(raw *rawmessage.Message) error {
}
func (m Audio) marshalBodySize() int {
var l int
if m.Codec == CodecMPEG1Audio {
l = 1 + len(m.Payload)
} else {
l = 2 + len(m.Payload)
if m.Codec == CodecMPEG4Audio {
return 2 + len(m.Payload)
}
return l
return 1 + len(m.Payload)
}
func (m Audio) marshal() (*rawmessage.Message, error) {

View File

@@ -200,6 +200,67 @@ var cases = []struct {
},
},
},
{
"decreasing timestamp",
[]*Message{
{
ChunkStreamID: 27,
Timestamp: 16 * time.Second,
Type: 6,
MessageStreamID: 3123,
Body: []byte{1, 2},
},
{
ChunkStreamID: 27,
Timestamp: 17 * time.Second,
Type: 6,
MessageStreamID: 3123,
Body: []byte{3, 4},
},
{
ChunkStreamID: 27,
Timestamp: 16 * time.Second,
Type: 6,
MessageStreamID: 3123,
Body: []byte{5, 6},
},
{
ChunkStreamID: 27,
Timestamp: 17 * time.Second,
Type: 6,
MessageStreamID: 3123,
Body: []byte{7, 8},
},
},
[]chunk.Chunk{
&chunk.Chunk0{
ChunkStreamID: 27,
Timestamp: 16000,
Type: 6,
MessageStreamID: 3123,
BodyLen: 2,
Body: []byte{1, 2},
},
&chunk.Chunk2{
ChunkStreamID: 27,
TimestampDelta: 1000,
Body: []byte{3, 4},
},
&chunk.Chunk0{
ChunkStreamID: 27,
Timestamp: 16000,
Type: 6,
MessageStreamID: 3123,
BodyLen: 2,
Body: []byte{5, 6},
},
&chunk.Chunk2{
ChunkStreamID: 27,
TimestampDelta: 1000,
Body: []byte{7, 8},
},
},
},
}
func TestReader(t *testing.T) {

View File

@@ -119,20 +119,6 @@ func (wc *writerChunkStream) writeMessage(msg *Message) error {
return err
}
}
v1 := msg.MessageStreamID
wc.lastMessageStreamID = &v1
v2 := msg.Type
wc.lastType = &v2
v3 := bodyLen
wc.lastBodyLen = &v3
v4 := timestamp
wc.lastTimestamp = &v4
if timestampDelta != nil {
v5 := *timestampDelta
wc.lastTimestampDelta = &v5
}
} else {
err := wc.writeChunk(&chunk.Chunk3{
ChunkStreamID: msg.ChunkStreamID,
@@ -146,9 +132,21 @@ func (wc *writerChunkStream) writeMessage(msg *Message) error {
pos += chunkBodyLen
if (bodyLen - pos) == 0 {
return wc.mw.bw.Flush()
break
}
}
v1 := msg.MessageStreamID
wc.lastMessageStreamID = &v1
v2 := msg.Type
wc.lastType = &v2
v3 := bodyLen
wc.lastBodyLen = &v3
v4 := timestamp
wc.lastTimestamp = &v4
wc.lastTimestampDelta = timestampDelta
return wc.mw.bw.Flush()
}
// Writer is a raw message writer.

View File

@@ -60,6 +60,8 @@ func TestWriter(t *testing.T) {
require.Equal(t, cach, ch)
hasExtendedTimestamp = chunkHasExtendedTimestamp(cach)
}
require.Zero(t, buf.Len())
})
}
}

View File

@@ -2,6 +2,7 @@ package rtmp
import (
"bytes"
"io"
"testing"
"time"
@@ -18,59 +19,41 @@ import (
"github.com/bluenviron/mediamtx/internal/test"
)
type dummyConn struct {
rw io.ReadWriter
bc *bytecounter.ReadWriter
mrw *message.ReadWriter
}
func (c *dummyConn) initialize() {
c.bc = bytecounter.NewReadWriter(c.rw)
c.mrw = message.NewReadWriter(c.bc, c.bc, false)
}
// BytesReceived returns the number of bytes received.
func (c *dummyConn) BytesReceived() uint64 {
return c.bc.Reader.Count()
}
// BytesSent returns the number of bytes sent.
func (c *dummyConn) BytesSent() uint64 {
return c.bc.Writer.Count()
}
func (c *dummyConn) Read() (message.Message, error) {
return c.mrw.Read()
}
func (c *dummyConn) Write(msg message.Message) error {
return c.mrw.Write(msg)
}
func TestReadTracks(t *testing.T) {
var spsp h265.SPS
err := spsp.Unmarshal(test.FormatH265.SPS)
require.NoError(t, err)
hvcc := &mp4.HvcC{
ConfigurationVersion: 1,
GeneralProfileIdc: spsp.ProfileTierLevel.GeneralProfileIdc,
GeneralProfileCompatibility: spsp.ProfileTierLevel.GeneralProfileCompatibilityFlag,
GeneralConstraintIndicator: [6]uint8{
test.FormatH265.SPS[7], test.FormatH265.SPS[8], test.FormatH265.SPS[9],
test.FormatH265.SPS[10], test.FormatH265.SPS[11], test.FormatH265.SPS[12],
},
GeneralLevelIdc: spsp.ProfileTierLevel.GeneralLevelIdc,
// MinSpatialSegmentationIdc
// ParallelismType
ChromaFormatIdc: uint8(spsp.ChromaFormatIdc),
BitDepthLumaMinus8: uint8(spsp.BitDepthLumaMinus8),
BitDepthChromaMinus8: uint8(spsp.BitDepthChromaMinus8),
// AvgFrameRate
// ConstantFrameRate
NumTemporalLayers: 1,
// TemporalIdNested
LengthSizeMinusOne: 3,
NumOfNaluArrays: 3,
NaluArrays: []mp4.HEVCNaluArray{
{
NaluType: byte(h265.NALUType_VPS_NUT),
NumNalus: 1,
Nalus: []mp4.HEVCNalu{{
Length: uint16(len(test.FormatH265.VPS)),
NALUnit: test.FormatH265.VPS,
}},
},
{
NaluType: byte(h265.NALUType_SPS_NUT),
NumNalus: 1,
Nalus: []mp4.HEVCNalu{{
Length: uint16(len(test.FormatH265.SPS)),
NALUnit: test.FormatH265.SPS,
}},
},
{
NaluType: byte(h265.NALUType_PPS_NUT),
NumNalus: 1,
Nalus: []mp4.HEVCNalu{{
Length: uint16(len(test.FormatH265.PPS)),
NALUnit: test.FormatH265.PPS,
}},
},
},
}
for _, ca := range []struct {
name string
tracks []format.Format
@@ -521,7 +504,7 @@ func TestReadTracks(t *testing.T) {
ChunkStreamID: 4,
MessageStreamID: 0x1000000,
FourCC: message.FourCCHEVC,
HEVCHeader: hvcc,
HEVCHeader: generateHvcC(test.FormatH265.VPS, test.FormatH265.SPS, test.FormatH265.PPS),
},
&message.VideoExCodedFrames{
ChunkStreamID: 4,
@@ -577,7 +560,7 @@ func TestReadTracks(t *testing.T) {
ChunkStreamID: 4,
MessageStreamID: 0x1000000,
FourCC: message.FourCCHEVC,
HEVCHeader: hvcc,
HEVCHeader: generateHvcC(test.FormatH265.VPS, test.FormatH265.SPS, test.FormatH265.PPS),
},
&message.VideoExCodedFrames{
ChunkStreamID: 6,

View File

@@ -110,10 +110,11 @@ type ServerConn struct {
RW io.ReadWriter
// filled by Initialize
connectCmd *message.CommandAMF0
connectObject amf0.Object
app string
tcURL string
connectChunkStreamID byte
connectCommandID int
app string
tcURL string
FourCcList amf0.StrictArray
// filled by Accept
URL *url.URL
@@ -144,40 +145,47 @@ func (c *ServerConn) Initialize() error {
c.mrw = message.NewReadWriter(rw, c.bc, false)
c.connectCmd, err = readCommand(c.mrw)
connectCmd, err := readCommand(c.mrw)
if err != nil {
return err
}
if c.connectCmd.Name != "connect" {
return fmt.Errorf("unexpected command: %+v", c.connectCmd)
if connectCmd.Name != "connect" {
return fmt.Errorf("unexpected command: %+v", connectCmd)
}
if len(c.connectCmd.Arguments) < 1 {
return fmt.Errorf("invalid connect command: %+v", c.connectCmd)
if len(connectCmd.Arguments) < 1 {
return fmt.Errorf("invalid connect command: %+v", connectCmd)
}
var ok bool
c.connectObject, ok = objectOrArray(c.connectCmd.Arguments[0])
c.connectChunkStreamID = connectCmd.ChunkStreamID
c.connectCommandID = connectCmd.CommandID
connectObject, ok := objectOrArray(connectCmd.Arguments[0])
if !ok {
return fmt.Errorf("invalid connect command: %+v", c.connectCmd)
return fmt.Errorf("invalid connect command: %+v", connectCmd)
}
c.app, ok = c.connectObject.GetString("app")
c.app, ok = connectObject.GetString("app")
if !ok {
return fmt.Errorf("invalid connect command: %+v", c.connectCmd)
return fmt.Errorf("invalid connect command: %+v", connectCmd)
}
c.tcURL, ok = c.connectObject.GetString("tcUrl")
c.tcURL, ok = connectObject.GetString("tcUrl")
if !ok {
c.tcURL, ok = c.connectObject.GetString("tcurl")
c.tcURL, ok = connectObject.GetString("tcurl")
if !ok {
return fmt.Errorf("invalid connect command: %+v", c.connectCmd)
return fmt.Errorf("invalid connect command: %+v", connectCmd)
}
}
c.tcURL = strings.Trim(c.tcURL, "'")
if raw, ok2 := connectObject.Get("fourCcList"); ok2 {
if arr, ok3 := raw.(amf0.StrictArray); ok3 {
c.FourCcList = arr
}
}
return nil
}
@@ -186,9 +194,9 @@ func (c *ServerConn) CheckCredentials(expectedUser string, expectedPass string)
i := strings.Index(c.app, "?authmod=adobe")
if i < 0 {
err := c.mrw.Write(&message.CommandAMF0{
ChunkStreamID: c.connectCmd.ChunkStreamID,
ChunkStreamID: c.connectChunkStreamID,
Name: "_error",
CommandID: c.connectCmd.CommandID,
CommandID: c.connectCommandID,
Arguments: []interface{}{
nil,
amf0.Object{
@@ -218,9 +226,9 @@ func (c *ServerConn) CheckCredentials(expectedUser string, expectedPass string)
if clientChallenge == "" || response == "" {
err := c.mrw.Write(&message.CommandAMF0{
ChunkStreamID: c.connectCmd.ChunkStreamID,
ChunkStreamID: c.connectChunkStreamID,
Name: "_error",
CommandID: c.connectCmd.CommandID,
CommandID: c.connectCommandID,
Arguments: []interface{}{
nil,
amf0.Object{
@@ -244,9 +252,9 @@ func (c *ServerConn) CheckCredentials(expectedUser string, expectedPass string)
expectedResponse := authResponse(expectedUser, expectedPass, serverSalt, "", serverChallenge, clientChallenge)
if expectedResponse != response {
err := c.mrw.Write(&message.CommandAMF0{
ChunkStreamID: c.connectCmd.ChunkStreamID,
ChunkStreamID: c.connectChunkStreamID,
Name: "_error",
CommandID: c.connectCmd.CommandID,
CommandID: c.connectCommandID,
Arguments: []interface{}{
nil,
amf0.Object{
@@ -301,12 +309,10 @@ func (c *ServerConn) Accept() error {
return err
}
oe, _ := c.connectObject.GetFloat64("objectEncoding")
err = c.mrw.Write(&message.CommandAMF0{
ChunkStreamID: c.connectCmd.ChunkStreamID,
ChunkStreamID: c.connectChunkStreamID,
Name: "_result",
CommandID: c.connectCmd.CommandID,
CommandID: c.connectCommandID,
Arguments: []interface{}{
amf0.Object{
{Key: "fmsVer", Value: "LNX 9,0,124,2"},
@@ -316,7 +322,7 @@ func (c *ServerConn) Accept() error {
{Key: "level", Value: "status"},
{Key: "code", Value: "NetConnection.Connect.Success"},
{Key: "description", Value: "Connection succeeded."},
{Key: "objectEncoding", Value: oe},
{Key: "objectEncoding", Value: float64(encodingAMF0)},
},
},
})

View File

@@ -727,3 +727,65 @@ func TestServerConnPath(t *testing.T) {
})
}
}
func TestServerConnFourCcList(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() {
defer close(done)
nconn, err2 := ln.Accept()
require.NoError(t, err2)
defer nconn.Close()
conn := &ServerConn{
RW: nconn,
}
err2 = conn.Initialize()
require.NoError(t, err2)
require.Equal(t, amf0.StrictArray{
"av01",
"Avc1",
}, conn.FourCcList)
}()
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, false, false)
require.NoError(t, err)
mrw := message.NewReadWriter(bc, bc, true)
err = mrw.Write(&message.CommandAMF0{
ChunkStreamID: 3,
Name: "connect",
CommandID: 1,
Arguments: []interface{}{
amf0.Object{
{Key: "app", Value: "stream?key=val"},
{Key: "flashVer", Value: "LNX 9,0,124,2"},
{Key: "tcUrl", Value: "rtmp://127.0.0.1:9121/stream?key=val"},
{Key: "fpad", Value: false},
{Key: "capabilities", Value: float64(15)},
{Key: "audioCodecs", Value: float64(4071)},
{Key: "videoCodecs", Value: float64(252)},
{Key: "videoFunction", Value: float64(1)},
{Key: "fourCcList", Value: amf0.StrictArray{
"av01",
"Avc1",
}},
},
},
})
require.NoError(t, err)
<-done
}

File diff suppressed because it is too large Load Diff

View File

@@ -3,104 +3,683 @@ package rtmp
import (
"bytes"
"testing"
"time"
"github.com/abema/go-mp4"
"github.com/bluenviron/gortsplib/v4/pkg/format"
"github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg1audio"
"github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio"
"github.com/stretchr/testify/require"
"github.com/bluenviron/mediamtx/internal/formatprocessor"
"github.com/bluenviron/mediamtx/internal/protocols/rtmp/amf0"
"github.com/bluenviron/mediamtx/internal/protocols/rtmp/bytecounter"
"github.com/bluenviron/mediamtx/internal/protocols/rtmp/message"
"github.com/bluenviron/mediamtx/internal/test"
)
func TestWriteTracks(t *testing.T) {
videoTrack := &format.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,
func TestWriter(t *testing.T) {
for _, ca := range []string{
"h264 + aac",
"av1",
"vp9",
"h265",
"h265 no params",
"h264 no params",
"opus",
"mp3",
"ac-3",
"pcma",
"pcmu",
"lpcm",
} {
t.Run(ca, func(t *testing.T) {
var tracks []format.Format
switch ca {
case "h264 + aac":
tracks = append(tracks, &format.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,
})
tracks = append(tracks, &format.MPEG4Audio{
PayloadTyp: 96,
Config: &mpeg4audio.AudioSpecificConfig{
Type: 2,
SampleRate: 44100,
ChannelCount: 2,
},
SizeLength: 13,
IndexLength: 3,
IndexDeltaLength: 3,
})
case "av1":
tracks = append(tracks, &format.AV1{
PayloadTyp: 96,
})
case "vp9":
tracks = append(tracks, &format.VP9{
PayloadTyp: 96,
})
case "h265":
tracks = append(tracks, test.FormatH265)
case "h265 no params":
tracks = append(tracks, &format.H265{})
case "h264 no params":
tracks = append(tracks, &format.H264{})
case "opus":
tracks = append(tracks, &format.Opus{
PayloadTyp: 96,
ChannelCount: 2,
})
case "mp3":
tracks = append(tracks, &format.MPEG1Audio{})
case "ac-3":
tracks = append(tracks, &format.AC3{
SampleRate: 44100,
ChannelCount: 2,
})
case "pcma":
tracks = append(tracks, &format.G711{
MULaw: false,
SampleRate: 8000,
ChannelCount: 1,
})
case "pcmu":
tracks = append(tracks, &format.G711{
MULaw: true,
SampleRate: 8000,
ChannelCount: 1,
})
case "lpcm":
tracks = append(tracks, &format.LPCM{
BitDepth: 16,
SampleRate: 44100,
ChannelCount: 1,
})
}
var buf bytes.Buffer
c := &dummyConn{
rw: &buf,
}
c.initialize()
w := &Writer{
Conn: c,
Tracks: tracks,
}
err := w.Initialize()
require.NoError(t, err)
bc := bytecounter.NewReadWriter(&buf)
mrw := message.NewReadWriter(bc, bc, true)
msg, err := mrw.Read()
require.NoError(t, err)
switch ca {
case "h264 + aac":
require.Equal(t, &message.DataAMF0{
ChunkStreamID: 4,
MessageStreamID: 0x1000000,
Payload: []interface{}{
"@setDataFrame",
"onMetaData",
amf0.Object{
{Key: "videocodecid", Value: float64(7)},
{Key: "videodatarate", Value: float64(0)},
{Key: "audiocodecid", Value: float64(10)},
{Key: "audiodatarate", Value: float64(0)},
},
},
}, msg)
case "av1":
require.Equal(t, &message.DataAMF0{
ChunkStreamID: 4,
MessageStreamID: 0x1000000,
Payload: []interface{}{
"@setDataFrame",
"onMetaData",
amf0.Object{
{Key: "videocodecid", Value: float64(1.635135537e+09)},
{Key: "videodatarate", Value: float64(0)},
{Key: "audiocodecid", Value: float64(0)},
{Key: "audiodatarate", Value: float64(0)},
},
},
}, msg)
case "vp9":
require.Equal(t, &message.DataAMF0{
ChunkStreamID: 4,
MessageStreamID: 0x1000000,
Payload: []interface{}{
"@setDataFrame",
"onMetaData",
amf0.Object{
{Key: "videocodecid", Value: float64(1.987063865e+09)},
{Key: "videodatarate", Value: float64(0)},
{Key: "audiocodecid", Value: float64(0)},
{Key: "audiodatarate", Value: float64(0)},
},
},
}, msg)
case "h265":
require.Equal(t, &message.DataAMF0{
ChunkStreamID: 4,
MessageStreamID: 0x1000000,
Payload: []interface{}{
"@setDataFrame",
"onMetaData",
amf0.Object{
{Key: "videocodecid", Value: float64(1.752589105e+09)},
{Key: "videodatarate", Value: float64(0)},
{Key: "audiocodecid", Value: float64(0)},
{Key: "audiodatarate", Value: float64(0)},
},
},
}, msg)
case "h265 no params":
require.Equal(t, &message.DataAMF0{
ChunkStreamID: 4,
MessageStreamID: 0x1000000,
Payload: []interface{}{
"@setDataFrame",
"onMetaData",
amf0.Object{
{Key: "videocodecid", Value: float64(1.752589105e+09)},
{Key: "videodatarate", Value: float64(0)},
{Key: "audiocodecid", Value: float64(0)},
{Key: "audiodatarate", Value: float64(0)},
},
},
}, msg)
case "h264 no params":
require.Equal(t, &message.DataAMF0{
ChunkStreamID: 4,
MessageStreamID: 0x1000000,
Payload: []interface{}{
"@setDataFrame",
"onMetaData",
amf0.Object{
{Key: "videocodecid", Value: float64(7)},
{Key: "videodatarate", Value: float64(0)},
{Key: "audiocodecid", Value: float64(0)},
{Key: "audiodatarate", Value: float64(0)},
},
},
}, msg)
case "opus":
require.Equal(t, &message.DataAMF0{
ChunkStreamID: 4,
MessageStreamID: 0x1000000,
Payload: []interface{}{
"@setDataFrame",
"onMetaData",
amf0.Object{
{Key: "videocodecid", Value: float64(0)},
{Key: "videodatarate", Value: float64(0)},
{Key: "audiocodecid", Value: float64(1.332770163e+09)},
{Key: "audiodatarate", Value: float64(0)},
},
},
}, msg)
case "mp3":
require.Equal(t, &message.DataAMF0{
ChunkStreamID: 4,
MessageStreamID: 0x1000000,
Payload: []interface{}{
"@setDataFrame",
"onMetaData",
amf0.Object{
{Key: "videocodecid", Value: float64(0)},
{Key: "videodatarate", Value: float64(0)},
{Key: "audiocodecid", Value: float64(2)},
{Key: "audiodatarate", Value: float64(0)},
},
},
}, msg)
case "ac-3":
require.Equal(t, &message.DataAMF0{
ChunkStreamID: 4,
MessageStreamID: 0x1000000,
Payload: []interface{}{
"@setDataFrame",
"onMetaData",
amf0.Object{
{Key: "videocodecid", Value: float64(0)},
{Key: "videodatarate", Value: float64(0)},
{Key: "audiocodecid", Value: float64(1.633889587e+09)},
{Key: "audiodatarate", Value: float64(0)},
},
},
}, msg)
case "pcma":
require.Equal(t, &message.DataAMF0{
ChunkStreamID: 4,
MessageStreamID: 0x1000000,
Payload: []interface{}{
"@setDataFrame",
"onMetaData",
amf0.Object{
{Key: "videocodecid", Value: float64(0)},
{Key: "videodatarate", Value: float64(0)},
{Key: "audiocodecid", Value: float64(7)},
{Key: "audiodatarate", Value: float64(0)},
},
},
}, msg)
case "pcmu":
require.Equal(t, &message.DataAMF0{
ChunkStreamID: 4,
MessageStreamID: 0x1000000,
Payload: []interface{}{
"@setDataFrame",
"onMetaData",
amf0.Object{
{Key: "videocodecid", Value: float64(0)},
{Key: "videodatarate", Value: float64(0)},
{Key: "audiocodecid", Value: float64(8)},
{Key: "audiodatarate", Value: float64(0)},
},
},
}, msg)
case "lpcm":
require.Equal(t, &message.DataAMF0{
ChunkStreamID: 4,
MessageStreamID: 0x1000000,
Payload: []interface{}{
"@setDataFrame",
"onMetaData",
amf0.Object{
{Key: "videocodecid", Value: float64(0)},
{Key: "videodatarate", Value: float64(0)},
{Key: "audiocodecid", Value: float64(3)},
{Key: "audiodatarate", Value: float64(0)},
},
},
}, msg)
}
switch ca {
case "h264 + aac":
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{
0x01, 0x64, 0x00, 0x0c, 0xff, 0xe1, 0x00, 0x15,
0x67, 0x64, 0x00, 0x0c, 0xac, 0x3b, 0x50, 0xb0,
0x4b, 0x42, 0x00, 0x00, 0x03, 0x00, 0x02, 0x00,
0x00, 0x03, 0x00, 0x3d, 0x08, 0x01, 0x00, 0x04,
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: message.Rate44100,
Depth: message.Depth16,
IsStereo: true,
AACType: message.AudioAACTypeConfig,
Payload: []byte{0x12, 0x10},
}, msg)
err = w.WriteH264(tracks[0].(*format.H264), 100*time.Millisecond, 0, [][]byte{{5, 1}})
require.NoError(t, err)
err = w.WriteMPEG4Audio(tracks[1], 0, []byte{1, 2})
require.NoError(t, err)
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.VideoTypeAU,
PTSDelta: 100 * time.Millisecond,
Payload: []byte{0, 0, 0, 2, 5, 1},
}, msg)
case "av1":
msg, err = mrw.Read()
require.NoError(t, err)
require.Equal(t, &message.VideoExSequenceStart{
ChunkStreamID: message.VideoChunkStreamID,
MessageStreamID: 0x1000000,
FourCC: message.FourCCAV1,
AV1Header: &mp4.Av1C{
Marker: 0x1,
Version: 0x1,
SeqLevelIdx0: 0x8,
ChromaSubsamplingX: 0x1,
ChromaSubsamplingY: 0x1,
ConfigOBUs: []uint8{0xa, 0xb, 0x0, 0x0, 0x0, 0x42, 0xab, 0xbf, 0xc3, 0x70, 0xb, 0xe0, 0x1},
},
}, msg)
err = w.WriteAV1(tracks[0].(*format.AV1), 0, 0, [][]byte{{1, 2}})
require.NoError(t, err)
msg, err = mrw.Read()
require.NoError(t, err)
require.Equal(t, &message.VideoExFramesX{
ChunkStreamID: message.VideoChunkStreamID,
MessageStreamID: 0x1000000,
FourCC: message.FourCCAV1,
Payload: []byte{0x12, 0x0, 0x3, 0x1, 0x2},
}, msg)
case "vp9":
msg, err = mrw.Read()
require.NoError(t, err)
require.Equal(t, &message.VideoExSequenceStart{
ChunkStreamID: message.VideoChunkStreamID,
MessageStreamID: 0x1000000,
FourCC: message.FourCCVP9,
VP9Header: &mp4.VpcC{
FullBox: mp4.FullBox{Version: 0x1},
Level: 0x28,
BitDepth: 0x8,
ChromaSubsampling: 0x1,
ColourPrimaries: 0x2,
TransferCharacteristics: 0x2,
MatrixCoefficients: 0x2,
CodecInitializationData: []uint8{},
},
}, msg)
err = w.WriteVP9(tracks[0].(*format.VP9), 0, 0, []byte{1, 2})
require.NoError(t, err)
msg, err = mrw.Read()
require.NoError(t, err)
require.Equal(t, &message.VideoExFramesX{
ChunkStreamID: message.VideoChunkStreamID,
MessageStreamID: 0x1000000,
FourCC: message.FourCCVP9,
Payload: []byte{0x1, 0x2},
}, msg)
case "h265":
msg, err = mrw.Read()
require.NoError(t, err)
require.Equal(t, &message.VideoExSequenceStart{
ChunkStreamID: message.VideoChunkStreamID,
MessageStreamID: 0x1000000,
FourCC: message.FourCCHEVC,
HEVCHeader: &mp4.HvcC{
ConfigurationVersion: 0x1,
GeneralProfileIdc: 2,
GeneralProfileCompatibility: [32]bool{
false, false, true, false, false, false, false, false,
false, false, false, false, false, false, false, false,
false, false, false, false, false, false, false, false,
false, false, false, false, false, false, false, false,
},
GeneralConstraintIndicator: [6]uint8{0x03, 0x0, 0xb0, 0x0, 0x0, 0x03},
GeneralLevelIdc: 0x7b,
ChromaFormatIdc: 0x1,
LengthSizeMinusOne: 0x3,
NumOfNaluArrays: 0x3,
BitDepthLumaMinus8: 2,
BitDepthChromaMinus8: 2,
NumTemporalLayers: 1,
NaluArrays: []mp4.HEVCNaluArray{
{
NaluType: 0x20,
NumNalus: 0x1,
Nalus: []mp4.HEVCNalu{{
Length: 24,
NALUnit: test.FormatH265.VPS,
}},
},
{
NaluType: 0x21,
NumNalus: 0x1,
Nalus: []mp4.HEVCNalu{{
Length: 60,
NALUnit: test.FormatH265.SPS,
}},
},
{
NaluType: 0x22,
NumNalus: 0x1,
Nalus: []mp4.HEVCNalu{{
Length: 8,
NALUnit: test.FormatH265.PPS,
}},
},
},
},
}, msg)
err = w.WriteH265(tracks[0].(*format.H265), 0, 0, [][]byte{{1, 2}})
require.NoError(t, err)
msg, err = mrw.Read()
require.NoError(t, err)
require.Equal(t, &message.VideoExFramesX{
ChunkStreamID: message.VideoChunkStreamID,
MessageStreamID: 0x1000000,
FourCC: message.FourCCHEVC,
Payload: []byte{0, 0, 0, 2, 1, 2},
}, msg)
case "h265 no params":
msg, err = mrw.Read()
require.NoError(t, err)
require.Equal(t, &message.VideoExSequenceStart{
ChunkStreamID: message.VideoChunkStreamID,
MessageStreamID: 0x1000000,
FourCC: message.FourCCHEVC,
HEVCHeader: generateHvcC(formatprocessor.H265DefaultVPS,
formatprocessor.H265DefaultSPS, formatprocessor.H265DefaultPPS),
}, msg)
err = w.WriteH265(tracks[0].(*format.H265), 0, 0, [][]byte{{1, 2}})
require.NoError(t, err)
msg, err = mrw.Read()
require.NoError(t, err)
require.Equal(t, &message.VideoExFramesX{
ChunkStreamID: message.VideoChunkStreamID,
MessageStreamID: 0x1000000,
FourCC: message.FourCCHEVC,
Payload: []byte{0, 0, 0, 2, 1, 2},
}, msg)
case "h264 no params":
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{
0x01, 0x42, 0xc0, 0x28, 0xff, 0xe1, 0x00, 0x19,
0x67, 0x42, 0xc0, 0x28, 0xd9, 0x00, 0x78, 0x02,
0x27, 0xe5, 0x84, 0x00, 0x00, 0x03, 0x00, 0x04,
0x00, 0x00, 0x03, 0x00, 0xf0, 0x3c, 0x60, 0xc9,
0x20, 0x01, 0x00, 0x04, 0x08, 0x06, 0x07, 0x08,
},
}, msg)
err = w.WriteH264(tracks[0].(*format.H264), 0, 0, [][]byte{{1, 2}})
require.NoError(t, err)
msg, err = mrw.Read()
require.NoError(t, err)
require.Equal(t, &message.Video{
ChunkStreamID: message.VideoChunkStreamID,
MessageStreamID: 0x1000000,
Codec: message.CodecH264,
Type: message.VideoTypeAU,
Payload: []byte{0, 0, 0, 2, 1, 2},
}, msg)
case "opus":
msg, err = mrw.Read()
require.NoError(t, err)
require.Equal(t, &message.AudioExSequenceStart{
ChunkStreamID: message.AudioChunkStreamID,
MessageStreamID: 0x1000000,
FourCC: message.FourCCOpus,
OpusHeader: &message.OpusIDHeader{
Version: 1,
PreSkip: 3840,
ChannelCount: 2,
ChannelMappingTable: []uint8{},
},
}, msg)
err = w.WriteOpus(tracks[0].(*format.Opus), 0, []byte{1, 2})
require.NoError(t, err)
msg, err = mrw.Read()
require.NoError(t, err)
require.Equal(t, &message.AudioExCodedFrames{
ChunkStreamID: message.AudioChunkStreamID,
MessageStreamID: 0x1000000,
FourCC: message.FourCCOpus,
Payload: []byte{1, 2},
}, msg)
case "mp3":
fr := []byte{
0xff, 0xfa, 0x52, 0x04, 0x00,
}
var h mpeg1audio.FrameHeader
err = h.Unmarshal(fr)
require.NoError(t, err)
err = w.WriteMPEG1Audio(tracks[0].(*format.MPEG1Audio), 0, &h, fr)
require.NoError(t, err)
msg, err = mrw.Read()
require.NoError(t, err)
require.Equal(t, &message.Audio{
ChunkStreamID: message.AudioChunkStreamID,
MessageStreamID: 0x1000000,
Codec: message.CodecMPEG1Audio,
Rate: message.Rate44100,
Depth: message.Depth16,
IsStereo: true,
Payload: []byte{
0xff, 0xfa, 0x52, 0x04, 0x00,
},
}, msg)
case "ac-3":
msg, err = mrw.Read()
require.NoError(t, err)
require.Equal(t, &message.AudioExSequenceStart{
ChunkStreamID: message.AudioChunkStreamID,
MessageStreamID: 0x1000000,
FourCC: message.FourCCAC3,
}, msg)
err = w.WriteAC3(tracks[0].(*format.AC3), 0, []byte{1, 2})
require.NoError(t, err)
msg, err = mrw.Read()
require.NoError(t, err)
require.Equal(t, &message.AudioExCodedFrames{
ChunkStreamID: message.AudioChunkStreamID,
MessageStreamID: 0x1000000,
FourCC: message.FourCCAC3,
Payload: []byte{1, 2},
}, msg)
case "pcma":
err = w.WriteG711(tracks[0].(*format.G711), 0, []byte{1, 2})
require.NoError(t, err)
msg, err = mrw.Read()
require.NoError(t, err)
require.Equal(t, &message.Audio{
ChunkStreamID: message.AudioChunkStreamID,
MessageStreamID: 0x1000000,
Codec: message.CodecPCMA,
Depth: message.Depth16,
Payload: []byte{1, 2},
}, msg)
case "pcmu":
err = w.WriteG711(tracks[0].(*format.G711), 0, []byte{1, 2})
require.NoError(t, err)
msg, err = mrw.Read()
require.NoError(t, err)
require.Equal(t, &message.Audio{
ChunkStreamID: message.AudioChunkStreamID,
MessageStreamID: 0x1000000,
Codec: message.CodecPCMU,
Depth: message.Depth16,
Payload: []byte{1, 2},
}, msg)
case "lpcm":
err = w.WriteLPCM(tracks[0].(*format.LPCM), 0, []byte{1, 2})
require.NoError(t, err)
msg, err = mrw.Read()
require.NoError(t, err)
require.Equal(t, &message.Audio{
ChunkStreamID: message.AudioChunkStreamID,
MessageStreamID: 0x1000000,
Codec: message.CodecLPCM,
Rate: message.Rate44100,
Depth: message.Depth16,
Payload: []byte{2, 1},
}, msg)
}
})
}
audioTrack := &format.MPEG4Audio{
PayloadTyp: 96,
Config: &mpeg4audio.AudioSpecificConfig{
Type: 2,
SampleRate: 44100,
ChannelCount: 2,
},
SizeLength: 13,
IndexLength: 3,
IndexDeltaLength: 3,
}
var buf bytes.Buffer
c := &dummyConn{
rw: &buf,
}
c.initialize()
w := &Writer{
Conn: c,
VideoTrack: videoTrack,
AudioTrack: audioTrack,
}
err := w.Initialize()
require.NoError(t, err)
bc := bytecounter.NewReadWriter(&buf)
mrw := message.NewReadWriter(bc, bc, true)
msg, err := mrw.Read()
require.NoError(t, err)
require.Equal(t, &message.DataAMF0{
ChunkStreamID: 4,
MessageStreamID: 0x1000000,
Payload: []interface{}{
"@setDataFrame",
"onMetaData",
amf0.Object{
{Key: "videodatarate", Value: float64(0)},
{Key: "videocodecid", Value: float64(7)},
{Key: "audiodatarate", Value: float64(0)},
{Key: "audiocodecid", Value: 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: message.Rate44100,
Depth: message.Depth16,
IsStereo: true,
AACType: message.AudioAACTypeConfig,
Payload: []byte{0x12, 0x10},
}, msg)
}

View File

@@ -158,13 +158,9 @@ func (f *formatFMP4) initialize() bool {
paramsChanged := false
for _, obu := range tunit.TU {
var h av1.OBUHeader
err := h.Unmarshal(obu)
if err != nil {
return err
}
typ := av1.OBUType((obu[0] >> 3) & 0b1111)
if h.Type == av1.OBUTypeSequenceHeader {
if typ == av1.OBUTypeSequenceHeader {
if !bytes.Equal(codec.SequenceHeader, obu) {
codec.SequenceHeader = obu
paramsChanged = true
@@ -285,7 +281,6 @@ func (f *formatFMP4) initialize() bool {
case *rtspformat.H265:
vps, sps, pps := forma.SafeParams()
if vps == nil || sps == nil || pps == nil {
vps = formatprocessor.H265DefaultVPS
sps = formatprocessor.H265DefaultSPS
@@ -376,7 +371,6 @@ func (f *formatFMP4) initialize() bool {
case *rtspformat.H264:
sps, pps := forma.SafeParams()
if sps == nil || pps == nil {
sps = formatprocessor.H264DefaultSPS
pps = formatprocessor.H264DefaultPPS

View File

@@ -9,7 +9,6 @@ import (
"github.com/bluenviron/gortsplib/v4/pkg/description"
rtspformat "github.com/bluenviron/gortsplib/v4/pkg/format"
"github.com/bluenviron/mediacommon/v2/pkg/codecs/h265"
"github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio"
"github.com/bluenviron/mediacommon/v2/pkg/formats/fmp4"
"github.com/bluenviron/mediacommon/v2/pkg/formats/mp4"
@@ -96,7 +95,7 @@ func TestRecorder(t *testing.T) {
test.FormatH265.VPS,
test.FormatH265.SPS,
test.FormatH265.PPS,
{byte(h265.NALUType_CRA_NUT) << 1, 0}, // IDR
{0x26, 0x1, 0xaf, 0x8, 0x42, 0x23, 0x48, 0x8a, 0x43, 0xe2},
},
})

View File

@@ -128,14 +128,14 @@ func TestServerPublish(t *testing.T) {
defer conn.Close()
w := &rtmp.Writer{
Conn: conn,
VideoTrack: test.FormatH264,
AudioTrack: test.FormatMPEG4Audio,
Conn: conn,
Tracks: []format.Format{test.FormatH264, test.FormatMPEG4Audio},
}
err = w.Initialize()
require.NoError(t, err)
err = w.WriteH264(
test.FormatH264,
2*time.Second, 2*time.Second, [][]byte{
{5, 2, 3, 4},
})
@@ -165,6 +165,7 @@ func TestServerPublish(t *testing.T) {
defer strm.RemoveReader(reader)
err = w.WriteH264(
test.FormatH264,
3*time.Second, 3*time.Second, [][]byte{
{5, 2, 3, 4},
})

View File

@@ -10,6 +10,7 @@ import (
"github.com/stretchr/testify/require"
"github.com/bluenviron/gortsplib/v4/pkg/format"
"github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/defs"
"github.com/bluenviron/mediamtx/internal/protocols/rtmp"
@@ -117,17 +118,16 @@ func TestSource(t *testing.T) {
require.NoError(t, err)
w := &rtmp.Writer{
Conn: conn,
VideoTrack: test.FormatH264,
AudioTrack: test.FormatMPEG4Audio,
Conn: conn,
Tracks: []format.Format{test.FormatH264, test.FormatMPEG4Audio},
}
err = w.Initialize()
require.NoError(t, err)
err = w.WriteH264(2*time.Second, 2*time.Second, [][]byte{{5, 2, 3, 4}})
err = w.WriteH264(test.FormatH264, 2*time.Second, 2*time.Second, [][]byte{{5, 2, 3, 4}})
require.NoError(t, err)
err = w.WriteH264(3*time.Second, 3*time.Second, [][]byte{{5, 2, 3, 4}})
err = w.WriteH264(test.FormatH264, 3*time.Second, 3*time.Second, [][]byte{{5, 2, 3, 4}})
require.NoError(t, err)
break