install go2rtc on bob
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
## Tests
|
||||
|
||||
- go2rtc rtmp client => Reolink
|
||||
- go2rtc rtmp server <= Dahua
|
||||
- go2rtc rtmp publish => YouTube
|
||||
- go2rtc rtmp publish => Telegram
|
||||
|
||||
## Logs
|
||||
|
||||
```
|
||||
request []interface {}{"connect", 1, map[string]interface {}{"app":"s", "flashVer":"FMLE/3.0 (compatible; FMSc/1.0)", "tcUrl":"rtmps://xxx.rtmp.t.me/s/xxxxx"}}
|
||||
response []interface {}{"_result", 1, map[string]interface {}{"capabilities":31, "fmsVer":"FMS/3,0,1,123"}, map[string]interface {}{"code":"NetConnection.Connect.Success", "description":"Connection succeeded.", "level":"status", "objectEncoding":0}}
|
||||
request []interface {}{"releaseStream", 2, interface {}(nil), "xxxxx"}
|
||||
request []interface {}{"FCPublish", 3, interface {}(nil), "xxxxx"}
|
||||
request []interface {}{"createStream", 4, interface {}(nil)}
|
||||
response []interface {}{"_result", 2, interface {}(nil)}
|
||||
response []interface {}{"_result", 4, interface {}(nil), 1}
|
||||
request []interface {}{"publish", 5, interface {}(nil), "xxxxx", "live"}
|
||||
response []interface {}{"onStatus", 0, interface {}(nil), map[string]interface {}{"code":"NetStream.Publish.Start", "description":"xxxxx is now published", "detail":"xxxxx", "level":"status"}}
|
||||
```
|
||||
|
||||
## Useful links
|
||||
|
||||
- https://en.wikipedia.org/wiki/Flash_Video
|
||||
- https://en.wikipedia.org/wiki/Real-Time_Messaging_Protocol
|
||||
- https://rtmp.veriskope.com/pdf/rtmp_specification_1.0.pdf
|
||||
- https://rtmp.veriskope.com/docs/spec/
|
||||
@@ -0,0 +1,161 @@
|
||||
package rtmp
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"net"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/flv"
|
||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||
)
|
||||
|
||||
func DialPlay(rawURL string) (*flv.Producer, error) {
|
||||
u, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
conn, err := tcp.Dial(u, core.ConnDialTimeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client, err := NewClient(conn, u)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = client.play(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return client.Producer()
|
||||
}
|
||||
|
||||
func DialPublish(rawURL string, cons *flv.Consumer) (io.Writer, error) {
|
||||
u, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
conn, err := tcp.Dial(u, core.ConnDialTimeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client, err := NewClient(conn, u)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = client.publish(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cons.FormatName = "rtmp"
|
||||
cons.Protocol = "rtmp"
|
||||
cons.RemoteAddr = conn.RemoteAddr().String()
|
||||
cons.URL = rawURL
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func NewClient(conn net.Conn, u *url.URL) (*Conn, error) {
|
||||
c := &Conn{
|
||||
url: u.String(),
|
||||
|
||||
conn: conn,
|
||||
rd: bufio.NewReaderSize(conn, core.BufferSize),
|
||||
wr: conn,
|
||||
|
||||
chunks: map[uint8]*chunk{},
|
||||
|
||||
rdPacketSize: 128,
|
||||
wrPacketSize: 4096, // OBS - 4096, Reolink - 4096
|
||||
}
|
||||
|
||||
if args := strings.Split(u.Path, "/"); len(args) >= 2 {
|
||||
c.App = args[1]
|
||||
if len(args) >= 3 {
|
||||
c.Stream = args[2]
|
||||
if u.RawQuery != "" {
|
||||
c.Stream += "?" + u.RawQuery
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := c.clienHandshake(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := c.writePacketSize(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (c *Conn) clienHandshake() error {
|
||||
// simple handshake without real random and check response
|
||||
b := make([]byte, 1+1536)
|
||||
b[0] = 0x03
|
||||
// write C0+C1
|
||||
if _, err := c.conn.Write(b); err != nil {
|
||||
return err
|
||||
}
|
||||
// read S0+S1
|
||||
if _, err := io.ReadFull(c.rd, b); err != nil {
|
||||
return err
|
||||
}
|
||||
// write S1
|
||||
if _, err := c.conn.Write(b[1:]); err != nil {
|
||||
return err
|
||||
}
|
||||
// read C1, skip check
|
||||
if _, err := io.ReadFull(c.rd, b[1:]); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) play() error {
|
||||
if err := c.writeConnect(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.writeCreateStream(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.writePlay(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) publish() error {
|
||||
if err := c.writeConnect(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.writeReleaseStream(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.writeCreateStream(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.writePublish(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go func() {
|
||||
for {
|
||||
_, _, _, err := c.readMessage()
|
||||
//log.Printf("!!! %d %d %.30x", msgType, timeMS, b)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,376 @@
|
||||
package rtmp
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/flv/amf"
|
||||
)
|
||||
|
||||
const (
|
||||
TypeSetPacketSize = 1
|
||||
TypeServerBandwidth = 5
|
||||
TypeClientBandwidth = 6
|
||||
TypeAudio = 8
|
||||
TypeVideo = 9
|
||||
TypeData = 18
|
||||
TypeCommand = 20
|
||||
)
|
||||
|
||||
type Conn struct {
|
||||
App string
|
||||
Stream string
|
||||
Intent string
|
||||
|
||||
rdPacketSize uint32
|
||||
wrPacketSize uint32
|
||||
|
||||
chunks map[byte]*chunk
|
||||
streamID byte
|
||||
url string
|
||||
|
||||
conn net.Conn
|
||||
rd io.Reader
|
||||
wr io.Writer
|
||||
|
||||
rdBuf []byte
|
||||
wrBuf []byte
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func (c *Conn) Close() error {
|
||||
return c.conn.Close()
|
||||
}
|
||||
|
||||
func (c *Conn) readResponse(wait func(items []any) bool) ([]any, error) {
|
||||
for {
|
||||
msgType, _, b, err := c.readMessage()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
//log.Printf("[rtmp] type=%d data=%s", msgType, b)
|
||||
|
||||
switch msgType {
|
||||
case TypeSetPacketSize:
|
||||
c.rdPacketSize = binary.BigEndian.Uint32(b)
|
||||
case TypeCommand:
|
||||
items, _ := amf.NewReader(b).ReadItems()
|
||||
if wait(items) {
|
||||
return items, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type chunk struct {
|
||||
conn *Conn
|
||||
rawTime uint32
|
||||
dataSize uint32
|
||||
tagType byte
|
||||
streamID uint32
|
||||
timeMS uint32
|
||||
}
|
||||
|
||||
func (c *chunk) readHeader(typ byte) error {
|
||||
switch typ {
|
||||
case 0: // 12 byte header (full header)
|
||||
b, err := c.conn.readSize(11)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.rawTime = Uint24(b)
|
||||
c.dataSize = Uint24(b[3:])
|
||||
c.tagType = b[6]
|
||||
c.streamID = binary.LittleEndian.Uint32(b[7:])
|
||||
c.timeMS = c.readExtendedTime()
|
||||
|
||||
case 1: // 8 bytes - like type b00, not including message ID (4 last bytes)
|
||||
b, err := c.conn.readSize(7)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.rawTime = Uint24(b)
|
||||
c.dataSize = Uint24(b[3:]) // msgdatalen
|
||||
c.tagType = b[6] // msgtypeid
|
||||
c.timeMS += c.readExtendedTime()
|
||||
|
||||
case 2: // 4 bytes - Basic Header and timestamp (3 bytes) are included
|
||||
b, err := c.conn.readSize(3)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.rawTime = Uint24(b) // timestamp
|
||||
c.timeMS += c.readExtendedTime()
|
||||
|
||||
case 3: // 1 byte - only the Basic Header is included
|
||||
// use here hdr from previous msg with same session ID (sid)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *chunk) readExtendedTime() uint32 {
|
||||
if c.rawTime == 0xFFFFFF {
|
||||
if b, err := c.conn.readSize(4); err == nil {
|
||||
return binary.BigEndian.Uint32(b)
|
||||
}
|
||||
}
|
||||
return c.rawTime
|
||||
}
|
||||
|
||||
//var ErrNotImplemented = errors.New("rtmp: not implemented")
|
||||
|
||||
func (c *Conn) readMessage() (byte, uint32, []byte, error) {
|
||||
b, err := c.readSize(1) // doesn't support big chunkID!!!
|
||||
if err != nil {
|
||||
return 0, 0, nil, err
|
||||
}
|
||||
|
||||
hdrType := b[0] >> 6
|
||||
chunkID := b[0] & 0b111111
|
||||
|
||||
// storing header information for support header type 3
|
||||
ch, ok := c.chunks[chunkID]
|
||||
if !ok {
|
||||
ch = &chunk{conn: c}
|
||||
c.chunks[chunkID] = ch
|
||||
}
|
||||
|
||||
if err = ch.readHeader(hdrType); err != nil {
|
||||
return 0, 0, nil, err
|
||||
}
|
||||
|
||||
//log.Printf("[rtmp] hdr=%d chunkID=%d timeMS=%d size=%d tagType=%d streamID=%d", hdrType, chunkID, ch.timeMS, ch.dataSize, ch.tagType, ch.streamID)
|
||||
|
||||
// 1. Response zero size
|
||||
if ch.dataSize == 0 {
|
||||
return ch.tagType, ch.timeMS, nil, nil
|
||||
}
|
||||
|
||||
data := make([]byte, ch.dataSize)
|
||||
|
||||
// 2. Response small packet
|
||||
if ch.dataSize <= c.rdPacketSize {
|
||||
if _, err = io.ReadFull(c.rd, data); err != nil {
|
||||
return 0, 0, nil, err
|
||||
}
|
||||
return ch.tagType, ch.timeMS, data, nil
|
||||
}
|
||||
|
||||
// 3. Response big packet
|
||||
var i0 uint32
|
||||
for i1 := c.rdPacketSize; i1 < ch.dataSize; i1 += c.rdPacketSize {
|
||||
if _, err = io.ReadFull(c.rd, data[i0:i1]); err != nil {
|
||||
return 0, 0, nil, err
|
||||
}
|
||||
|
||||
// hopefully this will be hdrType=3 with same chunkID
|
||||
if _, err = c.readSize(1); err != nil {
|
||||
return 0, 0, nil, err
|
||||
}
|
||||
|
||||
_ = ch.readExtendedTime()
|
||||
|
||||
i0 = i1
|
||||
}
|
||||
|
||||
if _, err = io.ReadFull(c.rd, data[i0:]); err != nil {
|
||||
return 0, 0, nil, err
|
||||
}
|
||||
|
||||
return ch.tagType, ch.timeMS, data, nil
|
||||
}
|
||||
|
||||
func (c *Conn) writeMessage(chunkID, tagType byte, timeMS uint32, payload []byte) error {
|
||||
c.mu.Lock()
|
||||
c.resetBuffer()
|
||||
|
||||
b := payload
|
||||
size := uint32(len(b))
|
||||
|
||||
if size > c.wrPacketSize {
|
||||
c.appendType0(chunkID, tagType, timeMS, size, b[:c.wrPacketSize])
|
||||
|
||||
for {
|
||||
b = b[c.wrPacketSize:]
|
||||
if uint32(len(b)) > c.wrPacketSize {
|
||||
c.appendType3(chunkID, b[:c.wrPacketSize])
|
||||
} else {
|
||||
c.appendType3(chunkID, b)
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
c.appendType0(chunkID, tagType, timeMS, size, b)
|
||||
}
|
||||
|
||||
//log.Printf("%d %2d %5d %6d %.32x", chunkID, tagType, timeMS, size, payload)
|
||||
|
||||
_, err := c.wr.Write(c.wrBuf)
|
||||
c.mu.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Conn) resetBuffer() {
|
||||
c.wrBuf = c.wrBuf[:0]
|
||||
}
|
||||
|
||||
func (c *Conn) appendType0(chunkID, tagType byte, timeMS, size uint32, payload []byte) {
|
||||
// TODO: timeMS more than 24 bit
|
||||
c.wrBuf = append(c.wrBuf,
|
||||
chunkID,
|
||||
byte(timeMS>>16), byte(timeMS>>8), byte(timeMS),
|
||||
byte(size>>16), byte(size>>8), byte(size),
|
||||
tagType,
|
||||
c.streamID, 0, 0, 0, // little endian streamID
|
||||
)
|
||||
c.wrBuf = append(c.wrBuf, payload...)
|
||||
}
|
||||
|
||||
func (c *Conn) appendType3(chunkID byte, payload []byte) {
|
||||
c.wrBuf = append(c.wrBuf, 3<<6|chunkID)
|
||||
c.wrBuf = append(c.wrBuf, payload...)
|
||||
}
|
||||
|
||||
func (c *Conn) writePacketSize() error {
|
||||
b := binary.BigEndian.AppendUint32(nil, c.wrPacketSize)
|
||||
return c.writeMessage(2, TypeSetPacketSize, 0, b)
|
||||
}
|
||||
|
||||
func (c *Conn) writeConnect() error {
|
||||
b := amf.EncodeItems("connect", 1, map[string]any{
|
||||
"app": c.App,
|
||||
"flashVer": "FMLE/3.0 (compatible; FMSc/1.0)",
|
||||
"tcUrl": c.url,
|
||||
})
|
||||
if err := c.writeMessage(3, TypeCommand, 0, b); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
v, err := c.readResponse(func(items []any) bool {
|
||||
return len(items) >= 3 && items[0] == "_result" && items[1] == float64(1)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
code := getString(v, 3, "code")
|
||||
if code != "NetConnection.Connect.Success" {
|
||||
return fmt.Errorf("rtmp: wrong response %#v", v)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) writeReleaseStream() error {
|
||||
b := amf.EncodeItems("releaseStream", 2, nil, c.Stream)
|
||||
if err := c.writeMessage(3, TypeCommand, 0, b); err != nil {
|
||||
return err
|
||||
}
|
||||
b = amf.EncodeItems("FCPublish", 3, nil, c.Stream)
|
||||
if err := c.writeMessage(3, TypeCommand, 0, b); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (c *Conn) writeCreateStream() error {
|
||||
b := amf.EncodeItems("createStream", 4, nil)
|
||||
if err := c.writeMessage(3, TypeCommand, 0, b); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
v, err := c.readResponse(func(items []any) bool {
|
||||
return len(items) >= 3 && items[0] == "_result" && items[1] == float64(4)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(v) == 4 {
|
||||
if f, ok := v[3].(float64); ok {
|
||||
c.streamID = byte(f)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("rtmp: wrong response %#v", v)
|
||||
}
|
||||
|
||||
func (c *Conn) writePublish() error {
|
||||
b := amf.EncodeItems("publish", 5, nil, c.Stream, "live")
|
||||
if err := c.writeMessage(3, TypeCommand, 0, b); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// YouTube can response with "onBWDone 0"
|
||||
v, err := c.readResponse(func(items []any) bool {
|
||||
return len(items) >= 3 && items[0] == "onStatus"
|
||||
})
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
code := getString(v, 3, "code")
|
||||
if code != "NetStream.Publish.Start" {
|
||||
return fmt.Errorf("rtmp: wrong response %#v", v)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) writePlay() error {
|
||||
b := amf.EncodeItems("play", 5, nil, c.Stream)
|
||||
if err := c.writeMessage(3, TypeCommand, 0, b); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Reolink response with ID=0, other software respose with ID=5
|
||||
v, err := c.readResponse(func(items []any) bool {
|
||||
return len(items) >= 3 && items[0] == "onStatus"
|
||||
})
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
code := getString(v, 3, "code")
|
||||
if !strings.HasPrefix(code, "NetStream.Play.") {
|
||||
return fmt.Errorf("rtmp: wrong response %#v", v)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) readSize(n uint32) ([]byte, error) {
|
||||
b := make([]byte, n)
|
||||
if _, err := io.ReadFull(c.rd, b); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func PutUint24(b []byte, v uint32) {
|
||||
_ = b[2]
|
||||
b[0] = byte(v >> 16)
|
||||
b[1] = byte(v >> 8)
|
||||
b[2] = byte(v)
|
||||
}
|
||||
|
||||
func Uint24(b []byte) uint32 {
|
||||
_ = b[2]
|
||||
return uint32(b[0])<<16 | uint32(b[1])<<8 | uint32(b[2])
|
||||
}
|
||||
|
||||
func getString(v []any, i int, key string) string {
|
||||
if len(v) <= i {
|
||||
return ""
|
||||
}
|
||||
if v, ok := v[i].(map[string]any); ok {
|
||||
if s, ok := v[key].(string); ok {
|
||||
return s
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package rtmp
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/flv"
|
||||
)
|
||||
|
||||
func (c *Conn) Producer() (*flv.Producer, error) {
|
||||
c.rdBuf = []byte{
|
||||
'F', 'L', 'V', // signature
|
||||
1, // version
|
||||
0, // flags (has video/audio)
|
||||
0, 0, 0, 9, // header size
|
||||
}
|
||||
|
||||
prod, err := flv.Open(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
prod.FormatName = "rtmp"
|
||||
prod.Protocol = "rtmp"
|
||||
prod.RemoteAddr = c.conn.RemoteAddr().String()
|
||||
prod.URL = c.url
|
||||
|
||||
return prod, nil
|
||||
}
|
||||
|
||||
// Read - convert RTMP to FLV format
|
||||
func (c *Conn) Read(p []byte) (n int, err error) {
|
||||
// 1. Check temporary tempbuffer
|
||||
if len(c.rdBuf) == 0 {
|
||||
msgType, timeMS, payload, err2 := c.readMessage()
|
||||
if err2 != nil {
|
||||
return 0, err2
|
||||
}
|
||||
|
||||
// previous tag size (4 byte) + header (11 byte) + payload
|
||||
n = 4 + 11 + len(payload)
|
||||
|
||||
// 2. Check if the message fits in the buffer
|
||||
if n <= len(p) {
|
||||
encodeFLV(p, msgType, timeMS, payload)
|
||||
return
|
||||
}
|
||||
|
||||
// 3. Put the message into a temporary buffer
|
||||
c.rdBuf = make([]byte, n)
|
||||
encodeFLV(c.rdBuf, msgType, timeMS, payload)
|
||||
}
|
||||
|
||||
// 4. Send temporary buffer
|
||||
n = copy(p, c.rdBuf)
|
||||
c.rdBuf = c.rdBuf[n:]
|
||||
return
|
||||
}
|
||||
|
||||
func encodeFLV(b []byte, msgType byte, time uint32, payload []byte) {
|
||||
_ = b[4+11]
|
||||
|
||||
b[0] = 0
|
||||
b[1] = 0
|
||||
b[2] = 0
|
||||
b[3] = 0
|
||||
b[4+0] = msgType
|
||||
PutUint24(b[4+1:], uint32(len(payload)))
|
||||
PutUint24(b[4+4:], time)
|
||||
b[4+7] = byte(time >> 24)
|
||||
|
||||
copy(b[4+11:], payload)
|
||||
}
|
||||
|
||||
// Write - convert FLV format to RTMP format
|
||||
func (c *Conn) Write(p []byte) (n int, err error) {
|
||||
n = len(p)
|
||||
|
||||
if p[0] == 'F' {
|
||||
p = p[9+4:] // skip first msg with FLV header
|
||||
|
||||
for len(p) > 0 {
|
||||
size := 11 + uint16(p[2])<<8 + uint16(p[3]) + 4
|
||||
if _, err = c.Write(p[:size]); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
p = p[size:]
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// decode FLV: 11 bytes header + payload + 4 byte size
|
||||
tagType := p[0]
|
||||
timeMS := uint32(p[4])<<16 | uint32(p[5])<<8 | uint32(p[6]) | uint32(p[7])<<24
|
||||
payload := p[11 : len(p)-4]
|
||||
|
||||
err = c.writeMessage(4, tagType, timeMS, payload)
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
package rtmp
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/flv/amf"
|
||||
)
|
||||
|
||||
func NewServer(conn net.Conn) (*Conn, error) {
|
||||
c := &Conn{
|
||||
conn: conn,
|
||||
rd: bufio.NewReaderSize(conn, core.BufferSize),
|
||||
wr: conn,
|
||||
|
||||
chunks: map[uint8]*chunk{},
|
||||
|
||||
rdPacketSize: 128,
|
||||
wrPacketSize: 4096,
|
||||
}
|
||||
|
||||
if err := c.serverHandshake(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := c.writePacketSize(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (c *Conn) serverHandshake() error {
|
||||
// based on https://rtmp.veriskope.com/docs/spec/
|
||||
_ = c.conn.SetDeadline(time.Now().Add(core.ConnDeadline))
|
||||
|
||||
// read C0
|
||||
b := make([]byte, 1)
|
||||
if _, err := io.ReadFull(c.rd, b); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if b[0] != 3 {
|
||||
return errors.New("rtmp: wrong handshake")
|
||||
}
|
||||
|
||||
// write S0
|
||||
if _, err := c.conn.Write([]byte{3}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b = make([]byte, 1536)
|
||||
|
||||
// write S1
|
||||
tsS1 := nowMS()
|
||||
binary.BigEndian.PutUint32(b, tsS1)
|
||||
binary.BigEndian.PutUint32(b[4:], 0)
|
||||
_, _ = rand.Read(b[8:])
|
||||
if _, err := c.conn.Write(b); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// read C1
|
||||
if _, err := io.ReadFull(c.rd, b); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// write S2
|
||||
tsS2 := nowMS()
|
||||
binary.BigEndian.PutUint32(b, tsS1)
|
||||
binary.BigEndian.PutUint32(b[4:], tsS2)
|
||||
if _, err := c.conn.Write(b); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// read C2
|
||||
if _, err := io.ReadFull(c.rd, b); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_ = c.conn.SetDeadline(time.Time{})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) ReadCommands() error {
|
||||
for {
|
||||
msgType, _, b, err := c.readMessage()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
//log.Printf("%d %.256x", msgType, b)
|
||||
|
||||
switch msgType {
|
||||
case TypeSetPacketSize:
|
||||
c.rdPacketSize = binary.BigEndian.Uint32(b)
|
||||
case TypeCommand:
|
||||
if err = c.acceptCommand(b); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.Intent != "" {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
CommandConnect = "connect"
|
||||
CommandReleaseStream = "releaseStream"
|
||||
CommandFCPublish = "FCPublish"
|
||||
CommandCreateStream = "createStream"
|
||||
CommandPublish = "publish"
|
||||
CommandPlay = "play"
|
||||
)
|
||||
|
||||
func (c *Conn) acceptCommand(b []byte) error {
|
||||
items, err := amf.NewReader(b).ReadItems()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
//log.Printf("%#v", items)
|
||||
|
||||
if len(items) < 2 {
|
||||
return fmt.Errorf("rtmp: read command %x", b)
|
||||
}
|
||||
|
||||
cmd, ok := items[0].(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("rtmp: read command %x", b)
|
||||
}
|
||||
|
||||
tID, ok := items[1].(float64) // transaction ID
|
||||
if !ok {
|
||||
return fmt.Errorf("rtmp: read command %x", b)
|
||||
}
|
||||
|
||||
switch cmd {
|
||||
case CommandConnect:
|
||||
if len(items) == 3 {
|
||||
if v, ok := items[2].(map[string]any); ok {
|
||||
c.App, _ = v["app"].(string)
|
||||
}
|
||||
}
|
||||
|
||||
payload := amf.EncodeItems(
|
||||
"_result", tID,
|
||||
map[string]any{"fmsVer": "FMS/3,0,1,123"},
|
||||
map[string]any{"code": "NetConnection.Connect.Success"},
|
||||
)
|
||||
return c.writeMessage(3, TypeCommand, 0, payload)
|
||||
|
||||
case CommandReleaseStream:
|
||||
// if app is empty - will use key as app
|
||||
if c.App == "" && len(items) == 4 {
|
||||
c.App, _ = items[3].(string)
|
||||
}
|
||||
|
||||
payload := amf.EncodeItems("_result", tID, nil)
|
||||
return c.writeMessage(3, TypeCommand, 0, payload)
|
||||
|
||||
case CommandFCPublish: // no response
|
||||
|
||||
case CommandCreateStream:
|
||||
payload := amf.EncodeItems("_result", tID, nil, 1)
|
||||
return c.writeMessage(3, TypeCommand, 0, payload)
|
||||
|
||||
case CommandPublish, CommandPlay: // response later
|
||||
c.Intent = cmd
|
||||
c.streamID = 1
|
||||
|
||||
default:
|
||||
println("rtmp: unknown command: " + cmd)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) WriteStart() error {
|
||||
var code string
|
||||
if c.Intent == CommandPublish {
|
||||
code = "NetStream.Publish.Start"
|
||||
} else {
|
||||
code = "NetStream.Play.Start"
|
||||
}
|
||||
|
||||
payload := amf.EncodeItems("onStatus", 0, nil, map[string]any{"code": code})
|
||||
return c.writeMessage(3, TypeCommand, 0, payload)
|
||||
}
|
||||
|
||||
func nowMS() uint32 {
|
||||
return uint32(time.Now().UnixNano() / int64(time.Millisecond))
|
||||
}
|
||||
Reference in New Issue
Block a user