Add support Hikvision ISAPI

This commit is contained in:
Alexey Khit
2023-02-28 22:55:19 +03:00
parent ab1b3932ac
commit 5846cbd989
7 changed files with 292 additions and 20 deletions

22
cmd/isapi/init.go Normal file
View File

@@ -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
}

View File

@@ -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()

152
pkg/isapi/client.go Normal file
View File

@@ -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, `<audioCompressionType>`, `<`)
switch codec {
case "G.711ulaw":
codec = streamer.CodecPCMU
case "G.711alaw":
codec = streamer.CodecPCMA
default:
return nil
}
c.channel = streamer.Between(xml, `<id>`, `<`)
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"`
//}

18
pkg/isapi/consumer.go Normal file
View File

@@ -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)
})
}

56
pkg/isapi/producer.go Normal file
View File

@@ -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)
}

View File

@@ -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) {

View File

@@ -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()
}
}