mirror of
https://github.com/AlexxIT/go2rtc.git
synced 2025-09-26 20:31:11 +08:00
315 lines
5.6 KiB
Go
315 lines
5.6 KiB
Go
package ivideon
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
|
"github.com/AlexxIT/go2rtc/pkg/h264"
|
|
"github.com/AlexxIT/go2rtc/pkg/iso"
|
|
"github.com/gorilla/websocket"
|
|
"github.com/pion/rtp"
|
|
)
|
|
|
|
type State byte
|
|
|
|
const (
|
|
StateNone State = iota
|
|
StateConn
|
|
StateHandle
|
|
)
|
|
|
|
// Deprecated: should be rewritten to core.Connection
|
|
type Client struct {
|
|
core.Listener
|
|
|
|
ID string
|
|
|
|
conn *websocket.Conn
|
|
|
|
medias []*core.Media
|
|
receiver *core.Receiver
|
|
|
|
msg *message
|
|
t0 time.Time
|
|
|
|
buffer chan []byte
|
|
state State
|
|
mu sync.Mutex
|
|
|
|
recv int
|
|
}
|
|
|
|
func Dial(source string) (*Client, error) {
|
|
id := strings.Replace(source[8:], "/", ":", 1)
|
|
client := &Client{ID: id}
|
|
if err := client.Dial(); err != nil {
|
|
return nil, err
|
|
}
|
|
return client, nil
|
|
}
|
|
|
|
func (c *Client) Dial() (err error) {
|
|
resp, err := http.Get(
|
|
"https://openapi-alpha.ivideon.com/cameras/" + c.ID +
|
|
"/live_stream?op=GET&access_token=public&q=2&" +
|
|
"video_codecs=h264&format=ws-fmp4",
|
|
)
|
|
|
|
data, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var v liveResponse
|
|
if err = json.Unmarshal(data, &v); err != nil {
|
|
return err
|
|
}
|
|
|
|
if !v.Success {
|
|
return fmt.Errorf("wrong response: %s", data)
|
|
}
|
|
|
|
c.conn, _, err = websocket.DefaultDialer.Dial(v.Result.URL, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err = c.getTracks(); err != nil {
|
|
_ = c.conn.Close()
|
|
return err
|
|
}
|
|
|
|
c.state = StateConn
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *Client) Handle() error {
|
|
// add delay to the stream for smooth playing (not a best solution)
|
|
c.t0 = time.Now().Add(time.Second)
|
|
|
|
c.mu.Lock()
|
|
|
|
if c.state == StateConn {
|
|
c.buffer = make(chan []byte, 5)
|
|
c.state = StateHandle
|
|
|
|
// processing stream in separate thread for lower delay between packets
|
|
go c.worker(c.buffer)
|
|
}
|
|
|
|
c.mu.Unlock()
|
|
|
|
_, data, err := c.conn.ReadMessage()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if c.receiver != nil && c.receiver.ID == c.msg.Track {
|
|
c.mu.Lock()
|
|
if c.state == StateHandle {
|
|
c.buffer <- data
|
|
c.recv += len(data)
|
|
}
|
|
c.mu.Unlock()
|
|
}
|
|
|
|
// we have one unprocessed msg after getTracks
|
|
for {
|
|
_, data, err = c.conn.ReadMessage()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var msg message
|
|
if err = json.Unmarshal(data, &msg); err != nil {
|
|
return err
|
|
}
|
|
|
|
switch msg.Type {
|
|
case "stream-init":
|
|
continue
|
|
|
|
case "metadata":
|
|
continue
|
|
|
|
case "fragment":
|
|
_, data, err = c.conn.ReadMessage()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if c.receiver != nil && c.receiver.ID == msg.Track {
|
|
c.mu.Lock()
|
|
if c.state == StateHandle {
|
|
c.buffer <- data
|
|
c.recv += len(data)
|
|
}
|
|
c.mu.Unlock()
|
|
}
|
|
|
|
default:
|
|
return fmt.Errorf("wrong message type: %s", data)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *Client) Close() error {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
|
|
switch c.state {
|
|
case StateNone:
|
|
return nil
|
|
case StateConn:
|
|
case StateHandle:
|
|
close(c.buffer)
|
|
}
|
|
|
|
c.state = StateNone
|
|
|
|
return c.conn.Close()
|
|
}
|
|
|
|
func (c *Client) getTracks() error {
|
|
for {
|
|
_, data, err := c.conn.ReadMessage()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var msg message
|
|
if err = json.Unmarshal(data, &msg); err != nil {
|
|
return err
|
|
}
|
|
|
|
switch msg.Type {
|
|
case "metadata":
|
|
continue
|
|
|
|
case "stream-init":
|
|
s := msg.CodecString
|
|
i := strings.IndexByte(s, '.')
|
|
if i > 0 {
|
|
s = s[:i]
|
|
}
|
|
|
|
switch s {
|
|
case "avc1": // avc1.4d0029
|
|
// skip multiple identical init
|
|
if c.receiver != nil {
|
|
continue
|
|
}
|
|
|
|
i = bytes.Index(msg.Data, []byte("avcC")) - 4
|
|
if i < 0 {
|
|
return fmt.Errorf("ivideon: wrong AVC: %s", msg.Data)
|
|
}
|
|
|
|
avccLen := binary.BigEndian.Uint32(msg.Data[i:])
|
|
data = msg.Data[i+8 : i+int(avccLen)]
|
|
|
|
codec := h264.ConfigToCodec(data)
|
|
|
|
media := &core.Media{
|
|
Kind: core.KindVideo,
|
|
Direction: core.DirectionRecvonly,
|
|
Codecs: []*core.Codec{codec},
|
|
}
|
|
c.medias = append(c.medias, media)
|
|
|
|
c.receiver = core.NewReceiver(media, codec)
|
|
c.receiver.ID = msg.TrackID
|
|
|
|
case "mp4a": // mp4a.40.2
|
|
}
|
|
|
|
case "fragment":
|
|
c.msg = &msg
|
|
return nil
|
|
|
|
default:
|
|
return fmt.Errorf("wrong message type: %s", data)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *Client) worker(buffer chan []byte) {
|
|
for data := range buffer {
|
|
atoms, err := iso.DecodeAtoms(data)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
var trun *iso.Atom
|
|
var ts uint32
|
|
|
|
for _, atom := range atoms {
|
|
switch atom.Name {
|
|
case iso.MoofTrafTrun:
|
|
trun = atom
|
|
case iso.MoofTrafTfdt:
|
|
ts = uint32(atom.DecodeTime)
|
|
case iso.Mdat:
|
|
data = atom.Data
|
|
}
|
|
}
|
|
|
|
if trun == nil || trun.SamplesDuration == nil || trun.SamplesSize == nil {
|
|
continue
|
|
}
|
|
|
|
for i := 0; i < len(trun.SamplesDuration); i++ {
|
|
duration := trun.SamplesDuration[i]
|
|
size := trun.SamplesSize[i]
|
|
|
|
// synchronize framerate for WebRTC and MSE
|
|
d := time.Duration(ts)*time.Millisecond - time.Since(c.t0)
|
|
if d < 0 {
|
|
d = time.Duration(duration) * time.Millisecond / 2
|
|
}
|
|
time.Sleep(d)
|
|
|
|
// can be SPS, PPS and IFrame in one packet
|
|
packet := &rtp.Packet{
|
|
// ivideon clockrate=1000, RTP clockrate=90000
|
|
Header: rtp.Header{Timestamp: ts * 90},
|
|
Payload: data[:size],
|
|
}
|
|
c.receiver.WriteRTP(packet)
|
|
|
|
data = data[size:]
|
|
ts += duration
|
|
}
|
|
}
|
|
}
|
|
|
|
type liveResponse struct {
|
|
Result struct {
|
|
URL string `json:"url"`
|
|
} `json:"result"`
|
|
Success bool `json:"success"`
|
|
}
|
|
|
|
type message struct {
|
|
Type string `json:"type"`
|
|
|
|
CodecString string `json:"codec_string"`
|
|
Data []byte `json:"data"`
|
|
TrackID byte `json:"track_id"`
|
|
|
|
Track byte `json:"track"`
|
|
StartTime float32 `json:"start_time"`
|
|
Duration float32 `json:"duration"`
|
|
IsKey bool `json:"is_key"`
|
|
DataOffset uint32 `json:"data_offset"`
|
|
}
|