diff --git a/cmd/isapi/init.go b/cmd/isapi/init.go new file mode 100644 index 0000000..895e05d --- /dev/null +++ b/cmd/isapi/init.go @@ -0,0 +1,22 @@ +package isapi + +import ( + "github.com/AlexxIT/go2rtc/cmd/streams" + "github.com/AlexxIT/go2rtc/pkg/isapi" + "github.com/AlexxIT/go2rtc/pkg/streamer" +) + +func Init() { + streams.HandleFunc("isapi", handle) +} + +func handle(url string) (streamer.Producer, error) { + conn, err := isapi.NewClient(url) + if err != nil { + return nil, err + } + if err = conn.Dial(); err != nil { + return nil, err + } + return conn, nil +} diff --git a/main.go b/main.go index 4c4335c..fb62c3b 100644 --- a/main.go +++ b/main.go @@ -12,6 +12,7 @@ import ( "github.com/AlexxIT/go2rtc/cmd/hls" "github.com/AlexxIT/go2rtc/cmd/homekit" "github.com/AlexxIT/go2rtc/cmd/http" + "github.com/AlexxIT/go2rtc/cmd/isapi" "github.com/AlexxIT/go2rtc/cmd/ivideon" "github.com/AlexxIT/go2rtc/cmd/mjpeg" "github.com/AlexxIT/go2rtc/cmd/mp4" @@ -43,6 +44,7 @@ func main() { http.Init() dvrip.Init() tapo.Init() + isapi.Init() mpegts.Init() srtp.Init() diff --git a/pkg/isapi/client.go b/pkg/isapi/client.go new file mode 100644 index 0000000..04ca22c --- /dev/null +++ b/pkg/isapi/client.go @@ -0,0 +1,152 @@ +package isapi + +import ( + "errors" + "github.com/AlexxIT/go2rtc/pkg/streamer" + "github.com/AlexxIT/go2rtc/pkg/tcp" + "io" + "net" + "net/http" + "net/url" +) + +type Client struct { + streamer.Element + + url string + + medias []*streamer.Media + tracks []*streamer.Track + + channel string + conn net.Conn + send int +} + +func NewClient(rawURL string) (*Client, error) { + // check if url is valid url + u, err := url.Parse(rawURL) + if err != nil { + return nil, err + } + + u.Scheme = "http" + u.Path = "" + + return &Client{url: u.String()}, nil +} + +func (c *Client) Dial() (err error) { + link := c.url + "/ISAPI/System/TwoWayAudio/channels" + req, err := http.NewRequest("GET", link, nil) + if err != nil { + return err + } + + res, err := tcp.Do(req) + if err != nil { + return + } + + if res.StatusCode != http.StatusOK { + tcp.Close(res) + return errors.New(res.Status) + } + + b, err := io.ReadAll(res.Body) + if err != nil { + return err + } + + xml := string(b) + + codec := streamer.Between(xml, ``, `<`) + switch codec { + case "G.711ulaw": + codec = streamer.CodecPCMU + case "G.711alaw": + codec = streamer.CodecPCMA + default: + return nil + } + + c.channel = streamer.Between(xml, ``, `<`) + + media := &streamer.Media{ + Kind: streamer.KindAudio, + Direction: streamer.DirectionRecvonly, + Codecs: []*streamer.Codec{ + {Name: codec, ClockRate: 8000}, + }, + } + c.medias = append(c.medias, media) + + return nil +} + +func (c *Client) Open() (err error) { + link := c.url + "/ISAPI/System/TwoWayAudio/channels/" + c.channel + + req, err := http.NewRequest("PUT", link+"/open", nil) + if err != nil { + return err + } + + res, err := tcp.Do(req) + if err != nil { + return + } + + tcp.Close(res) + + ctx, pconn := tcp.WithConn() + req, err = http.NewRequestWithContext(ctx, "PUT", link+"/audioData", nil) + if err != nil { + return err + } + + req.Header.Set("Content-Type", "application/octet-stream") + req.Header.Set("Content-Length", "0") + + res, err = tcp.Do(req) + if err != nil { + return err + } + + c.conn = *pconn + + // just block until c.conn closed + b := make([]byte, 1) + _, _ = c.conn.Read(b) + + tcp.Close(res) + + return nil +} + +func (c *Client) Close() (err error) { + link := c.url + "/ISAPI/System/TwoWayAudio/channels/" + c.channel + "/close" + req, err := http.NewRequest("PUT", link+"/open", nil) + if err != nil { + return err + } + + res, err := tcp.Do(req) + if err != nil { + return err + } + + tcp.Close(res) + + return nil +} + +//type XMLChannels struct { +// Channels []Channel `xml:"TwoWayAudioChannel"` +//} + +//type Channel struct { +// ID string `xml:"id"` +// Enabled string `xml:"enabled"` +// Codec string `xml:"audioCompressionType"` +//} diff --git a/pkg/isapi/consumer.go b/pkg/isapi/consumer.go new file mode 100644 index 0000000..4784e98 --- /dev/null +++ b/pkg/isapi/consumer.go @@ -0,0 +1,18 @@ +package isapi + +import ( + "github.com/AlexxIT/go2rtc/pkg/streamer" + "github.com/pion/rtp" +) + +func (c *Client) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track { + consCodec := media.MatchCodec(track.Codec) + consTrack := c.GetTrack(media, consCodec) + if consTrack == nil { + return nil + } + + return track.Bind(func(packet *rtp.Packet) error { + return consTrack.WriteRTP(packet) + }) +} diff --git a/pkg/isapi/producer.go b/pkg/isapi/producer.go new file mode 100644 index 0000000..e9cd371 --- /dev/null +++ b/pkg/isapi/producer.go @@ -0,0 +1,56 @@ +package isapi + +import ( + "encoding/json" + "github.com/AlexxIT/go2rtc/pkg/streamer" + "github.com/pion/rtp" +) + +func (c *Client) GetMedias() []*streamer.Media { + return c.medias +} + +func (c *Client) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.Track { + for _, track := range c.tracks { + if track.Codec == codec { + return track + } + } + + track := streamer.NewTrack(codec, media.Direction) + track = track.Bind(func(packet *rtp.Packet) (err error) { + if c.conn != nil { + c.send += len(packet.Payload) + _, err = c.conn.Write(packet.Payload) + } + return + }) + c.tracks = append(c.tracks, track) + + return track +} + +func (c *Client) Start() (err error) { + if err = c.Open(); err != nil { + return + } + return +} + +func (c *Client) Stop() (err error) { + if c.conn == nil { + return + } + _ = c.Close() + return c.conn.Close() +} + +func (c *Client) MarshalJSON() ([]byte, error) { + info := &streamer.Info{ + Type: "ISAPI", + Medias: c.medias, + Tracks: c.tracks, + Send: uint32(c.send), + } + return json.Marshal(info) +} diff --git a/pkg/tapo/client.go b/pkg/tapo/client.go index a5e5444..aa8928e 100644 --- a/pkg/tapo/client.go +++ b/pkg/tapo/client.go @@ -73,7 +73,9 @@ func (c *Client) newConn() (net.Conn, error) { u.Host += ":8800" } - req, err := http.NewRequest("POST", u.String(), nil) + // TODO: fix closing connection + ctx, pconn := tcp.WithConn() + req, err := http.NewRequestWithContext(ctx, "POST", u.String(), nil) if err != nil { return nil, err } @@ -93,7 +95,7 @@ func (c *Client) newConn() (net.Conn, error) { c.newDectypter(res, username, password) } - return res.Body.(net.Conn), nil + return *pconn, nil } func (c *Client) newDectypter(res *http.Response, username, password string) { diff --git a/pkg/tcp/request.go b/pkg/tcp/request.go index 2859b75..94e0bd2 100644 --- a/pkg/tcp/request.go +++ b/pkg/tcp/request.go @@ -12,20 +12,29 @@ import ( // Do - http.Client with support Digest Authorization func Do(req *http.Request) (*http.Response, error) { - var conn net.Conn + if client == nil { + transport := http.DefaultTransport.(*http.Transport).Clone() - client := http.Client{Timeout: time.Second * 5000} - - // for multipart requests return conn as Body (for write support) - if ct := req.Header.Get("Content-Type"); strings.HasPrefix(ct, "multipart/mixed") { - var d net.Dialer - client.Transport = &http.Transport{ - DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { - var err error - conn, err = d.DialContext(ctx, network, addr) - return conn, err - }, + dial := transport.DialContext + transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) { + conn, err := dial(ctx, network, addr) + if pconn, ok := ctx.Value(connKey).(*net.Conn); ok { + *pconn = conn + } + return conn, err } + + client = &http.Client{ + Timeout: time.Second * 5000, + Transport: transport, + } + } + + user := req.URL.User + + // Hikvision won't answer on Basic auth with any headers + if strings.HasPrefix(req.URL.Path, "/ISAPI/") { + req.URL.User = nil } res, err := client.Do(req) @@ -33,7 +42,9 @@ func Do(req *http.Request) (*http.Response, error) { return nil, err } - if res.StatusCode == http.StatusUnauthorized && req.URL.User != nil { + if res.StatusCode == http.StatusUnauthorized && user != nil { + Close(res) + auth := res.Header.Get("WWW-Authenticate") if !strings.HasPrefix(auth, "Digest") { return nil, errors.New("unsupported auth: " + auth) @@ -43,7 +54,6 @@ func Do(req *http.Request) (*http.Response, error) { nonce := Between(auth, `nonce="`, `"`) qop := Between(auth, `qop="`, `"`) - user := req.URL.User username := user.Username() password, _ := user.Password() ha1 := HexMD5(username, realm, password) @@ -80,9 +90,19 @@ func Do(req *http.Request) (*http.Response, error) { } } - if conn != nil { - res.Body = conn - } - return res, nil } + +var client *http.Client +var connKey struct{} + +func WithConn() (context.Context, *net.Conn) { + pconn := new(net.Conn) + return context.WithValue(context.Background(), connKey, pconn), pconn +} + +func Close(res *http.Response) { + if res.Body != nil { + _ = res.Body.Close() + } +}