Files
go2rtc/pkg/ivideon/client.go
2024-06-16 06:20:45 +03:00

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"`
}