Add support Hikvision ISAPI
This commit is contained in:
@@ -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
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/AlexxIT/go2rtc/cmd/hls"
|
"github.com/AlexxIT/go2rtc/cmd/hls"
|
||||||
"github.com/AlexxIT/go2rtc/cmd/homekit"
|
"github.com/AlexxIT/go2rtc/cmd/homekit"
|
||||||
"github.com/AlexxIT/go2rtc/cmd/http"
|
"github.com/AlexxIT/go2rtc/cmd/http"
|
||||||
|
"github.com/AlexxIT/go2rtc/cmd/isapi"
|
||||||
"github.com/AlexxIT/go2rtc/cmd/ivideon"
|
"github.com/AlexxIT/go2rtc/cmd/ivideon"
|
||||||
"github.com/AlexxIT/go2rtc/cmd/mjpeg"
|
"github.com/AlexxIT/go2rtc/cmd/mjpeg"
|
||||||
"github.com/AlexxIT/go2rtc/cmd/mp4"
|
"github.com/AlexxIT/go2rtc/cmd/mp4"
|
||||||
@@ -43,6 +44,7 @@ func main() {
|
|||||||
http.Init()
|
http.Init()
|
||||||
dvrip.Init()
|
dvrip.Init()
|
||||||
tapo.Init()
|
tapo.Init()
|
||||||
|
isapi.Init()
|
||||||
mpegts.Init()
|
mpegts.Init()
|
||||||
|
|
||||||
srtp.Init()
|
srtp.Init()
|
||||||
|
|||||||
@@ -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"`
|
||||||
|
//}
|
||||||
@@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
+4
-2
@@ -73,7 +73,9 @@ func (c *Client) newConn() (net.Conn, error) {
|
|||||||
u.Host += ":8800"
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -93,7 +95,7 @@ func (c *Client) newConn() (net.Conn, error) {
|
|||||||
c.newDectypter(res, username, password)
|
c.newDectypter(res, username, password)
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.Body.(net.Conn), nil
|
return *pconn, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) newDectypter(res *http.Response, username, password string) {
|
func (c *Client) newDectypter(res *http.Response, username, password string) {
|
||||||
|
|||||||
+38
-18
@@ -12,20 +12,29 @@ import (
|
|||||||
|
|
||||||
// Do - http.Client with support Digest Authorization
|
// Do - http.Client with support Digest Authorization
|
||||||
func Do(req *http.Request) (*http.Response, error) {
|
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}
|
dial := transport.DialContext
|
||||||
|
transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||||
// for multipart requests return conn as Body (for write support)
|
conn, err := dial(ctx, network, addr)
|
||||||
if ct := req.Header.Get("Content-Type"); strings.HasPrefix(ct, "multipart/mixed") {
|
if pconn, ok := ctx.Value(connKey).(*net.Conn); ok {
|
||||||
var d net.Dialer
|
*pconn = conn
|
||||||
client.Transport = &http.Transport{
|
}
|
||||||
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
return conn, err
|
||||||
var err error
|
|
||||||
conn, err = d.DialContext(ctx, network, addr)
|
|
||||||
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)
|
res, err := client.Do(req)
|
||||||
@@ -33,7 +42,9 @@ func Do(req *http.Request) (*http.Response, error) {
|
|||||||
return nil, err
|
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")
|
auth := res.Header.Get("WWW-Authenticate")
|
||||||
if !strings.HasPrefix(auth, "Digest") {
|
if !strings.HasPrefix(auth, "Digest") {
|
||||||
return nil, errors.New("unsupported auth: " + auth)
|
return nil, errors.New("unsupported auth: " + auth)
|
||||||
@@ -43,7 +54,6 @@ func Do(req *http.Request) (*http.Response, error) {
|
|||||||
nonce := Between(auth, `nonce="`, `"`)
|
nonce := Between(auth, `nonce="`, `"`)
|
||||||
qop := Between(auth, `qop="`, `"`)
|
qop := Between(auth, `qop="`, `"`)
|
||||||
|
|
||||||
user := req.URL.User
|
|
||||||
username := user.Username()
|
username := user.Username()
|
||||||
password, _ := user.Password()
|
password, _ := user.Password()
|
||||||
ha1 := HexMD5(username, realm, 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
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user