mirror of
https://github.com/aler9/rtsp-simple-server
synced 2025-09-27 03:56:15 +08:00

* support reading AV1, VP9, H265, Opus, AC-3, G711, LPCM * support reading multiple video or audio tracks at once
517 lines
11 KiB
Go
517 lines
11 KiB
Go
package rtmp
|
|
|
|
import (
|
|
"crypto/md5"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"io"
|
|
"net/url"
|
|
"strings"
|
|
|
|
"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"
|
|
)
|
|
|
|
const (
|
|
serverSalt = "testsalt"
|
|
serverChallenge = "testchallenge"
|
|
)
|
|
|
|
func queryDecode(enc string) map[string]string {
|
|
// do not use url.ParseQuery since values are not URL-encoded
|
|
vals := make(map[string]string)
|
|
|
|
for _, kv := range strings.Split(enc, "&") {
|
|
tmp := strings.SplitN(kv, "=", 2)
|
|
if len(tmp) == 2 {
|
|
vals[tmp[0]] = tmp[1]
|
|
}
|
|
}
|
|
|
|
return vals
|
|
}
|
|
|
|
func queryEncode(dec map[string]string) string {
|
|
tmp := make([]string, len(dec))
|
|
i := 0
|
|
|
|
for k, v := range dec {
|
|
tmp[i] = k + "=" + v
|
|
i++
|
|
}
|
|
|
|
return strings.Join(tmp, "&")
|
|
}
|
|
|
|
func authResponse(user, pass, salt, opaque, challenge, challenge2 string) string {
|
|
h := md5.New()
|
|
h.Write([]byte(user))
|
|
h.Write([]byte(salt))
|
|
h.Write([]byte(pass))
|
|
str := base64.StdEncoding.EncodeToString(h.Sum(nil))
|
|
|
|
h = md5.New()
|
|
h.Write([]byte(str))
|
|
if opaque != "" {
|
|
h.Write([]byte(opaque))
|
|
} else {
|
|
h.Write([]byte(challenge))
|
|
}
|
|
h.Write([]byte(challenge2))
|
|
return base64.StdEncoding.EncodeToString(h.Sum(nil))
|
|
}
|
|
|
|
func buildURL(tcURL string, app string, streamKey string) (*url.URL, error) {
|
|
raw := "/" + app
|
|
if streamKey != "" {
|
|
raw += "/" + streamKey
|
|
}
|
|
|
|
u, err := url.ParseRequestURI(raw)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
tu, err := url.Parse(tcURL)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if tu.Host == "" {
|
|
return nil, fmt.Errorf("invalid host")
|
|
}
|
|
u.Host = tu.Host
|
|
|
|
if tu.Scheme == "" {
|
|
return nil, fmt.Errorf("invalid scheme")
|
|
}
|
|
u.Scheme = tu.Scheme
|
|
|
|
return u, nil
|
|
}
|
|
|
|
func objectOrArray(in interface{}) (amf0.Object, bool) {
|
|
switch o := in.(type) {
|
|
case amf0.Object:
|
|
return o, true
|
|
|
|
case amf0.ECMAArray:
|
|
return amf0.Object(o), true
|
|
|
|
default:
|
|
return nil, false
|
|
}
|
|
}
|
|
|
|
// ServerConn is a server-side RTMP connection.
|
|
type ServerConn struct {
|
|
RW io.ReadWriter
|
|
|
|
// filled by Initialize
|
|
connectChunkStreamID byte
|
|
connectCommandID int
|
|
app string
|
|
tcURL string
|
|
FourCcList amf0.StrictArray
|
|
|
|
// filled by Accept
|
|
URL *url.URL
|
|
Publish bool
|
|
|
|
bc *bytecounter.ReadWriter
|
|
mrw *message.ReadWriter
|
|
}
|
|
|
|
// Initialize initializes ServerConn.
|
|
func (c *ServerConn) Initialize() error {
|
|
c.bc = bytecounter.NewReadWriter(c.RW)
|
|
|
|
keyIn, keyOut, err := handshake.DoServer(c.bc, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var rw io.ReadWriter
|
|
if keyIn != nil {
|
|
rw, err = newRC4ReadWriter(c.bc, keyIn, keyOut)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
rw = c.bc
|
|
}
|
|
|
|
c.mrw = message.NewReadWriter(rw, c.bc, false)
|
|
|
|
connectCmd, err := readCommand(c.mrw)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if connectCmd.Name != "connect" {
|
|
return fmt.Errorf("unexpected command: %+v", connectCmd)
|
|
}
|
|
|
|
if len(connectCmd.Arguments) < 1 {
|
|
return fmt.Errorf("invalid connect command: %+v", connectCmd)
|
|
}
|
|
|
|
c.connectChunkStreamID = connectCmd.ChunkStreamID
|
|
c.connectCommandID = connectCmd.CommandID
|
|
|
|
connectObject, ok := objectOrArray(connectCmd.Arguments[0])
|
|
if !ok {
|
|
return fmt.Errorf("invalid connect command: %+v", connectCmd)
|
|
}
|
|
|
|
c.app, ok = connectObject.GetString("app")
|
|
if !ok {
|
|
return fmt.Errorf("invalid connect command: %+v", connectCmd)
|
|
}
|
|
|
|
c.tcURL, ok = connectObject.GetString("tcUrl")
|
|
if !ok {
|
|
c.tcURL, ok = connectObject.GetString("tcurl")
|
|
if !ok {
|
|
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
|
|
}
|
|
|
|
// CheckCredentials checks credentials.
|
|
func (c *ServerConn) CheckCredentials(expectedUser string, expectedPass string) error {
|
|
i := strings.Index(c.app, "?authmod=adobe")
|
|
if i < 0 {
|
|
err := c.mrw.Write(&message.CommandAMF0{
|
|
ChunkStreamID: c.connectChunkStreamID,
|
|
Name: "_error",
|
|
CommandID: c.connectCommandID,
|
|
Arguments: []interface{}{
|
|
nil,
|
|
amf0.Object{
|
|
{Key: "level", Value: "error"},
|
|
{Key: "code", Value: "NetConnection.Connect.Rejected"},
|
|
{Key: "description", Value: "code=403 need auth; authmod=adobe"},
|
|
},
|
|
},
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return fmt.Errorf("need auth")
|
|
}
|
|
|
|
authParams := c.app[i+1:]
|
|
vals := queryDecode(authParams)
|
|
|
|
user := vals["user"]
|
|
if user == "" {
|
|
return fmt.Errorf("user not provided")
|
|
}
|
|
|
|
clientChallenge := vals["challenge"]
|
|
response := vals["response"]
|
|
|
|
if clientChallenge == "" || response == "" {
|
|
err := c.mrw.Write(&message.CommandAMF0{
|
|
ChunkStreamID: c.connectChunkStreamID,
|
|
Name: "_error",
|
|
CommandID: c.connectCommandID,
|
|
Arguments: []interface{}{
|
|
nil,
|
|
amf0.Object{
|
|
{Key: "level", Value: "error"},
|
|
{Key: "code", Value: "NetConnection.Connect.Rejected"},
|
|
{
|
|
Key: "description",
|
|
Value: fmt.Sprintf("authmod=adobe ?reason=needauth&user=%s&salt=%s&challenge=%s",
|
|
user, serverSalt, serverChallenge),
|
|
},
|
|
},
|
|
},
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return fmt.Errorf("need auth 2")
|
|
}
|
|
|
|
expectedResponse := authResponse(expectedUser, expectedPass, serverSalt, "", serverChallenge, clientChallenge)
|
|
if expectedResponse != response {
|
|
err := c.mrw.Write(&message.CommandAMF0{
|
|
ChunkStreamID: c.connectChunkStreamID,
|
|
Name: "_error",
|
|
CommandID: c.connectCommandID,
|
|
Arguments: []interface{}{
|
|
nil,
|
|
amf0.Object{
|
|
{Key: "level", Value: "error"},
|
|
{Key: "code", Value: "NetConnection.Connect.Rejected"},
|
|
{Key: "description", Value: "authmod=adobe ?reason=authfailed"},
|
|
},
|
|
},
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return fmt.Errorf("authentication failed")
|
|
}
|
|
|
|
// remove auth parameters from app
|
|
c.app = c.app[:i]
|
|
delete(vals, "authmod")
|
|
delete(vals, "user")
|
|
delete(vals, "challenge")
|
|
delete(vals, "response")
|
|
q := queryEncode(vals)
|
|
if q != "" {
|
|
c.app += "?" + q
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Accept accepts the connection.
|
|
func (c *ServerConn) Accept() error {
|
|
err := c.mrw.Write(&message.SetWindowAckSize{
|
|
Value: 2500000,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = c.mrw.Write(&message.SetPeerBandwidth{
|
|
Value: 2500000,
|
|
Type: 2,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = c.mrw.Write(&message.SetChunkSize{
|
|
Value: 65536,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = c.mrw.Write(&message.CommandAMF0{
|
|
ChunkStreamID: c.connectChunkStreamID,
|
|
Name: "_result",
|
|
CommandID: c.connectCommandID,
|
|
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(encodingAMF0)},
|
|
},
|
|
},
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for {
|
|
var cmd *message.CommandAMF0
|
|
cmd, err = readCommand(c.mrw)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
switch cmd.Name {
|
|
case "createStream":
|
|
err = c.mrw.Write(&message.CommandAMF0{
|
|
ChunkStreamID: cmd.ChunkStreamID,
|
|
Name: "_result",
|
|
CommandID: cmd.CommandID,
|
|
Arguments: []interface{}{
|
|
nil,
|
|
float64(1),
|
|
},
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
case "play":
|
|
if len(cmd.Arguments) < 2 {
|
|
return fmt.Errorf("invalid play command arguments")
|
|
}
|
|
|
|
streamKey, ok := cmd.Arguments[1].(string)
|
|
if !ok {
|
|
return fmt.Errorf("invalid play command arguments")
|
|
}
|
|
|
|
c.URL, err = buildURL(c.tcURL, c.app, streamKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = c.mrw.Write(&message.UserControlStreamIsRecorded{
|
|
StreamID: 1,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = c.mrw.Write(&message.UserControlStreamBegin{
|
|
StreamID: 1,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = c.mrw.Write(&message.CommandAMF0{
|
|
ChunkStreamID: 5,
|
|
MessageStreamID: 0x1000000,
|
|
Name: "onStatus",
|
|
CommandID: cmd.CommandID,
|
|
Arguments: []interface{}{
|
|
nil,
|
|
amf0.Object{
|
|
{Key: "level", Value: "status"},
|
|
{Key: "code", Value: "NetStream.Play.Reset"},
|
|
{Key: "description", Value: "play reset"},
|
|
},
|
|
},
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = c.mrw.Write(&message.CommandAMF0{
|
|
ChunkStreamID: 5,
|
|
MessageStreamID: 0x1000000,
|
|
Name: "onStatus",
|
|
CommandID: cmd.CommandID,
|
|
Arguments: []interface{}{
|
|
nil,
|
|
amf0.Object{
|
|
{Key: "level", Value: "status"},
|
|
{Key: "code", Value: "NetStream.Play.Start"},
|
|
{Key: "description", Value: "play start"},
|
|
},
|
|
},
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = c.mrw.Write(&message.CommandAMF0{
|
|
ChunkStreamID: 5,
|
|
MessageStreamID: 0x1000000,
|
|
Name: "onStatus",
|
|
CommandID: cmd.CommandID,
|
|
Arguments: []interface{}{
|
|
nil,
|
|
amf0.Object{
|
|
{Key: "level", Value: "status"},
|
|
{Key: "code", Value: "NetStream.Data.Start"},
|
|
{Key: "description", Value: "data start"},
|
|
},
|
|
},
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = c.mrw.Write(&message.CommandAMF0{
|
|
ChunkStreamID: 5,
|
|
MessageStreamID: 0x1000000,
|
|
Name: "onStatus",
|
|
CommandID: cmd.CommandID,
|
|
Arguments: []interface{}{
|
|
nil,
|
|
amf0.Object{
|
|
{Key: "level", Value: "status"},
|
|
{Key: "code", Value: "NetStream.Play.PublishNotify"},
|
|
{Key: "description", Value: "publish notify"},
|
|
},
|
|
},
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
c.Publish = false
|
|
return nil
|
|
|
|
case "publish":
|
|
if len(cmd.Arguments) < 2 {
|
|
return fmt.Errorf("invalid publish command arguments")
|
|
}
|
|
|
|
streamKey, ok := cmd.Arguments[1].(string)
|
|
if !ok {
|
|
return fmt.Errorf("invalid publish command arguments")
|
|
}
|
|
|
|
c.URL, err = buildURL(c.tcURL, c.app, streamKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = c.mrw.Write(&message.CommandAMF0{
|
|
ChunkStreamID: 5,
|
|
Name: "onStatus",
|
|
CommandID: cmd.CommandID,
|
|
MessageStreamID: 0x1000000,
|
|
Arguments: []interface{}{
|
|
nil,
|
|
amf0.Object{
|
|
{Key: "level", Value: "status"},
|
|
{Key: "code", Value: "NetStream.Publish.Start"},
|
|
{Key: "description", Value: "publish start"},
|
|
},
|
|
},
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
c.Publish = true
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
// BytesReceived returns the number of bytes received.
|
|
func (c *ServerConn) BytesReceived() uint64 {
|
|
return c.bc.Reader.Count()
|
|
}
|
|
|
|
// BytesSent returns the number of bytes sent.
|
|
func (c *ServerConn) BytesSent() uint64 {
|
|
return c.bc.Writer.Count()
|
|
}
|
|
|
|
// Read reads a message.
|
|
func (c *ServerConn) Read() (message.Message, error) {
|
|
return c.mrw.Read()
|
|
}
|
|
|
|
// Write writes a message.
|
|
func (c *ServerConn) Write(msg message.Message) error {
|
|
return c.mrw.Write(msg)
|
|
}
|