Skip to content

Commit 9cdc702

Browse files
committed
Introduce Dahua CGI source for 2-way audio
1 parent a4d7fd0 commit 9cdc702

File tree

6 files changed

+652
-0
lines changed

6 files changed

+652
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
.idea/
22
.tmp/
33

4+
go2rtc
45
go2rtc.yaml
56
go2rtc.json
67

internal/dahua/init.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package dahua
2+
3+
import (
4+
"github.com/AlexxIT/go2rtc/internal/streams"
5+
"github.com/AlexxIT/go2rtc/pkg/core"
6+
"github.com/AlexxIT/go2rtc/pkg/dahua"
7+
)
8+
9+
func Init() {
10+
streams.HandleFunc("dahua", func(source string) (core.Producer, error) {
11+
return dahua.Dial(source)
12+
})
13+
}

main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"github.com/AlexxIT/go2rtc/internal/api/ws"
77
"github.com/AlexxIT/go2rtc/internal/app"
88
"github.com/AlexxIT/go2rtc/internal/bubble"
9+
"github.com/AlexxIT/go2rtc/internal/dahua"
910
"github.com/AlexxIT/go2rtc/internal/debug"
1011
"github.com/AlexxIT/go2rtc/internal/doorbird"
1112
"github.com/AlexxIT/go2rtc/internal/dvrip"
@@ -84,6 +85,7 @@ func main() {
8485
dvrip.Init() // dvrip source
8586
tapo.Init() // tapo source
8687
isapi.Init() // isapi source
88+
dahua.Init() // dahua source
8789
mpegts.Init() // mpegts passive source
8890
roborock.Init() // roborock source
8991
homekit.Init() // homekit source

pkg/dahua/CGI_SDK_API.txt

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
11. Audio
2+
3+
11.1 Audio MIME type
4+
5+
MIME Description
6+
Audio/PCM
7+
Audio/ADPCM
8+
Audio/G.711A
9+
Audio/G.711Mu
10+
Audio/G.726
11+
Audio/G.729
12+
13+
92
14+
Audio/MPEG2
15+
Audio/AMR
16+
Audio/AAC
17+
18+
19+
20+
11.2 Post Audio
21+
22+
URL Syntax http://<ip>/cgi-bin/audio.cgi?action=postAudio&<paramName>=<paramValue>[&<paramName>=<paramValue>...]
23+
Comment paramValue as below table.
24+
Response OK or ERROR
25+
26+
27+
ParamName ParamValue type Description
28+
httptype string singlepart:HTTP content is a continuous flow of audio
29+
packets
30+
multipart:HTTP content type is
31+
multipart/x-mixed-replace,and each audio packet ends
32+
with a boundary string
33+
channel integer The audio channel
34+
35+
36+
37+
11.2.1 Example for singlepart
38+
39+
The RUL of transmit a singlepart、channel 1 audio stream(encoded with G.711 A-law) is:
40+
http: //<ip>/cgi-bin/audio.cgi?action=postAudio&httptype=singlepart&channel=1
41+
42+
43+
example:
44+
POST /cgi-bin/audio.cgi?action=postAudio&httptype=singlepart&channel=1 HTTP/1.1
45+
Content-Type: Audio/G.711A
46+
Content-Length:9999999
47+
48+
49+
<Audio data>
50+
<Audio data>
51+
52+
53+
54+
11.2.2 Example for multipart
55+
56+
The RUL of transmit a multipart、channel 1 audio stream(encoded with G.711 A-law) is:
57+
http: //<ip>/cgi-bin/audio.cgi?action=postAudio&httptype= multipart &channel=1
58+
59+
60+
example:
61+
POST /cgi-bin/audio.cgi?action=postAudio&httptype= multipart &channel=1 HTTP/1.1
62+
Content-Type: multipart/x-mixed-replace; boundary=<boundary>
63+
--<boundary>
64+
Content-Type: Audio/G.711A
65+
66+
93
67+
Content-Length: 800
68+
69+
70+
<Audio data>
71+
--<boundary>
72+
73+
74+
75+
11.3 Get Audio
76+
77+
URL Syntax http://<ip>/cgi-bin/audio.cgi?action=getAudio&<paramName>=<paramValue>[&<paramName>=<paramValue>...]
78+
Comment paramValue as below table.
79+
Response OK or ERROR
80+
81+
82+
83+
84+
ParamName ParamValue type Description
85+
httptype string singlepart:HTTP content is a continuous flow of audio
86+
packets
87+
multipart:HTTP content type is
88+
multipart/x-mixed-replace,and each audio packet ends
89+
with a boundary string
90+
channel integer The audio channel
91+
92+
93+
94+
95+
11.3.1 Example for singlepart
96+
97+
The RUL of Request a singlepart、channel 1 audio stream(encoded with G.711 A-law) is:
98+
http: //<ip>/cgi-bin/audio.cgi?action=getAudio&httptype=singlepart&channel=1
99+
100+
101+
If the request was successful, the server returns a continuous flow of audio packets.The content type is only set at the beginning of the
102+
connection.
103+
Return:
104+
HTTP Code: 200 OK
105+
Content-Type: Audio/G.711A
106+
Body:
107+
<Audio data>
108+
<Audio data>
109+
110+
111+
112+
11.3.2 Example for multipart
113+
114+
The RUL of Request a multipart、channel 1 audio stream(encoded with G.711 A-law) is:
115+
http: //<ip>/cgi-bin/audio.cgi?action=getAudio&httptype=multipart&channel=1
116+
117+
118+
If the request was successful, the server returns a continuous flow of audio packets. The content type is “multipart/x-mixed-replace” and each
119+
audio packet ends with a boundary string.
120+
94
121+
Return:
122+
HTTP Code: 200 OK
123+
Content-Type: multipart/x-mixed-replace; boundary=<boundary>
124+
--<boundary>
125+
Content-Type: Audio/G.711A
126+
Content-Length: 800
127+
128+
129+
<Audio data>
130+
--<boundary>
131+
132+
133+
134+
135+
11.4 Audio Input
136+
137+
11.4.1 getCollect
138+
139+
URL Syntax http://<ip>/cgi-bin/devAudioInput.cgi?action=getCollect
140+
Comment Get Audio input channel number.
141+
Below response means there are 2 audio input channels.
142+
Response result=2
143+
144+
145+
146+
147+
11.5 Audio Output
148+
149+
11.5.1 getCollect
150+
151+
URL Syntax http://<ip>/cgi-bin/devAudioOutput.cgi?action=getCollect
152+
Comment Get Audio output channel number.
153+
Below response means there are 2 audio output channels.
154+
Response result=2

pkg/dahua/backchannel.go

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
package dahua
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"io"
7+
8+
"github.com/AlexxIT/go2rtc/internal/app"
9+
"github.com/AlexxIT/go2rtc/pkg/core"
10+
"github.com/pion/rtp"
11+
)
12+
13+
func (c *Client) GetMedias() []*core.Media {
14+
return c.medias
15+
}
16+
17+
func (c *Client) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
18+
return nil, core.ErrCantGetTrack
19+
}
20+
21+
func (c *Client) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiver) error {
22+
log := app.GetLogger("dahua")
23+
log.Debug().
24+
Str("media_kind", string(media.Kind)).
25+
Str("codec", track.Codec.Name).
26+
Msg("[dahua] adding track")
27+
28+
if c.sender == nil {
29+
c.sender = core.NewSender(media, track.Codec)
30+
c.sender.Handler = func(packet *rtp.Packet) {
31+
// Get connection in a thread-safe way
32+
conn := c.getConnection()
33+
if conn == nil {
34+
return
35+
}
36+
37+
// Send multipart boundary with payload
38+
boundary := "\r\n--go2rtc-audio-boundary\r\n"
39+
40+
// Determine content type based on codec
41+
var contentType string
42+
switch track.Codec.Name {
43+
case core.CodecPCMA:
44+
contentType = "Audio/G.711A"
45+
case core.CodecPCMU:
46+
contentType = "Audio/G.711Mu"
47+
default:
48+
contentType = "Audio/G.711A"
49+
}
50+
51+
boundary += "Content-Type: " + contentType + "\r\n"
52+
boundary += "Content-Length: " + fmt.Sprintf("%d", len(packet.Payload)) + "\r\n\r\n"
53+
54+
// Write boundary first
55+
if _, err := conn.Write([]byte(boundary)); err != nil {
56+
log.Error().Err(err).Msg("[dahua] failed to write boundary")
57+
return
58+
}
59+
60+
// Write payload
61+
n, err := conn.Write(packet.Payload)
62+
if err != nil {
63+
log.Error().Err(err).Msg("[dahua] failed to write audio payload")
64+
return
65+
}
66+
67+
c.send += n
68+
}
69+
70+
log.Debug().
71+
Str("codec", track.Codec.Name).
72+
Uint32("clock_rate", track.Codec.ClockRate).
73+
Msg("[dahua] audio sender created")
74+
}
75+
76+
c.sender.HandleRTP(track)
77+
return nil
78+
}
79+
80+
// getConnection returns the current connection in a thread-safe way
81+
func (c *Client) getConnection() io.WriteCloser {
82+
// For now, just return the connection directly
83+
// TODO: Add proper synchronization if needed
84+
return c.conn
85+
}
86+
87+
func (c *Client) Start() (err error) {
88+
log := app.GetLogger("dahua")
89+
log.Debug().Msg("[dahua] starting client")
90+
91+
if err = c.open(); err != nil {
92+
log.Error().Err(err).Msg("[dahua] failed to open connection")
93+
return
94+
}
95+
96+
log.Debug().Msg("[dahua] client started successfully")
97+
return
98+
}
99+
100+
func (c *Client) Stop() (err error) {
101+
log := app.GetLogger("dahua")
102+
log.Debug().Msg("[dahua] stopping client")
103+
104+
if c.sender != nil {
105+
c.sender.Close()
106+
log.Debug().Msg("[dahua] sender closed")
107+
}
108+
109+
if c.conn != nil {
110+
err = c.close()
111+
if err != nil {
112+
log.Error().Err(err).Msg("[dahua] error during close")
113+
}
114+
// Don't call c.conn.Close() again since c.close() already handles it
115+
log.Debug().Msg("[dahua] connection closed")
116+
}
117+
118+
log.Debug().Msg("[dahua] client stopped")
119+
return nil
120+
}
121+
122+
func (c *Client) MarshalJSON() ([]byte, error) {
123+
info := &core.Connection{
124+
ID: core.NewID(),
125+
FormatName: "dahua",
126+
Protocol: "http",
127+
Medias: c.medias,
128+
Send: c.send,
129+
}
130+
if c.conn != nil {
131+
info.RemoteAddr = c.url // Use URL instead of RemoteAddr since we don't have a net.Conn
132+
}
133+
if c.sender != nil {
134+
info.Senders = []*core.Sender{c.sender}
135+
}
136+
return json.Marshal(info)
137+
}

0 commit comments

Comments
 (0)