Rewrite FLV/RTMP clients
This commit is contained in:
+117
-55
@@ -2,6 +2,8 @@ package flv
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
@@ -12,10 +14,10 @@ import (
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
Transport
|
||||
|
||||
URL string
|
||||
|
||||
rd *core.ReadSeeker
|
||||
|
||||
medias []*core.Media
|
||||
receivers []*core.Receiver
|
||||
|
||||
@@ -24,15 +26,33 @@ type Client struct {
|
||||
recv int
|
||||
}
|
||||
|
||||
func NewClient(rd io.Reader) (*Client, error) {
|
||||
tr, err := NewTransport(rd)
|
||||
if err != nil {
|
||||
func Open(rd io.Reader) (*Client, error) {
|
||||
client := &Client{
|
||||
rd: core.NewReadSeeker(rd),
|
||||
}
|
||||
if err := client.describe(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Client{Transport: tr}, nil
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (c *Client) Describe() error {
|
||||
const (
|
||||
TagAudio = 8
|
||||
TagVideo = 9
|
||||
TagData = 18
|
||||
|
||||
CodecAAC = 10
|
||||
CodecAVC = 7
|
||||
)
|
||||
|
||||
func (c *Client) describe() error {
|
||||
if err := c.readHeader(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.rd.BufferSize = core.ProbeSize
|
||||
defer c.rd.Rewind()
|
||||
|
||||
// Normal software sends:
|
||||
// 1. Video/audio flag in header
|
||||
// 2. MetaData as first tag (with video/audio codec info)
|
||||
@@ -42,40 +62,39 @@ func (c *Client) Describe() error {
|
||||
// 1. Empty video/audio flag
|
||||
// 2. MedaData without stereo key for AAC
|
||||
// 3. Audio header after Video keyframe tag
|
||||
waitVideo := true
|
||||
waitAudio := true
|
||||
waitType := []byte{TagData}
|
||||
timeout := time.Now().Add(core.ProbeTimeout)
|
||||
|
||||
for (waitVideo || waitAudio) && time.Now().Before(timeout) {
|
||||
tagType, _, b, err := c.Transport.ReadTag()
|
||||
for len(waitType) != 0 && time.Now().Before(timeout) {
|
||||
pkt, err := c.readPacket()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.recv += len(b)
|
||||
if i := bytes.IndexByte(waitType, pkt.PayloadType); i < 0 {
|
||||
continue
|
||||
} else {
|
||||
waitType = append(waitType[:i], waitType[i+1:]...)
|
||||
}
|
||||
|
||||
switch tagType {
|
||||
switch pkt.PayloadType {
|
||||
case TagAudio:
|
||||
if !waitAudio {
|
||||
continue
|
||||
}
|
||||
_ = pkt.Payload[1] // bounds
|
||||
|
||||
waitAudio = false
|
||||
|
||||
codecID := b[0] >> 4 // SoundFormat
|
||||
_ = b[0] & 0b1100 // SoundRate
|
||||
_ = b[0] & 0b0010 // SoundSize
|
||||
_ = b[0] & 0b0001 // SoundType
|
||||
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 b[1] != 0 { // check if header
|
||||
if pkt.Payload[1] != 0 { // check if header
|
||||
continue
|
||||
}
|
||||
|
||||
codec := aac.ConfigToCodec(b[2:])
|
||||
codec := aac.ConfigToCodec(pkt.Payload[2:])
|
||||
media := &core.Media{
|
||||
Kind: core.KindAudio,
|
||||
Direction: core.DirectionRecvonly,
|
||||
@@ -84,24 +103,20 @@ func (c *Client) Describe() error {
|
||||
c.medias = append(c.medias, media)
|
||||
|
||||
case TagVideo:
|
||||
if !waitVideo {
|
||||
continue
|
||||
}
|
||||
_ = pkt.Payload[1] // bounds
|
||||
|
||||
waitVideo = false
|
||||
|
||||
_ = b[0] >> 4 // FrameType
|
||||
codecID := b[0] & 0b1111 // CodecID
|
||||
_ = pkt.Payload[0] >> 4 // FrameType
|
||||
codecID := pkt.Payload[0] & 0b1111 // CodecID
|
||||
|
||||
if codecID != CodecAVC {
|
||||
continue
|
||||
}
|
||||
|
||||
if b[1] != 0 { // check if header
|
||||
if pkt.Payload[1] != 0 { // check if header
|
||||
continue
|
||||
}
|
||||
|
||||
codec := h264.ConfigToCodec(b[5:])
|
||||
codec := h264.ConfigToCodec(pkt.Payload[5:])
|
||||
media := &core.Media{
|
||||
Kind: core.KindVideo,
|
||||
Direction: core.DirectionRecvonly,
|
||||
@@ -110,53 +125,100 @@ func (c *Client) Describe() error {
|
||||
c.medias = append(c.medias, media)
|
||||
|
||||
case TagData:
|
||||
if !bytes.Contains(b, []byte("onMetaData")) {
|
||||
continue
|
||||
if !bytes.Contains(pkt.Payload, []byte("onMetaData")) {
|
||||
waitType = append(waitType, TagData)
|
||||
}
|
||||
if bytes.Contains(pkt.Payload, []byte("videocodecid")) {
|
||||
waitType = append(waitType, TagVideo)
|
||||
}
|
||||
if bytes.Contains(pkt.Payload, []byte("audiocodecid")) {
|
||||
waitType = append(waitType, TagAudio)
|
||||
}
|
||||
waitVideo = bytes.Contains(b, []byte("videocodecid"))
|
||||
waitAudio = bytes.Contains(b, []byte("audiocodecid"))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) Play() error {
|
||||
func (c *Client) play() error {
|
||||
for {
|
||||
tagType, timeMS, b, err := c.Transport.ReadTag()
|
||||
pkt, err := c.readPacket()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.recv += len(b)
|
||||
c.recv += len(pkt.Payload)
|
||||
|
||||
switch tagType {
|
||||
switch pkt.PayloadType {
|
||||
case TagAudio:
|
||||
if c.audio == nil || b[1] == 0 {
|
||||
if c.audio == nil || pkt.Payload[1] == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
pkt := &rtp.Packet{
|
||||
Header: rtp.Header{
|
||||
Timestamp: TimeToRTP(timeMS, c.audio.Codec.ClockRate),
|
||||
},
|
||||
Payload: b[2:],
|
||||
}
|
||||
pkt.Timestamp = TimeToRTP(pkt.Timestamp, c.audio.Codec.ClockRate)
|
||||
pkt.Payload = pkt.Payload[2:]
|
||||
c.audio.WriteRTP(pkt)
|
||||
|
||||
case TagVideo:
|
||||
// frame type 4b, codecID 4b, avc packet type 8b, composition time 24b
|
||||
if c.video == nil || b[1] == 0 {
|
||||
if c.video == nil || pkt.Payload[1] == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
pkt := &rtp.Packet{
|
||||
Header: rtp.Header{
|
||||
Timestamp: TimeToRTP(timeMS, c.video.Codec.ClockRate),
|
||||
},
|
||||
Payload: b[5:],
|
||||
}
|
||||
pkt.Timestamp = TimeToRTP(pkt.Timestamp, c.video.Codec.ClockRate)
|
||||
pkt.Payload = pkt.Payload[5:]
|
||||
c.video.WriteRTP(pkt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) readHeader() error {
|
||||
b := make([]byte, 9)
|
||||
if _, err := io.ReadFull(c.rd, b); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if string(b[:3]) != "FLV" {
|
||||
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 *Client) 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
|
||||
}
|
||||
|
||||
return pkt, nil
|
||||
}
|
||||
|
||||
func TimeToRTP(timeMS uint32, clockRate uint32) uint32 {
|
||||
return timeMS * clockRate / 1000
|
||||
}
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
package flv
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"io"
|
||||
)
|
||||
|
||||
const (
|
||||
TagAudio = 8
|
||||
TagVideo = 9
|
||||
TagData = 18
|
||||
|
||||
CodecAAC = 10
|
||||
CodecAVC = 7
|
||||
)
|
||||
|
||||
// Transport - it is recommended to implement io.Closer
|
||||
type Transport interface {
|
||||
ReadTag() (byte, uint32, []byte, error)
|
||||
}
|
||||
|
||||
// NewTransport - it is recommended to use bufio.Reader
|
||||
func NewTransport(rd io.Reader) (Transport, error) {
|
||||
c := &flv{rd: rd}
|
||||
if err := c.readHeader(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
type flv struct {
|
||||
rd io.Reader
|
||||
}
|
||||
|
||||
func (c *flv) ReadTag() (byte, uint32, []byte, 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 0, 0, nil, err
|
||||
}
|
||||
|
||||
b = b[4 : 4+11] // skip previous tag size
|
||||
|
||||
tagType := b[0]
|
||||
size := uint32(b[1])<<16 | uint32(b[2])<<8 | uint32(b[3])
|
||||
timeMS := uint32(b[4])<<16 | uint32(b[5])<<8 | uint32(b[6]) | uint32(b[7])<<24
|
||||
|
||||
b = make([]byte, size)
|
||||
if _, err := io.ReadFull(c.rd, b); err != nil {
|
||||
return 0, 0, nil, err
|
||||
}
|
||||
|
||||
return tagType, timeMS, b, nil
|
||||
}
|
||||
|
||||
func (c *flv) Close() error {
|
||||
if closer, ok := c.rd.(io.Closer); ok {
|
||||
return closer.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *flv) readHeader() error {
|
||||
b := make([]byte, 9)
|
||||
if _, err := io.ReadFull(c.rd, b); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if string(b[:3]) != "FLV" {
|
||||
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 TimeToRTP(timeMS uint32, clockRate uint32) uint32 {
|
||||
return timeMS * clockRate / 1000
|
||||
}
|
||||
+2
-2
@@ -28,11 +28,11 @@ func (c *Client) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver,
|
||||
}
|
||||
|
||||
func (c *Client) Start() error {
|
||||
return c.Play()
|
||||
return c.play()
|
||||
}
|
||||
|
||||
func (c *Client) Stop() error {
|
||||
if closer, ok := c.Transport.(io.Closer); ok {
|
||||
if closer, ok := c.rd.Reader.(io.Closer); ok {
|
||||
return closer.Close()
|
||||
}
|
||||
return nil
|
||||
|
||||
Reference in New Issue
Block a user