install go2rtc on bob
This commit is contained in:
@@ -0,0 +1,239 @@
|
||||
package amf
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"math"
|
||||
)
|
||||
|
||||
const (
|
||||
TypeNumber byte = iota
|
||||
TypeBoolean
|
||||
TypeString
|
||||
TypeObject
|
||||
TypeNull = 5
|
||||
TypeEcmaArray = 8
|
||||
TypeObjectEnd = 9
|
||||
)
|
||||
|
||||
// AMF spec: http://download.macromedia.com/pub/labs/amf/amf0_spec_121207.pdf
|
||||
type AMF struct {
|
||||
buf []byte
|
||||
pos int
|
||||
}
|
||||
|
||||
var ErrRead = errors.New("amf: read error")
|
||||
|
||||
func NewReader(b []byte) *AMF {
|
||||
return &AMF{buf: b}
|
||||
}
|
||||
|
||||
func (a *AMF) ReadItems() ([]any, error) {
|
||||
var items []any
|
||||
for a.pos < len(a.buf) {
|
||||
v, err := a.ReadItem()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, v)
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (a *AMF) ReadItem() (any, error) {
|
||||
dataType, err := a.ReadByte()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch dataType {
|
||||
case TypeNumber:
|
||||
return a.ReadNumber()
|
||||
|
||||
case TypeBoolean:
|
||||
b, err := a.ReadByte()
|
||||
return b != 0, err
|
||||
|
||||
case TypeString:
|
||||
return a.ReadString()
|
||||
|
||||
case TypeObject:
|
||||
return a.ReadObject()
|
||||
|
||||
case TypeEcmaArray:
|
||||
return a.ReadEcmaArray()
|
||||
|
||||
case TypeNull:
|
||||
return nil, nil
|
||||
|
||||
case TypeObjectEnd:
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return nil, ErrRead
|
||||
}
|
||||
|
||||
func (a *AMF) ReadByte() (byte, error) {
|
||||
if a.pos >= len(a.buf) {
|
||||
return 0, ErrRead
|
||||
}
|
||||
|
||||
v := a.buf[a.pos]
|
||||
a.pos++
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func (a *AMF) ReadNumber() (float64, error) {
|
||||
if a.pos+8 > len(a.buf) {
|
||||
return 0, ErrRead
|
||||
}
|
||||
|
||||
v := binary.BigEndian.Uint64(a.buf[a.pos : a.pos+8])
|
||||
a.pos += 8
|
||||
return math.Float64frombits(v), nil
|
||||
}
|
||||
|
||||
func (a *AMF) ReadString() (string, error) {
|
||||
if a.pos+2 > len(a.buf) {
|
||||
return "", ErrRead
|
||||
}
|
||||
|
||||
size := int(binary.BigEndian.Uint16(a.buf[a.pos:]))
|
||||
a.pos += 2
|
||||
|
||||
if a.pos+size > len(a.buf) {
|
||||
return "", ErrRead
|
||||
}
|
||||
|
||||
s := string(a.buf[a.pos : a.pos+size])
|
||||
a.pos += size
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (a *AMF) ReadObject() (map[string]any, error) {
|
||||
obj := make(map[string]any)
|
||||
|
||||
for {
|
||||
k, err := a.ReadString()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
v, err := a.ReadItem()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if k == "" {
|
||||
break
|
||||
}
|
||||
|
||||
obj[k] = v
|
||||
}
|
||||
|
||||
return obj, nil
|
||||
}
|
||||
|
||||
func (a *AMF) ReadEcmaArray() (map[string]any, error) {
|
||||
if a.pos+4 > len(a.buf) {
|
||||
return nil, ErrRead
|
||||
}
|
||||
a.pos += 4 // skip size
|
||||
|
||||
return a.ReadObject()
|
||||
}
|
||||
|
||||
func NewWriter() *AMF {
|
||||
return &AMF{}
|
||||
}
|
||||
|
||||
func (a *AMF) Bytes() []byte {
|
||||
return a.buf
|
||||
}
|
||||
|
||||
func (a *AMF) WriteNumber(n float64) {
|
||||
b := math.Float64bits(n)
|
||||
a.buf = append(
|
||||
a.buf, TypeNumber,
|
||||
byte(b>>56), byte(b>>48), byte(b>>40), byte(b>>32),
|
||||
byte(b>>24), byte(b>>16), byte(b>>8), byte(b),
|
||||
)
|
||||
}
|
||||
|
||||
func (a *AMF) WriteBool(b bool) {
|
||||
if b {
|
||||
a.buf = append(a.buf, TypeBoolean, 1)
|
||||
} else {
|
||||
a.buf = append(a.buf, TypeBoolean, 0)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AMF) WriteString(s string) {
|
||||
n := len(s)
|
||||
a.buf = append(a.buf, TypeString, byte(n>>8), byte(n))
|
||||
a.buf = append(a.buf, s...)
|
||||
}
|
||||
|
||||
func (a *AMF) WriteObject(obj map[string]any) {
|
||||
a.buf = append(a.buf, TypeObject)
|
||||
a.writeKV(obj)
|
||||
a.buf = append(a.buf, 0, 0, TypeObjectEnd)
|
||||
}
|
||||
|
||||
func (a *AMF) WriteEcmaArray(obj map[string]any) {
|
||||
n := len(obj)
|
||||
a.buf = append(a.buf, TypeEcmaArray, byte(n>>24), byte(n>>16), byte(n>>8), byte(n))
|
||||
a.writeKV(obj)
|
||||
a.buf = append(a.buf, 0, 0, TypeObjectEnd)
|
||||
}
|
||||
|
||||
func (a *AMF) writeKV(obj map[string]any) {
|
||||
for k, v := range obj {
|
||||
n := len(k)
|
||||
a.buf = append(a.buf, byte(n>>8), byte(n))
|
||||
a.buf = append(a.buf, k...)
|
||||
|
||||
switch v := v.(type) {
|
||||
case string:
|
||||
a.WriteString(v)
|
||||
case int:
|
||||
a.WriteNumber(float64(v))
|
||||
case uint16:
|
||||
a.WriteNumber(float64(v))
|
||||
case uint32:
|
||||
a.WriteNumber(float64(v))
|
||||
case float64:
|
||||
a.WriteNumber(v)
|
||||
case bool:
|
||||
a.WriteBool(v)
|
||||
default:
|
||||
panic(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AMF) WriteNull() {
|
||||
a.buf = append(a.buf, TypeNull)
|
||||
}
|
||||
|
||||
func EncodeItems(items ...any) []byte {
|
||||
a := &AMF{}
|
||||
for _, item := range items {
|
||||
switch v := item.(type) {
|
||||
case float64:
|
||||
a.WriteNumber(v)
|
||||
case int:
|
||||
a.WriteNumber(float64(v))
|
||||
case string:
|
||||
a.WriteString(v)
|
||||
case map[string]any:
|
||||
a.WriteObject(v)
|
||||
case nil:
|
||||
a.WriteNull()
|
||||
default:
|
||||
panic(v)
|
||||
}
|
||||
}
|
||||
return a.Bytes()
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
package amf
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNewReader(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
actual string
|
||||
expect []any
|
||||
}{
|
||||
{
|
||||
name: "ffmpeg-http",
|
||||
actual: "02000a6f6e4d65746144617461080000001000086475726174696f6e000000000000000000000577696474680040940000000000000006686569676874004086800000000000000d766964656f646174617261746500409e62770000000000096672616d6572617465004038000000000000000c766964656f636f646563696400401c000000000000000d617564696f646174617261746500405ea93000000000000f617564696f73616d706c65726174650040e5888000000000000f617564696f73616d706c6573697a65004030000000000000000673746572656f0101000c617564696f636f6465636964004024000000000000000b6d616a6f725f6272616e640200046d703432000d6d696e6f725f76657273696f6e020001300011636f6d70617469626c655f6272616e647302000c69736f6d617663316d7034320007656e636f64657202000c4c61766636302e352e313030000866696c6573697a65000000000000000000000009",
|
||||
expect: []any{
|
||||
"onMetaData",
|
||||
map[string]any{
|
||||
"compatible_brands": "isomavc1mp42",
|
||||
"major_brand": "mp42",
|
||||
"minor_version": "0",
|
||||
"encoder": "Lavf60.5.100",
|
||||
|
||||
"filesize": float64(0),
|
||||
"duration": float64(0),
|
||||
|
||||
"videocodecid": float64(7),
|
||||
"width": float64(1280),
|
||||
"height": float64(720),
|
||||
"framerate": float64(24),
|
||||
"videodatarate": 1944.6162109375,
|
||||
|
||||
"audiocodecid": float64(10),
|
||||
"audiosamplerate": float64(44100),
|
||||
"stereo": true,
|
||||
"audiosamplesize": float64(16),
|
||||
"audiodatarate": 122.6435546875,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ffmpeg-file",
|
||||
actual: "02000a6f6e4d65746144617461080000000800086475726174696f6e004000000000000000000577696474680040940000000000000006686569676874004086800000000000000d766964656f646174617261746500000000000000000000096672616d6572617465004039000000000000000c766964656f636f646563696400401c0000000000000007656e636f64657202000c4c61766636302e352e313030000866696c6573697a6500411f541400000000000009",
|
||||
expect: []any{
|
||||
"onMetaData",
|
||||
map[string]any{
|
||||
"encoder": "Lavf60.5.100",
|
||||
|
||||
"filesize": float64(513285),
|
||||
"duration": float64(2),
|
||||
|
||||
"videocodecid": float64(7),
|
||||
"width": float64(1280),
|
||||
"height": float64(720),
|
||||
"framerate": float64(25),
|
||||
"videodatarate": float64(0),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "reolink-1",
|
||||
actual: "0200075f726573756c74003ff0000000000000030006666d7356657202000d464d532f332c302c312c313233000c6361706162696c697469657300403f0000000000000000090300056c6576656c0200067374617475730004636f646502001d4e6574436f6e6e656374696f6e2e436f6e6e6563742e53756363657373000b6465736372697074696f6e020015436f6e6e656374696f6e207375636365656465642e000e6f626a656374456e636f64696e67000000000000000000000009",
|
||||
expect: []any{
|
||||
"_result", float64(1),
|
||||
map[string]any{
|
||||
"capabilities": float64(31),
|
||||
"fmsVer": "FMS/3,0,1,123",
|
||||
},
|
||||
map[string]any{
|
||||
"code": "NetConnection.Connect.Success",
|
||||
"description": "Connection succeeded.",
|
||||
"level": "status",
|
||||
"objectEncoding": float64(0),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "reolink-2",
|
||||
actual: "0200075f726573756c7400400000000000000005003ff0000000000000",
|
||||
expect: []any{
|
||||
"_result", float64(2), nil, float64(1),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "reolink-3",
|
||||
actual: "0200086f6e537461747573000000000000000000050300056c6576656c0200067374617475730004636f64650200144e657453747265616d2e506c61792e5374617274000b6465736372697074696f6e020015537461727420766964656f206f6e2064656d616e64000009",
|
||||
expect: []any{
|
||||
"onStatus", float64(0), nil,
|
||||
map[string]any{
|
||||
"code": "NetStream.Play.Start",
|
||||
"description": "Start video on demand",
|
||||
"level": "status",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "reolink-4",
|
||||
actual: "0200117c52746d7053616d706c6541636365737301010101",
|
||||
expect: []any{
|
||||
"|RtmpSampleAccess", true, true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "reolink-5",
|
||||
actual: "02000a6f6e4d6574614461746103000577696474680040a4000000000000000668656967687400409e000000000000000c646973706c617957696474680040a4000000000000000d646973706c617948656967687400409e00000000000000086475726174696f6e000000000000000000000c766964656f636f646563696400401c000000000000000c617564696f636f6465636964004024000000000000000f617564696f73616d706c65726174650040cf40000000000000096672616d657261746500403e000000000000000009",
|
||||
expect: []any{
|
||||
"onMetaData",
|
||||
map[string]any{
|
||||
"duration": float64(0),
|
||||
|
||||
"videocodecid": float64(7),
|
||||
"width": float64(2560),
|
||||
"height": float64(1920),
|
||||
"displayWidth": float64(2560),
|
||||
"displayHeight": float64(1920),
|
||||
"framerate": float64(30),
|
||||
|
||||
"audiocodecid": float64(10),
|
||||
"audiosamplerate": float64(16000),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "mediamtx",
|
||||
actual: "02000d40736574446174614672616d6502000a6f6e4d6574614461746103000d766964656f6461746172617465000000000000000000000c766964656f636f646563696400401c000000000000000d617564696f6461746172617465000000000000000000000c617564696f636f6465636964004024000000000000000009",
|
||||
expect: []any{
|
||||
"@setDataFrame",
|
||||
"onMetaData",
|
||||
map[string]any{
|
||||
"videocodecid": float64(7),
|
||||
"videodatarate": float64(0),
|
||||
"audiocodecid": float64(10),
|
||||
"audiodatarate": float64(0),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "mediamtx",
|
||||
actual: "0200075f726573756c74003ff0000000000000030006666d7356657202000d4c4e5820392c302c3132342c32000c6361706162696c697469657300403f0000000000000000090300056c6576656c0200067374617475730004636f646502001d4e6574436f6e6e656374696f6e2e436f6e6e6563742e53756363657373000b6465736372697074696f6e020015436f6e6e656374696f6e207375636365656465642e000e6f626a656374456e636f64696e67000000000000000000000009",
|
||||
expect: []any{
|
||||
"_result", float64(1), map[string]any{
|
||||
"capabilities": float64(31),
|
||||
"fmsVer": "LNX 9,0,124,2",
|
||||
}, map[string]any{
|
||||
"code": "NetConnection.Connect.Success",
|
||||
"description": "Connection succeeded.",
|
||||
"level": "status",
|
||||
"objectEncoding": float64(0),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "mediamtx",
|
||||
actual: "0200075f726573756c7400401000000000000005003ff0000000000000",
|
||||
expect: []any{"_result", float64(4), any(nil), float64(1)},
|
||||
},
|
||||
{
|
||||
name: "mediamtx",
|
||||
actual: "0200086f6e537461747573004014000000000000050300056c6576656c0200067374617475730004636f64650200144e657453747265616d2e506c61792e5265736574000b6465736372697074696f6e02000a706c6179207265736574000009",
|
||||
expect: []any{
|
||||
"onStatus", float64(5), any(nil), map[string]any{
|
||||
"code": "NetStream.Play.Reset",
|
||||
"description": "play reset",
|
||||
"level": "status",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "mediamtx",
|
||||
actual: "0200086f6e537461747573004014000000000000050300056c6576656c0200067374617475730004636f64650200144e657453747265616d2e506c61792e5374617274000b6465736372697074696f6e02000a706c6179207374617274000009",
|
||||
expect: []any{
|
||||
"onStatus", float64(5), any(nil), map[string]any{
|
||||
"code": "NetStream.Play.Start",
|
||||
"description": "play start",
|
||||
"level": "status",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "mediamtx",
|
||||
actual: "0200086f6e537461747573004014000000000000050300056c6576656c0200067374617475730004636f64650200144e657453747265616d2e446174612e5374617274000b6465736372697074696f6e02000a64617461207374617274000009",
|
||||
expect: []any{
|
||||
"onStatus", float64(5), any(nil), map[string]any{
|
||||
"code": "NetStream.Data.Start",
|
||||
"description": "data start",
|
||||
"level": "status",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "mediamtx",
|
||||
actual: "0200086f6e537461747573004014000000000000050300056c6576656c0200067374617475730004636f646502001c4e657453747265616d2e506c61792e5075626c6973684e6f74696679000b6465736372697074696f6e02000e7075626c697368206e6f74696679000009",
|
||||
expect: []any{
|
||||
"onStatus", float64(5), any(nil), map[string]any{
|
||||
"code": "NetStream.Play.PublishNotify",
|
||||
"description": "publish notify",
|
||||
"level": "status",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "obs-connect",
|
||||
actual: "020007636f6e6e656374003ff000000000000003000361707002000c617070312f73747265616d3100047479706502000a6e6f6e70726976617465000e737570706f727473476f4177617901010008666c61736856657202001f464d4c452f332e302028636f6d70617469626c653b20464d53632f312e3029000673776655726c02002272746d703a2f2f3139322e3136382e31302e3130312f617070312f73747265616d310005746355726c02002272746d703a2f2f3139322e3136382e31302e3130312f617070312f73747265616d31000009",
|
||||
expect: []any{
|
||||
"connect", float64(1),
|
||||
map[string]any{
|
||||
"app": "app1/stream1",
|
||||
"flashVer": "FMLE/3.0 (compatible; FMSc/1.0)",
|
||||
"supportsGoAway": true,
|
||||
"swfUrl": "rtmp://192.168.10.101/app1/stream1",
|
||||
"tcUrl": "rtmp://192.168.10.101/app1/stream1",
|
||||
"type": "nonprivate",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "obs-key",
|
||||
actual: "02000d72656c6561736553747265616d004000000000000000050200046b657931",
|
||||
expect: []any{
|
||||
"releaseStream", float64(2), nil, "key1",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "obs",
|
||||
actual: "02000d40736574446174614672616d6502000a6f6e4d65746144617461080000001400086475726174696f6e000000000000000000000866696c6553697a65000000000000000000000577696474680040840000000000000006686569676874004076800000000000000c766964656f636f646563696400401c000000000000000d766964656f64617461726174650040a388000000000000096672616d6572617465004039000000000000000c617564696f636f6465636964004024000000000000000d617564696f6461746172617465004064000000000000000f617564696f73616d706c65726174650040e5888000000000000f617564696f73616d706c6573697a65004030000000000000000d617564696f6368616e6e656c73004000000000000000000673746572656f01010003322e3101000003332e3101000003342e3001000003342e3101000003352e3101000003372e3101000007656e636f6465720200376f62732d6f7574707574206d6f64756c6520286c69626f62732076657273696f6e2032392e302e302d36322d6739303031323131663829000009",
|
||||
expect: []any{
|
||||
"@setDataFrame", "onMetaData", map[string]any{
|
||||
"2.1": false,
|
||||
"3.1": false,
|
||||
"4.0": false,
|
||||
"4.1": false,
|
||||
"5.1": false,
|
||||
"7.1": false,
|
||||
"audiochannels": float64(2),
|
||||
"audiocodecid": float64(10),
|
||||
"audiodatarate": float64(160),
|
||||
"audiosamplerate": float64(44100),
|
||||
"audiosamplesize": float64(16),
|
||||
"duration": float64(0),
|
||||
"encoder": "obs-output module (libobs version 29.0.0-62-g9001211f8)",
|
||||
"fileSize": float64(0),
|
||||
"framerate": float64(25),
|
||||
"height": float64(360),
|
||||
"stereo": true,
|
||||
"videocodecid": float64(7),
|
||||
"videodatarate": float64(2500),
|
||||
"width": float64(640),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "telegram-2",
|
||||
actual: "0200075f726573756c7400400000000000000005",
|
||||
expect: []any{
|
||||
"_result", float64(2), nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "telegram-4",
|
||||
actual: "0200075f726573756c7400401000000000000005003ff0000000000000",
|
||||
expect: []any{
|
||||
"_result", float64(4), nil, float64(1),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
b, err := hex.DecodeString(test.actual)
|
||||
require.Nil(t, err)
|
||||
|
||||
rd := NewReader(b)
|
||||
v, err := rd.ReadItems()
|
||||
require.Nil(t, err)
|
||||
|
||||
require.Equal(t, test.expect, v)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package flv
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/aac"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||
"github.com/pion/rtp"
|
||||
)
|
||||
|
||||
type Consumer struct {
|
||||
core.Connection
|
||||
wr *core.WriteBuffer
|
||||
muxer *Muxer
|
||||
}
|
||||
|
||||
func NewConsumer() *Consumer {
|
||||
medias := []*core.Media{
|
||||
{
|
||||
Kind: core.KindVideo,
|
||||
Direction: core.DirectionSendonly,
|
||||
Codecs: []*core.Codec{
|
||||
{Name: core.CodecH264},
|
||||
},
|
||||
},
|
||||
{
|
||||
Kind: core.KindAudio,
|
||||
Direction: core.DirectionSendonly,
|
||||
Codecs: []*core.Codec{
|
||||
{Name: core.CodecAAC},
|
||||
},
|
||||
},
|
||||
}
|
||||
wr := core.NewWriteBuffer(nil)
|
||||
return &Consumer{
|
||||
Connection: core.Connection{
|
||||
ID: core.NewID(),
|
||||
FormatName: "flv",
|
||||
Medias: medias,
|
||||
Transport: wr,
|
||||
},
|
||||
wr: wr,
|
||||
muxer: &Muxer{},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Consumer) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error {
|
||||
sender := core.NewSender(media, track.Codec)
|
||||
|
||||
switch track.Codec.Name {
|
||||
case core.CodecH264:
|
||||
payload := c.muxer.GetPayloader(track.Codec)
|
||||
|
||||
sender.Handler = func(pkt *rtp.Packet) {
|
||||
b := payload(pkt)
|
||||
if n, err := c.wr.Write(b); err == nil {
|
||||
c.Send += n
|
||||
}
|
||||
}
|
||||
|
||||
if track.Codec.IsRTP() {
|
||||
sender.Handler = h264.RTPDepay(track.Codec, sender.Handler)
|
||||
} else {
|
||||
sender.Handler = h264.RepairAVCC(track.Codec, sender.Handler)
|
||||
}
|
||||
|
||||
case core.CodecAAC:
|
||||
payload := c.muxer.GetPayloader(track.Codec)
|
||||
|
||||
sender.Handler = func(pkt *rtp.Packet) {
|
||||
b := payload(pkt)
|
||||
if n, err := c.wr.Write(b); err == nil {
|
||||
c.Send += n
|
||||
}
|
||||
}
|
||||
|
||||
if track.Codec.IsRTP() {
|
||||
sender.Handler = aac.RTPDepay(sender.Handler)
|
||||
}
|
||||
}
|
||||
|
||||
sender.HandleRTP(track)
|
||||
c.Senders = append(c.Senders, sender)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Consumer) WriteTo(wr io.Writer) (int64, error) {
|
||||
b := c.muxer.GetInit()
|
||||
if _, err := wr.Write(b); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return c.wr.WriteTo(wr)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package flv
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestTimeToRTP(t *testing.T) {
|
||||
// Reolink camera has 20 FPS
|
||||
// Video timestamp increases by 50ms, SampleRate 90000, RTP timestamp increases by 4500
|
||||
// Audio timestamp increases by 64ms, SampleRate 16000, RTP timestamp increases by 1024
|
||||
frameN := 1
|
||||
for i := 0; i < 32; i++ {
|
||||
// 1000ms/(90000/4500) = 50ms
|
||||
require.Equal(t, uint32(frameN*4500), TimeToRTP(uint32(frameN*50), 90000))
|
||||
// 1000ms/(16000/1024) = 64ms
|
||||
require.Equal(t, uint32(frameN*1024), TimeToRTP(uint32(frameN*64), 16000))
|
||||
frameN *= 2
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
package flv
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/flv/amf"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||
"github.com/pion/rtp"
|
||||
)
|
||||
|
||||
type Muxer struct {
|
||||
codecs []*core.Codec
|
||||
}
|
||||
|
||||
const (
|
||||
FlagsVideo = 0b001
|
||||
FlagsAudio = 0b100
|
||||
)
|
||||
|
||||
func (m *Muxer) GetInit() []byte {
|
||||
b := []byte{
|
||||
'F', 'L', 'V', // signature
|
||||
1, // version
|
||||
0, // flags (has video/audio)
|
||||
0, 0, 0, 9, // header size
|
||||
0, 0, 0, 0, // tag 0 size
|
||||
}
|
||||
|
||||
obj := map[string]any{}
|
||||
|
||||
for _, codec := range m.codecs {
|
||||
switch codec.Name {
|
||||
case core.CodecH264:
|
||||
b[4] |= FlagsVideo
|
||||
obj["videocodecid"] = CodecH264
|
||||
|
||||
case core.CodecAAC:
|
||||
b[4] |= FlagsAudio
|
||||
obj["audiocodecid"] = CodecAAC
|
||||
obj["audiosamplerate"] = codec.ClockRate
|
||||
obj["audiosamplesize"] = 16
|
||||
obj["stereo"] = codec.Channels == 2
|
||||
}
|
||||
}
|
||||
|
||||
data := amf.EncodeItems("@setDataFrame", "onMetaData", obj)
|
||||
b = append(b, EncodeTag(TagData, 0, data)...)
|
||||
|
||||
for _, codec := range m.codecs {
|
||||
switch codec.Name {
|
||||
case core.CodecH264:
|
||||
sps, pps := h264.GetParameterSet(codec.FmtpLine)
|
||||
if len(sps) == 0 {
|
||||
sps = []byte{0x67, 0x42, 0x00, 0x0a, 0xf8, 0x41, 0xa2}
|
||||
} else {
|
||||
h264.FixPixFmt(sps)
|
||||
}
|
||||
if len(pps) == 0 {
|
||||
pps = []byte{0x68, 0xce, 0x38, 0x80}
|
||||
}
|
||||
|
||||
config := h264.EncodeConfig(sps, pps)
|
||||
video := append(encodeAVData(codec, 0), config...)
|
||||
b = append(b, EncodeTag(TagVideo, 0, video)...)
|
||||
|
||||
case core.CodecAAC:
|
||||
s := core.Between(codec.FmtpLine, "config=", ";")
|
||||
config, _ := hex.DecodeString(s)
|
||||
audio := append(encodeAVData(codec, 0), config...)
|
||||
b = append(b, EncodeTag(TagAudio, 0, audio)...)
|
||||
}
|
||||
}
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
func (m *Muxer) GetPayloader(codec *core.Codec) func(packet *rtp.Packet) []byte {
|
||||
m.codecs = append(m.codecs, codec)
|
||||
|
||||
var ts0 uint32
|
||||
var k = codec.ClockRate / 1000
|
||||
|
||||
switch codec.Name {
|
||||
case core.CodecH264:
|
||||
buf := encodeAVData(codec, 1)
|
||||
|
||||
return func(packet *rtp.Packet) []byte {
|
||||
if h264.IsKeyframe(packet.Payload) {
|
||||
buf[0] = 1<<4 | 7
|
||||
} else {
|
||||
buf[0] = 2<<4 | 7
|
||||
}
|
||||
|
||||
buf = append(buf[:5], packet.Payload...) // reset buffer to previous place
|
||||
|
||||
if ts0 == 0 {
|
||||
ts0 = packet.Timestamp
|
||||
}
|
||||
|
||||
timeMS := (packet.Timestamp - ts0) / k
|
||||
return EncodeTag(TagVideo, timeMS, buf)
|
||||
}
|
||||
|
||||
case core.CodecAAC:
|
||||
buf := encodeAVData(codec, 1)
|
||||
|
||||
return func(packet *rtp.Packet) []byte {
|
||||
buf = append(buf[:2], packet.Payload...)
|
||||
|
||||
if ts0 == 0 {
|
||||
ts0 = packet.Timestamp
|
||||
}
|
||||
|
||||
timeMS := (packet.Timestamp - ts0) / k
|
||||
return EncodeTag(TagAudio, timeMS, buf)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func EncodeTag(tagType byte, timeMS uint32, payload []byte) []byte {
|
||||
payloadSize := uint32(len(payload))
|
||||
tagSize := payloadSize + 11
|
||||
|
||||
b := make([]byte, tagSize+4)
|
||||
b[0] = tagType
|
||||
b[1] = byte(payloadSize >> 16)
|
||||
b[2] = byte(payloadSize >> 8)
|
||||
b[3] = byte(payloadSize)
|
||||
b[4] = byte(timeMS >> 16)
|
||||
b[5] = byte(timeMS >> 8)
|
||||
b[6] = byte(timeMS)
|
||||
b[7] = byte(timeMS >> 24)
|
||||
copy(b[11:], payload)
|
||||
|
||||
binary.BigEndian.PutUint32(b[tagSize:], tagSize)
|
||||
return b
|
||||
}
|
||||
|
||||
func encodeAVData(codec *core.Codec, isFrame byte) []byte {
|
||||
switch codec.Name {
|
||||
case core.CodecH264:
|
||||
return []byte{
|
||||
1<<4 | 7, // keyframe + AVC
|
||||
isFrame, // 0 - config, 1 - frame
|
||||
0, 0, 0, // composition time = 0
|
||||
}
|
||||
|
||||
case core.CodecAAC:
|
||||
var b0 byte = 10 << 4 // AAC
|
||||
|
||||
switch codec.ClockRate {
|
||||
case 11025:
|
||||
b0 |= 1 << 2
|
||||
case 22050:
|
||||
b0 |= 2 << 2
|
||||
case 44100:
|
||||
b0 |= 3 << 2
|
||||
}
|
||||
|
||||
b0 |= 1 << 1 // 16 bits
|
||||
|
||||
if codec.Channels == 2 {
|
||||
b0 |= 1
|
||||
}
|
||||
|
||||
return []byte{b0, isFrame} // 0 - config, 1 - frame
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,312 @@
|
||||
package flv
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/aac"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h265"
|
||||
"github.com/pion/rtp"
|
||||
)
|
||||
|
||||
type Producer struct {
|
||||
core.Connection
|
||||
rd *core.ReadBuffer
|
||||
|
||||
video, audio *core.Receiver
|
||||
}
|
||||
|
||||
func Open(rd io.Reader) (*Producer, error) {
|
||||
prod := &Producer{
|
||||
Connection: core.Connection{
|
||||
ID: core.NewID(),
|
||||
FormatName: "flv",
|
||||
Transport: rd,
|
||||
},
|
||||
rd: core.NewReadBuffer(rd),
|
||||
}
|
||||
if err := prod.probe(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return prod, nil
|
||||
}
|
||||
|
||||
const (
|
||||
Signature = "FLV"
|
||||
|
||||
TagAudio = 8
|
||||
TagVideo = 9
|
||||
TagData = 18
|
||||
|
||||
CodecAAC = 10
|
||||
|
||||
CodecH264 = 7
|
||||
CodecHEVC = 12
|
||||
)
|
||||
|
||||
const (
|
||||
PacketTypeAVCHeader = iota
|
||||
PacketTypeAVCNALU
|
||||
PacketTypeAVCEnd
|
||||
)
|
||||
|
||||
const (
|
||||
PacketTypeSequenceStart = iota
|
||||
PacketTypeCodedFrames
|
||||
PacketTypeSequenceEnd
|
||||
PacketTypeCodedFramesX
|
||||
PacketTypeMetadata
|
||||
PacketTypeMPEG2TSSequenceStart
|
||||
)
|
||||
|
||||
func (c *Producer) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
|
||||
receiver, _ := c.Connection.GetTrack(media, codec)
|
||||
if media.Kind == core.KindVideo {
|
||||
c.video = receiver
|
||||
} else {
|
||||
c.audio = receiver
|
||||
}
|
||||
return receiver, nil
|
||||
}
|
||||
|
||||
func (c *Producer) Start() error {
|
||||
for {
|
||||
pkt, err := c.readPacket()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Recv += len(pkt.Payload)
|
||||
|
||||
switch pkt.PayloadType {
|
||||
case TagAudio:
|
||||
if c.audio == nil || pkt.Payload[1] == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
pkt.Timestamp = TimeToRTP(pkt.Timestamp, c.audio.Codec.ClockRate)
|
||||
pkt.Payload = pkt.Payload[2:]
|
||||
c.audio.WriteRTP(pkt)
|
||||
|
||||
case TagVideo:
|
||||
if c.video == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if isExHeader(pkt.Payload) {
|
||||
switch packetType := pkt.Payload[0] & 0b1111; packetType {
|
||||
case PacketTypeCodedFrames:
|
||||
// frame type 4b, packet type 4b, fourCC 32b, composition time 24b
|
||||
pkt.Payload = pkt.Payload[8:]
|
||||
case PacketTypeCodedFramesX:
|
||||
// frame type 4b, packet type 4b, fourCC 32b
|
||||
pkt.Payload = pkt.Payload[5:]
|
||||
default:
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
switch pkt.Payload[1] {
|
||||
case PacketTypeAVCNALU:
|
||||
// frame type 4b, codecID 4b, avc packet type 8b, composition time 24b
|
||||
pkt.Payload = pkt.Payload[5:]
|
||||
default:
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
pkt.Timestamp = TimeToRTP(pkt.Timestamp, c.video.Codec.ClockRate)
|
||||
c.video.WriteRTP(pkt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Producer) probe() error {
|
||||
if err := c.readHeader(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.rd.BufferSize = core.ProbeSize
|
||||
defer c.rd.Reset()
|
||||
|
||||
// Normal software sends:
|
||||
// 1. Video/audio flag in header
|
||||
// 2. MetaData as first tag (with video/audio codec info)
|
||||
// 3. Video/audio headers in 2nd and 3rd tag
|
||||
|
||||
// Reolink camera sends:
|
||||
// 1. Empty video/audio flag
|
||||
// 2. MedaData without stereo key for AAC
|
||||
// 3. Audio header after Video keyframe tag
|
||||
|
||||
// OpenIPC camera (on old firmwares) sends:
|
||||
// 1. Empty video/audio flag
|
||||
// 2. No MetaData packet
|
||||
// 3. Sends a video packet in more than 3 seconds
|
||||
waitVideo := true
|
||||
waitAudio := true
|
||||
timeout := time.Now().Add(time.Second * 5)
|
||||
|
||||
for (waitVideo || waitAudio) && time.Now().Before(timeout) {
|
||||
pkt, err := c.readPacket()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
//log.Printf("%d %0.20s", pkt.PayloadType, pkt.Payload)
|
||||
|
||||
switch pkt.PayloadType {
|
||||
case TagAudio:
|
||||
if !waitAudio {
|
||||
continue
|
||||
}
|
||||
|
||||
_ = pkt.Payload[1] // bounds
|
||||
|
||||
codecID := pkt.Payload[0] >> 4 // SoundFormat
|
||||
_ = pkt.Payload[0] & 0b1100 // SoundRate
|
||||
_ = pkt.Payload[0] & 0b0010 // SoundSize
|
||||
_ = pkt.Payload[0] & 0b0001 // SoundType
|
||||
|
||||
if codecID != CodecAAC {
|
||||
continue
|
||||
}
|
||||
|
||||
if pkt.Payload[1] != 0 { // check if header
|
||||
continue
|
||||
}
|
||||
|
||||
codec := aac.ConfigToCodec(pkt.Payload[2:])
|
||||
media := &core.Media{
|
||||
Kind: core.KindAudio,
|
||||
Direction: core.DirectionRecvonly,
|
||||
Codecs: []*core.Codec{codec},
|
||||
}
|
||||
c.Medias = append(c.Medias, media)
|
||||
waitAudio = false
|
||||
|
||||
case TagVideo:
|
||||
if !waitVideo {
|
||||
continue
|
||||
}
|
||||
|
||||
var codec *core.Codec
|
||||
|
||||
if isExHeader(pkt.Payload) {
|
||||
if string(pkt.Payload[1:5]) != "hvc1" {
|
||||
continue
|
||||
}
|
||||
|
||||
if packetType := pkt.Payload[0] & 0b1111; packetType != PacketTypeSequenceStart {
|
||||
continue
|
||||
}
|
||||
|
||||
codec = h265.ConfigToCodec(pkt.Payload[5:])
|
||||
} else {
|
||||
_ = pkt.Payload[0] >> 4 // FrameType
|
||||
|
||||
if packetType := pkt.Payload[1]; packetType != PacketTypeAVCHeader { // check if header
|
||||
continue
|
||||
}
|
||||
|
||||
switch codecID := pkt.Payload[0] & 0b1111; codecID {
|
||||
case CodecH264:
|
||||
codec = h264.ConfigToCodec(pkt.Payload[5:])
|
||||
case CodecHEVC:
|
||||
codec = h265.ConfigToCodec(pkt.Payload[5:])
|
||||
default:
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
media := &core.Media{
|
||||
Kind: core.KindVideo,
|
||||
Direction: core.DirectionRecvonly,
|
||||
Codecs: []*core.Codec{codec},
|
||||
}
|
||||
c.Medias = append(c.Medias, media)
|
||||
waitVideo = false
|
||||
|
||||
case TagData:
|
||||
if !bytes.Contains(pkt.Payload, []byte("onMetaData")) {
|
||||
continue
|
||||
}
|
||||
// Dahua cameras doesn't send videocodecid
|
||||
if !bytes.Contains(pkt.Payload, []byte("videocodecid")) &&
|
||||
!bytes.Contains(pkt.Payload, []byte("width")) &&
|
||||
!bytes.Contains(pkt.Payload, []byte("framerate")) {
|
||||
waitVideo = false
|
||||
}
|
||||
if !bytes.Contains(pkt.Payload, []byte("audiocodecid")) {
|
||||
waitAudio = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Producer) readHeader() error {
|
||||
b := make([]byte, 9)
|
||||
if _, err := io.ReadFull(c.rd, b); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if string(b[:3]) != Signature {
|
||||
return errors.New("flv: wrong header")
|
||||
}
|
||||
|
||||
_ = b[4] // flags (skip because unsupported by Reolink cameras)
|
||||
|
||||
if skip := binary.BigEndian.Uint32(b[5:]) - 9; skip > 0 {
|
||||
if _, err := io.ReadFull(c.rd, make([]byte, skip)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Producer) readPacket() (*rtp.Packet, error) {
|
||||
// https://rtmp.veriskope.com/pdf/video_file_format_spec_v10.pdf
|
||||
b := make([]byte, 4+11)
|
||||
if _, err := io.ReadFull(c.rd, b); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
b = b[4 : 4+11] // skip previous tag size
|
||||
|
||||
size := uint32(b[1])<<16 | uint32(b[2])<<8 | uint32(b[3])
|
||||
|
||||
pkt := &rtp.Packet{
|
||||
Header: rtp.Header{
|
||||
PayloadType: b[0],
|
||||
Timestamp: uint32(b[4])<<16 | uint32(b[5])<<8 | uint32(b[6]) | uint32(b[7])<<24,
|
||||
},
|
||||
Payload: make([]byte, size),
|
||||
}
|
||||
|
||||
if _, err := io.ReadFull(c.rd, pkt.Payload); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
//log.Printf("[FLV] %d %.40x", pkt.PayloadType, pkt.Payload)
|
||||
|
||||
return pkt, nil
|
||||
}
|
||||
|
||||
// TimeToRTP convert time in milliseconds to RTP time
|
||||
func TimeToRTP(timeMS, clockRate uint32) uint32 {
|
||||
// for clockRates 90000, 16000, 8000, etc. - we can use:
|
||||
// return timeMS * (clockRate / 1000)
|
||||
// but for clockRates 44100, 22050, 11025 - we should use:
|
||||
return uint32(uint64(timeMS) * uint64(clockRate) / 1000)
|
||||
}
|
||||
|
||||
func isExHeader(data []byte) bool {
|
||||
return data[0]&0b1000_0000 != 0
|
||||
}
|
||||
Reference in New Issue
Block a user