Remove old HTTP-FLV client
This commit is contained in:
@@ -1,3 +0,0 @@
|
|||||||
## Useful links
|
|
||||||
|
|
||||||
- https://medium.com/@nate510/don-t-use-go-s-default-http-client-4804cb19f779
|
|
||||||
@@ -1,165 +0,0 @@
|
|||||||
package httpflv
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/binary"
|
|
||||||
"errors"
|
|
||||||
"math"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
TypeNumber byte = iota
|
|
||||||
TypeBoolean
|
|
||||||
TypeString
|
|
||||||
TypeObject
|
|
||||||
TypeEcmaArray = 8
|
|
||||||
TypeObjectEnd = 9
|
|
||||||
)
|
|
||||||
|
|
||||||
var Err = errors.New("amf0 read error")
|
|
||||||
|
|
||||||
// AMF0 spec: http://download.macromedia.com/pub/labs/amf/amf0_spec_121207.pdf
|
|
||||||
type AMF0 struct {
|
|
||||||
buf []byte
|
|
||||||
pos int
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewReader(b []byte) *AMF0 {
|
|
||||||
return &AMF0{buf: b}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *AMF0) ReadMetaData() map[string]any {
|
|
||||||
if b, _ := a.ReadByte(); b != TypeString {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if s, _ := a.ReadString(); s != "onMetaData" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
b, _ := a.ReadByte()
|
|
||||||
switch b {
|
|
||||||
case TypeObject:
|
|
||||||
v, _ := a.ReadObject()
|
|
||||||
return v
|
|
||||||
case TypeEcmaArray:
|
|
||||||
v, _ := a.ReadEcmaArray()
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *AMF0) ReadMap() (map[any]any, error) {
|
|
||||||
dict := make(map[any]any)
|
|
||||||
|
|
||||||
for a.pos < len(a.buf) {
|
|
||||||
k, err := a.ReadItem()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
v, err := a.ReadItem()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
dict[k] = v
|
|
||||||
}
|
|
||||||
|
|
||||||
return dict, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *AMF0) ReadItem() (any, error) {
|
|
||||||
dataType, err := a.ReadByte()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
switch dataType {
|
|
||||||
case TypeNumber:
|
|
||||||
return a.ReadNumber()
|
|
||||||
|
|
||||||
case TypeBoolean:
|
|
||||||
v, err := a.ReadByte()
|
|
||||||
return v != 0, err
|
|
||||||
|
|
||||||
case TypeString:
|
|
||||||
return a.ReadString()
|
|
||||||
|
|
||||||
case TypeObject:
|
|
||||||
return a.ReadObject()
|
|
||||||
|
|
||||||
case TypeObjectEnd:
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, Err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *AMF0) ReadByte() (byte, error) {
|
|
||||||
if a.pos >= len(a.buf) {
|
|
||||||
return 0, Err
|
|
||||||
}
|
|
||||||
|
|
||||||
v := a.buf[a.pos]
|
|
||||||
a.pos++
|
|
||||||
return v, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *AMF0) ReadNumber() (float64, error) {
|
|
||||||
if a.pos+8 >= len(a.buf) {
|
|
||||||
return 0, Err
|
|
||||||
}
|
|
||||||
|
|
||||||
v := binary.BigEndian.Uint64(a.buf[a.pos : a.pos+8])
|
|
||||||
a.pos += 8
|
|
||||||
return math.Float64frombits(v), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *AMF0) ReadString() (string, error) {
|
|
||||||
if a.pos+2 >= len(a.buf) {
|
|
||||||
return "", Err
|
|
||||||
}
|
|
||||||
|
|
||||||
size := int(binary.BigEndian.Uint16(a.buf[a.pos:]))
|
|
||||||
a.pos += 2
|
|
||||||
|
|
||||||
if a.pos+size >= len(a.buf) {
|
|
||||||
return "", Err
|
|
||||||
}
|
|
||||||
|
|
||||||
s := string(a.buf[a.pos : a.pos+size])
|
|
||||||
a.pos += size
|
|
||||||
|
|
||||||
return s, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *AMF0) 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 *AMF0) ReadEcmaArray() (map[string]any, error) {
|
|
||||||
if a.pos+4 >= len(a.buf) {
|
|
||||||
return nil, Err
|
|
||||||
}
|
|
||||||
a.pos += 4 // skip size
|
|
||||||
|
|
||||||
return a.ReadObject()
|
|
||||||
}
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
package httpflv
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/deepch/vdk/format/flv/flvio"
|
|
||||||
"github.com/deepch/vdk/utils/bits/pio"
|
|
||||||
"io"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TODO: rewrite all of this someday
|
|
||||||
|
|
||||||
func ReadTag(r io.Reader, b []byte) (tag flvio.Tag, ts int32, err error) {
|
|
||||||
if _, err = io.ReadFull(r, b[:flvio.TagHeaderLength]); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var datalen int
|
|
||||||
if tag, ts, datalen, err = flvio.ParseTagHeader(b); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
data := make([]byte, datalen)
|
|
||||||
if _, err = io.ReadFull(r, data); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
n, err := ParseHeader(&tag, data)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
tag.Data = data[n:]
|
|
||||||
|
|
||||||
if _, err = io.ReadFull(r, b[:4]); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func ParseHeader(self *flvio.Tag, b []byte) (n int, err error) {
|
|
||||||
switch self.Type {
|
|
||||||
case flvio.TAG_AUDIO:
|
|
||||||
return audioParseHeader(self, b)
|
|
||||||
|
|
||||||
case flvio.TAG_VIDEO:
|
|
||||||
return videoParseHeader(self, b)
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func audioParseHeader(tag *flvio.Tag, b []byte) (n int, err error) {
|
|
||||||
if len(b) < n+1 {
|
|
||||||
err = fmt.Errorf("audiodata: parse invalid")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
flags := b[n]
|
|
||||||
n++
|
|
||||||
tag.SoundFormat = flags >> 4
|
|
||||||
tag.SoundRate = (flags >> 2) & 0x3
|
|
||||||
tag.SoundSize = (flags >> 1) & 0x1
|
|
||||||
tag.SoundType = flags & 0x1
|
|
||||||
|
|
||||||
switch tag.SoundFormat {
|
|
||||||
case flvio.SOUND_AAC:
|
|
||||||
if len(b) < n+1 {
|
|
||||||
err = fmt.Errorf("audiodata: parse invalid")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
tag.AACPacketType = b[n]
|
|
||||||
n++
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func videoParseHeader(tag *flvio.Tag, b []byte) (n int, err error) {
|
|
||||||
if len(b) < n+1 {
|
|
||||||
err = fmt.Errorf("videodata: parse invalid")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
flags := b[n]
|
|
||||||
tag.FrameType = flags >> 4
|
|
||||||
tag.CodecID = flags & 0xf
|
|
||||||
n++
|
|
||||||
|
|
||||||
if len(b) < n+4 {
|
|
||||||
err = fmt.Errorf("videodata: parse invalid")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
tag.AVCPacketType = b[n]
|
|
||||||
n++
|
|
||||||
|
|
||||||
tag.CompositionTime = pio.I24BE(b[n:])
|
|
||||||
n += 3
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
@@ -1,214 +0,0 @@
|
|||||||
package httpflv
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"bytes"
|
|
||||||
"github.com/deepch/vdk/av"
|
|
||||||
"github.com/deepch/vdk/codec/aacparser"
|
|
||||||
"github.com/deepch/vdk/codec/h264parser"
|
|
||||||
"github.com/deepch/vdk/format/flv/flvio"
|
|
||||||
"github.com/deepch/vdk/utils/bits/pio"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Dial(uri string) (*Conn, error) {
|
|
||||||
req, err := http.NewRequest("GET", uri, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err := http.DefaultClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return Accept(res)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Accept(res *http.Response) (*Conn, error) {
|
|
||||||
c := Conn{
|
|
||||||
conn: res.Body,
|
|
||||||
reader: bufio.NewReaderSize(res.Body, pio.RecommendBufioSize),
|
|
||||||
buf: make([]byte, 256),
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := io.ReadFull(c.reader, c.buf[:flvio.FileHeaderLength]); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
flags, n, err := flvio.ParseFileHeader(c.buf)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if flags&flvio.FILE_HAS_VIDEO != 0 {
|
|
||||||
c.videoIdx = -1
|
|
||||||
}
|
|
||||||
|
|
||||||
if flags&flvio.FILE_HAS_AUDIO != 0 {
|
|
||||||
c.audioIdx = -1
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err = c.reader.Discard(n); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &c, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type Conn struct {
|
|
||||||
conn io.ReadCloser
|
|
||||||
reader *bufio.Reader
|
|
||||||
buf []byte
|
|
||||||
|
|
||||||
videoIdx int8
|
|
||||||
audioIdx int8
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Conn) Streams() ([]av.CodecData, error) {
|
|
||||||
var video, audio av.CodecData
|
|
||||||
|
|
||||||
// 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
|
|
||||||
|
|
||||||
waitVideo := c.videoIdx != 0
|
|
||||||
waitAudio := c.audioIdx != 0
|
|
||||||
|
|
||||||
for i := 0; i < 20; i++ {
|
|
||||||
tag, _, err := flvio.ReadTag(c.reader, c.buf)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
//log.Printf("[FLV] type=%d avc=%d aac=%d video=%t audio=%t", tag.Type, tag.AVCPacketType, tag.AACPacketType, video != nil, audio != nil)
|
|
||||||
|
|
||||||
switch tag.Type {
|
|
||||||
case flvio.TAG_SCRIPTDATA:
|
|
||||||
if meta := NewReader(tag.Data).ReadMetaData(); meta != nil {
|
|
||||||
waitVideo = meta["videocodecid"] != nil
|
|
||||||
|
|
||||||
// don't wait audio tag because parse all info from MetaData
|
|
||||||
waitAudio = false
|
|
||||||
|
|
||||||
audio = parseAudioConfig(meta)
|
|
||||||
} else {
|
|
||||||
waitVideo = bytes.Contains(tag.Data, []byte("videocodecid"))
|
|
||||||
waitAudio = bytes.Contains(tag.Data, []byte("audiocodecid"))
|
|
||||||
}
|
|
||||||
|
|
||||||
case flvio.TAG_VIDEO:
|
|
||||||
if tag.AVCPacketType == flvio.AVC_SEQHDR {
|
|
||||||
video, _ = h264parser.NewCodecDataFromAVCDecoderConfRecord(tag.Data)
|
|
||||||
}
|
|
||||||
waitVideo = false
|
|
||||||
|
|
||||||
case flvio.TAG_AUDIO:
|
|
||||||
if tag.SoundFormat == flvio.SOUND_AAC && tag.AACPacketType == flvio.AAC_SEQHDR {
|
|
||||||
audio, _ = aacparser.NewCodecDataFromMPEG4AudioConfigBytes(tag.Data)
|
|
||||||
}
|
|
||||||
waitAudio = false
|
|
||||||
}
|
|
||||||
|
|
||||||
if !waitVideo && !waitAudio {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if video != nil && audio != nil {
|
|
||||||
c.videoIdx = 0
|
|
||||||
c.audioIdx = 1
|
|
||||||
return []av.CodecData{video, audio}, nil
|
|
||||||
} else if video != nil {
|
|
||||||
c.videoIdx = 0
|
|
||||||
c.audioIdx = -1
|
|
||||||
return []av.CodecData{video}, nil
|
|
||||||
} else if audio != nil {
|
|
||||||
c.videoIdx = -1
|
|
||||||
c.audioIdx = 0
|
|
||||||
return []av.CodecData{audio}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Conn) ReadPacket() (av.Packet, error) {
|
|
||||||
for {
|
|
||||||
tag, ts, err := ReadTag(c.reader, c.buf)
|
|
||||||
if err != nil {
|
|
||||||
return av.Packet{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
switch tag.Type {
|
|
||||||
case flvio.TAG_VIDEO:
|
|
||||||
if c.videoIdx < 0 || tag.AVCPacketType != flvio.AVC_NALU {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
//log.Printf("[FLV] %v, len: %d, ts: %10d", h264.Types(tag.Data), len(tag.Data), flvio.TsToTime(ts))
|
|
||||||
|
|
||||||
return av.Packet{
|
|
||||||
Idx: c.videoIdx,
|
|
||||||
Data: tag.Data,
|
|
||||||
CompositionTime: flvio.TsToTime(tag.CompositionTime),
|
|
||||||
IsKeyFrame: tag.FrameType == flvio.FRAME_KEY,
|
|
||||||
Time: flvio.TsToTime(ts),
|
|
||||||
}, nil
|
|
||||||
|
|
||||||
case flvio.TAG_AUDIO:
|
|
||||||
if c.audioIdx < 0 || tag.SoundFormat != flvio.SOUND_AAC || tag.AACPacketType != flvio.AAC_RAW {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
return av.Packet{Idx: c.audioIdx, Data: tag.Data, Time: flvio.TsToTime(ts)}, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Conn) Close() (err error) {
|
|
||||||
return c.conn.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseAudioConfig(meta map[string]any) av.CodecData {
|
|
||||||
if meta["audiocodecid"] != float64(10) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
config := aacparser.MPEG4AudioConfig{
|
|
||||||
ObjectType: aacparser.AOT_AAC_LC,
|
|
||||||
}
|
|
||||||
|
|
||||||
switch v := meta["audiosamplerate"].(type) {
|
|
||||||
case float64:
|
|
||||||
config.SampleRate = int(v)
|
|
||||||
default:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
switch meta["stereo"] {
|
|
||||||
case true:
|
|
||||||
config.ChannelConfig = 2
|
|
||||||
config.ChannelLayout = av.CH_STEREO
|
|
||||||
default:
|
|
||||||
// Reolink doesn't have this setting
|
|
||||||
config.ChannelConfig = 1
|
|
||||||
config.ChannelLayout = av.CH_MONO
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := &bytes.Buffer{}
|
|
||||||
if err := aacparser.WriteMPEG4AudioConfig(buf, config); err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return aacparser.CodecData{
|
|
||||||
Config: config,
|
|
||||||
ConfigBytes: buf.Bytes(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user