Files
rtsp-simple-server/internal/protocols/rtmp/server_conn_test.go
Alessandro Ros 9318107779 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
2025-09-11 23:18:46 +02:00

792 lines
19 KiB
Go

package rtmp
import (
"fmt"
"net"
"net/url"
"testing"
"github.com/bluenviron/mediamtx/internal/protocols/rtmp/amf0"
"github.com/bluenviron/mediamtx/internal/protocols/rtmp/bytecounter"
"github.com/bluenviron/mediamtx/internal/protocols/rtmp/handshake"
"github.com/bluenviron/mediamtx/internal/protocols/rtmp/message"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
)
func TestServerConn(t *testing.T) {
for _, ca := range []string{
"auth 1",
"auth 2",
"auth 3",
"read",
"publish",
} {
t.Run(ca, 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() {
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)
if ca == "auth 1" || ca == "auth 2" || ca == "auth 3" {
err2 = conn.CheckCredentials("myuser", "mypass")
switch ca {
case "auth 1":
require.Error(t, err2, "need auth")
return
case "auth 2":
require.Error(t, err2, "need auth 2")
return
case "auth 3":
require.NoError(t, err2)
}
}
err2 = conn.Accept()
require.NoError(t, err2)
require.Equal(t, &url.URL{
Scheme: "rtmp",
Host: "127.0.0.1:9121",
Path: "/stream",
RawQuery: "key=val",
}, conn.URL)
require.Equal(t, (ca == "publish"), conn.Publish)
}()
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)
switch ca {
case "auth 1": //nolint:dupl
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)},
},
},
})
require.NoError(t, err)
var msg message.Message
msg, err = mrw.Read()
require.NoError(t, err)
require.Equal(t, &message.CommandAMF0{
ChunkStreamID: 3,
Name: "_error",
CommandID: 1,
Arguments: []interface{}{
nil,
amf0.Object{
{Key: "level", Value: "error"},
{Key: "code", Value: "NetConnection.Connect.Rejected"},
{Key: "description", Value: "code=403 need auth; authmod=adobe"},
},
},
}, msg)
case "auth 2": //nolint:dupl
err = mrw.Write(&message.CommandAMF0{
ChunkStreamID: 3,
Name: "connect",
CommandID: 1,
Arguments: []interface{}{
amf0.Object{
{Key: "app", Value: "stream?key=val?authmod=adobe&user=myuser"},
{Key: "flashVer", Value: "LNX 9,0,124,2"},
{Key: "tcUrl", Value: "rtmp://127.0.0.1:9121/stream?key=val?authmod=adobe&user=myuser"},
{Key: "fpad", Value: false},
{Key: "capabilities", Value: float64(15)},
{Key: "audioCodecs", Value: float64(4071)},
{Key: "videoCodecs", Value: float64(252)},
{Key: "videoFunction", Value: float64(1)},
},
},
})
require.NoError(t, err)
var msg message.Message
msg, err = mrw.Read()
require.NoError(t, err)
require.Equal(t, &message.CommandAMF0{
ChunkStreamID: 3,
Name: "_error",
CommandID: 1,
Arguments: []interface{}{
nil,
amf0.Object{
{Key: "level", Value: "error"},
{Key: "code", Value: "NetConnection.Connect.Rejected"},
{Key: "description", Value: "authmod=adobe ?reason=needauth&user=myuser&salt=testsalt&challenge=testchallenge"},
},
},
}, msg)
case "auth 3":
clientChallenge := uuid.New().String()
response := authResponse("myuser", "mypass", serverSalt, "", serverChallenge, clientChallenge)
err = mrw.Write(&message.CommandAMF0{
ChunkStreamID: 3,
Name: "connect",
CommandID: 1,
Arguments: []interface{}{
amf0.Object{
{
Key: "app",
Value: fmt.Sprintf("stream?key=val?authmod=adobe&user=myuser&challenge=%s&response=%s",
clientChallenge, response),
},
{Key: "flashVer", Value: "LNX 9,0,124,2"},
{
Key: "tcUrl",
Value: fmt.Sprintf("rtmp://127.0.0.1:9121/stream?key=val?authmod=adobe&user=myuser&challenge=%s&response=%s",
clientChallenge, response),
},
{Key: "fpad", Value: false},
{Key: "capabilities", Value: float64(15)},
{Key: "audioCodecs", Value: float64(4071)},
{Key: "videoCodecs", Value: float64(252)},
{Key: "videoFunction", Value: float64(1)},
},
},
})
require.NoError(t, err)
var msg message.Message
msg, err = mrw.Read()
require.NoError(t, err)
require.Equal(t, &message.SetWindowAckSize{
Value: 2500000,
}, msg)
msg, err = mrw.Read()
require.NoError(t, err)
require.Equal(t, &message.SetPeerBandwidth{
Value: 2500000,
Type: 2,
}, msg)
msg, err = mrw.Read()
require.NoError(t, err)
require.Equal(t, &message.SetChunkSize{
Value: 65536,
}, msg)
msg, err = mrw.Read()
require.NoError(t, err)
require.Equal(t, &message.CommandAMF0{
ChunkStreamID: 3,
Name: "_result",
CommandID: 1,
Arguments: []interface{}{
amf0.Object{
{Key: "fmsVer", Value: "LNX 9,0,124,2"},
{Key: "capabilities", Value: float64(31)},
},
amf0.Object{
{Key: "level", Value: "status"},
{Key: "code", Value: "NetConnection.Connect.Success"},
{Key: "description", Value: "Connection succeeded."},
{Key: "objectEncoding", Value: float64(0)},
},
},
}, msg)
err = mrw.Write(&message.SetChunkSize{
Value: 65536,
})
require.NoError(t, err)
err = mrw.Write(&message.CommandAMF0{
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.CommandAMF0{
ChunkStreamID: 3,
Name: "_result",
CommandID: 2,
Arguments: []interface{}{
nil,
float64(1),
},
}, msg)
err = mrw.Write(&message.UserControlSetBufferLength{
BufferLength: 0x64,
})
require.NoError(t, err)
err = mrw.Write(&message.CommandAMF0{
ChunkStreamID: 4,
MessageStreamID: 0x1000000,
Name: "play",
CommandID: 0,
Arguments: []interface{}{
nil,
"",
},
})
require.NoError(t, err)
case "read":
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)},
},
},
})
require.NoError(t, err)
var msg message.Message
msg, err = mrw.Read()
require.NoError(t, err)
require.Equal(t, &message.SetWindowAckSize{
Value: 2500000,
}, msg)
msg, err = mrw.Read()
require.NoError(t, err)
require.Equal(t, &message.SetPeerBandwidth{
Value: 2500000,
Type: 2,
}, msg)
msg, err = mrw.Read()
require.NoError(t, err)
require.Equal(t, &message.SetChunkSize{
Value: 65536,
}, msg)
msg, err = mrw.Read()
require.NoError(t, err)
require.Equal(t, &message.CommandAMF0{
ChunkStreamID: 3,
Name: "_result",
CommandID: 1,
Arguments: []interface{}{
amf0.Object{
{Key: "fmsVer", Value: "LNX 9,0,124,2"},
{Key: "capabilities", Value: float64(31)},
},
amf0.Object{
{Key: "level", Value: "status"},
{Key: "code", Value: "NetConnection.Connect.Success"},
{Key: "description", Value: "Connection succeeded."},
{Key: "objectEncoding", Value: float64(0)},
},
},
}, msg)
err = mrw.Write(&message.SetChunkSize{
Value: 65536,
})
require.NoError(t, err)
err = mrw.Write(&message.CommandAMF0{
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.CommandAMF0{
ChunkStreamID: 3,
Name: "_result",
CommandID: 2,
Arguments: []interface{}{
nil,
float64(1),
},
}, msg)
err = mrw.Write(&message.UserControlSetBufferLength{
BufferLength: 0x64,
})
require.NoError(t, err)
err = mrw.Write(&message.CommandAMF0{
ChunkStreamID: 4,
MessageStreamID: 0x1000000,
Name: "play",
CommandID: 0,
Arguments: []interface{}{
nil,
"",
},
})
require.NoError(t, err)
case "publish":
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)},
},
},
})
require.NoError(t, err)
var msg message.Message
msg, err = mrw.Read()
require.NoError(t, err)
require.Equal(t, &message.SetWindowAckSize{
Value: 2500000,
}, msg)
msg, err = mrw.Read()
require.NoError(t, err)
require.Equal(t, &message.SetPeerBandwidth{
Value: 2500000,
Type: 2,
}, msg)
msg, err = mrw.Read()
require.NoError(t, err)
require.Equal(t, &message.SetChunkSize{
Value: 65536,
}, msg)
msg, err = mrw.Read()
require.NoError(t, err)
require.Equal(t, &message.CommandAMF0{
ChunkStreamID: 3,
Name: "_result",
CommandID: 1,
Arguments: []interface{}{
amf0.Object{
{Key: "fmsVer", Value: "LNX 9,0,124,2"},
{Key: "capabilities", Value: float64(31)},
},
amf0.Object{
{Key: "level", Value: "status"},
{Key: "code", Value: "NetConnection.Connect.Success"},
{Key: "description", Value: "Connection succeeded."},
{Key: "objectEncoding", Value: float64(0)},
},
},
}, msg)
err = mrw.Write(&message.SetChunkSize{
Value: 65536,
})
require.NoError(t, err)
err = mrw.Write(&message.CommandAMF0{
ChunkStreamID: 3,
Name: "releaseStream",
CommandID: 2,
Arguments: []interface{}{
nil,
"",
},
})
require.NoError(t, err)
err = mrw.Write(&message.CommandAMF0{
ChunkStreamID: 3,
Name: "FCPublish",
CommandID: 3,
Arguments: []interface{}{
nil,
"",
},
})
require.NoError(t, err)
err = mrw.Write(&message.CommandAMF0{
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.CommandAMF0{
ChunkStreamID: 3,
Name: "_result",
CommandID: 4,
Arguments: []interface{}{
nil,
float64(1),
},
}, msg)
err = mrw.Write(&message.CommandAMF0{
ChunkStreamID: 4,
MessageStreamID: 0x1000000,
Name: "publish",
CommandID: 5,
Arguments: []interface{}{
nil,
"",
"stream",
},
})
require.NoError(t, err)
}
<-done
})
}
}
func TestServerConnPath(t *testing.T) {
for _, ca := range []string{
"standard",
"leading slash",
"query",
"stream key",
"stream key and query",
"neko",
} {
t.Run(ca, 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() {
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)
err2 = conn.Accept()
require.NoError(t, err2)
switch ca {
case "standard", "neko":
require.Equal(t, &url.URL{
Scheme: "rtmp",
Host: "127.0.0.1:9121",
Path: "/stream",
}, conn.URL)
case "leading slash":
require.Equal(t, &url.URL{
Scheme: "rtmp",
Host: "127.0.0.1:9121",
Path: "//stream",
}, conn.URL)
case "query":
require.Equal(t, &url.URL{
Scheme: "rtmp",
Host: "127.0.0.1:9121",
Path: "/stream",
RawQuery: "key=val",
}, conn.URL)
case "stream key":
require.Equal(t, &url.URL{
Scheme: "rtmp",
Host: "127.0.0.1:9121",
Path: "/stream/key",
}, conn.URL)
case "stream key and query":
require.Equal(t, &url.URL{
Scheme: "rtmp",
Host: "127.0.0.1:9121",
Path: "/stream/key",
RawQuery: "key=val",
}, conn.URL)
}
}()
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)
var app string
var tcURL string
switch ca {
case "standard":
app = "stream"
tcURL = "rtmp://127.0.0.1:9121/stream"
case "leading slash":
app = "/stream"
tcURL = "rtmp://127.0.0.1:9121//stream"
case "query":
app = "stream?key=val"
tcURL = "rtmp://127.0.0.1:9121/stream?key=val"
case "stream key":
app = "stream"
tcURL = "rtmp://127.0.0.1:9121/stream"
case "stream key and query":
app = "stream"
tcURL = "rtmp://127.0.0.1:9121/stream"
case "neko":
app = "stream"
tcURL = "'rtmp://127.0.0.1:9121/stream"
}
err = mrw.Write(&message.CommandAMF0{
ChunkStreamID: 3,
Name: "connect",
CommandID: 1,
Arguments: []interface{}{
amf0.Object{
{Key: "app", Value: app},
{Key: "flashVer", Value: "LNX 9,0,124,2"},
{Key: "tcUrl", Value: tcURL},
{Key: "fpad", Value: false},
{Key: "capabilities", Value: float64(15)},
{Key: "audioCodecs", Value: float64(4071)},
{Key: "videoCodecs", Value: float64(252)},
{Key: "videoFunction", Value: float64(1)},
},
},
})
require.NoError(t, err)
msg, err := mrw.Read()
require.NoError(t, err)
require.Equal(t, &message.SetWindowAckSize{
Value: 2500000,
}, msg)
msg, err = mrw.Read()
require.NoError(t, err)
require.Equal(t, &message.SetPeerBandwidth{
Value: 2500000,
Type: 2,
}, msg)
msg, err = mrw.Read()
require.NoError(t, err)
require.Equal(t, &message.SetChunkSize{
Value: 65536,
}, msg)
msg, err = mrw.Read()
require.NoError(t, err)
require.Equal(t, &message.CommandAMF0{
ChunkStreamID: 3,
Name: "_result",
CommandID: 1,
Arguments: []interface{}{
amf0.Object{
{Key: "fmsVer", Value: "LNX 9,0,124,2"},
{Key: "capabilities", Value: float64(31)},
},
amf0.Object{
{Key: "level", Value: "status"},
{Key: "code", Value: "NetConnection.Connect.Success"},
{Key: "description", Value: "Connection succeeded."},
{Key: "objectEncoding", Value: float64(0)},
},
},
}, msg)
err = mrw.Write(&message.SetChunkSize{
Value: 65536,
})
require.NoError(t, err)
err = mrw.Write(&message.CommandAMF0{
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.CommandAMF0{
ChunkStreamID: 3,
Name: "_result",
CommandID: 2,
Arguments: []interface{}{
nil,
float64(1),
},
}, msg)
err = mrw.Write(&message.UserControlSetBufferLength{
BufferLength: 0x64,
})
require.NoError(t, err)
var streamKey string
switch ca {
case "stream key":
streamKey = "key"
case "stream key and query":
streamKey = "key?key=val"
}
err = mrw.Write(&message.CommandAMF0{
ChunkStreamID: 4,
MessageStreamID: 0x1000000,
Name: "play",
CommandID: 0,
Arguments: []interface{}{
nil,
streamKey,
},
})
require.NoError(t, err)
<-done
})
}
}
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
}