Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dbe9e4aade | |||
| 715be4dad0 | |||
| 570b7d0d97 | |||
| 80ac0ab17f | |||
| 9ee8174d5f | |||
| 831aa03c9f | |||
| d372597bdb | |||
| 172437b6fc | |||
| 7640a42bfc | |||
| fde04bd625 | |||
| ad14a5ccba | |||
| 2348d12e9d | |||
| 5cafc05e13 | |||
| e982257271 | |||
| 340fd81778 | |||
| 2c34a17d88 | |||
| 6b005a666e | |||
| 1d1bcb0a63 | |||
| 3f5f1328e7 |
@@ -170,7 +170,7 @@ Available modules:
|
||||
- [api](#module-api) - HTTP API (important for WebRTC support)
|
||||
- [rtsp](#module-rtsp) - RTSP Server (important for FFmpeg support)
|
||||
- [webrtc](#module-webrtc) - WebRTC Server
|
||||
- [mp4](#module-mp4) - MSE, MP4 stream and MP4 shapshot Server
|
||||
- [mp4](#module-mp4) - MSE, MP4 stream and MP4 snapshot Server
|
||||
- [hls](#module-hls) - HLS TS or fMP4 stream Server
|
||||
- [mjpeg](#module-mjpeg) - MJPEG Server
|
||||
- [ffmpeg](#source-ffmpeg) - FFmpeg integration
|
||||
@@ -648,10 +648,11 @@ This source type support Roborock vacuums with cameras. Known working models:
|
||||
|
||||
- Roborock S6 MaxV - only video (the vacuum has no microphone)
|
||||
- Roborock S7 MaxV - video and two way audio
|
||||
- Roborock Qrevo MaxV - video and two way audio
|
||||
|
||||
Source support load Roborock credentials from Home Assistant [custom integration](https://github.com/humbertogontijo/homeassistant-roborock). Otherwise, you need to log in to your Roborock account (MiHome account is not supported). Go to: go2rtc WebUI > Add webpage. Copy `roborock://...` source for your vacuum and paste it to `go2rtc.yaml` config.
|
||||
Source support load Roborock credentials from Home Assistant [custom integration](https://github.com/humbertogontijo/homeassistant-roborock) or the [core integration](https://www.home-assistant.io/integrations/roborock). Otherwise, you need to log in to your Roborock account (MiHome account is not supported). Go to: go2rtc WebUI > Add webpage. Copy `roborock://...` source for your vacuum and paste it to `go2rtc.yaml` config.
|
||||
|
||||
If you have graphic pin for your vacuum - add it as numeric pin (lines: 123, 456, 678) to the end of the roborock-link.
|
||||
If you have graphic pin for your vacuum - add it as numeric pin (lines: 123, 456, 789) to the end of the roborock-link.
|
||||
|
||||
#### Source: WebRTC
|
||||
|
||||
|
||||
+10
-10
@@ -37,16 +37,21 @@ var log zerolog.Logger
|
||||
type Message struct {
|
||||
Type string `json:"type"`
|
||||
Value any `json:"value,omitempty"`
|
||||
Raw []byte `json:"-"`
|
||||
}
|
||||
|
||||
func (m *Message) String() (value string) {
|
||||
_ = json.Unmarshal(m.Raw, &value)
|
||||
if s, ok := m.Value.(string); ok {
|
||||
return s
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (m *Message) Unmarshal(v any) error {
|
||||
return json.Unmarshal(m.Raw, v)
|
||||
b, err := json.Marshal(m.Value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(b, v)
|
||||
}
|
||||
|
||||
type WSHandler func(tr *Transport, msg *Message) error
|
||||
@@ -113,11 +118,8 @@ func apiWS(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
|
||||
for {
|
||||
var raw struct {
|
||||
Type string `json:"type"`
|
||||
Value json.RawMessage `json:"value"`
|
||||
}
|
||||
if err = ws.ReadJSON(&raw); err != nil {
|
||||
msg := new(Message)
|
||||
if err = ws.ReadJSON(msg); err != nil {
|
||||
if !websocket.IsCloseError(err, websocket.CloseNoStatusReceived) {
|
||||
log.Trace().Err(err).Caller().Send()
|
||||
}
|
||||
@@ -125,8 +127,6 @@ func apiWS(w http.ResponseWriter, r *http.Request) {
|
||||
break
|
||||
}
|
||||
|
||||
msg := &Message{Type: raw.Type, Raw: raw.Value}
|
||||
|
||||
log.Trace().Str("type", msg.Type).Msg("[api] ws msg")
|
||||
|
||||
if handler := wsHandlers[msg.Type]; handler != nil {
|
||||
|
||||
@@ -19,15 +19,15 @@ go2rtc -c log.format=text -c /config/go2rtc.yaml -c rtsp.listen='' -c /usr/local
|
||||
|
||||
## Environment variables
|
||||
|
||||
Also go2rtc support templates for using environment variables in any part of config:
|
||||
There is support for loading external variables into the config. First, they will be attempted to be loaded from [credential files](https://systemd.io/CREDENTIALS). If `CREDENTIALS_DIRECTORY` is not set, then the key will be loaded from an environment variable. If no environment variable is set, then the string will be left as-is.
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
camera1: rtsp://rtsp:${CAMERA_PASSWORD}@192.168.1.123/av_stream/ch0
|
||||
|
||||
rtsp:
|
||||
username: ${RTSP_USER:admin} # "admin" if env "RTSP_USER" not set
|
||||
password: ${RTSP_PASS:secret} # "secret" if env "RTSP_PASS" not set
|
||||
username: ${RTSP_USER:admin} # "admin" if "RTSP_USER" not set
|
||||
password: ${RTSP_PASS:secret} # "secret" if "RTSP_PASS" not set
|
||||
```
|
||||
|
||||
## JSON Schema
|
||||
|
||||
@@ -179,6 +179,7 @@ func parseArgs(s string) *ffmpeg.Args {
|
||||
Version: verAV,
|
||||
}
|
||||
|
||||
var source = s
|
||||
var query url.Values
|
||||
if i := strings.IndexByte(s, '#'); i >= 0 {
|
||||
query = streams.ParseQuery(s[i+1:])
|
||||
@@ -221,6 +222,10 @@ func parseArgs(s string) *ffmpeg.Args {
|
||||
default:
|
||||
s += "?video&audio"
|
||||
}
|
||||
s += "&source=ffmpeg:" + url.QueryEscape(source)
|
||||
for _, v := range query["query"] {
|
||||
s += "&" + v
|
||||
}
|
||||
args.Input = inputTemplate("rtsp", s, query)
|
||||
} else if i = strings.Index(s, "?"); i > 0 {
|
||||
switch s[:i] {
|
||||
|
||||
+13
-2
@@ -147,6 +147,7 @@ func tcpHandler(conn *rtsp.Conn) {
|
||||
var closer func()
|
||||
|
||||
trace := log.Trace().Enabled()
|
||||
level := zerolog.WarnLevel
|
||||
|
||||
conn.Listen(func(msg any) {
|
||||
if trace {
|
||||
@@ -188,8 +189,18 @@ func tcpHandler(conn *rtsp.Conn) {
|
||||
conn.PacketSize = uint16(core.Atoi(s))
|
||||
}
|
||||
|
||||
// param name like ffmpeg style https://ffmpeg.org/ffmpeg-protocols.html
|
||||
if s := query.Get("log_level"); s != "" {
|
||||
if lvl, err := zerolog.ParseLevel(s); err == nil {
|
||||
level = lvl
|
||||
}
|
||||
}
|
||||
|
||||
// will help to protect looping requests to same source
|
||||
conn.Connection.Source = query.Get("source")
|
||||
|
||||
if err := stream.AddConsumer(conn); err != nil {
|
||||
log.Warn().Err(err).Str("stream", name).Msg("[rtsp]")
|
||||
log.WithLevel(level).Err(err).Str("stream", name).Msg("[rtsp]")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -227,7 +238,7 @@ func tcpHandler(conn *rtsp.Conn) {
|
||||
|
||||
if err := conn.Accept(); err != nil {
|
||||
if err != io.EOF {
|
||||
log.Warn().Err(err).Caller().Send()
|
||||
log.WithLevel(level).Err(err).Caller().Send()
|
||||
}
|
||||
if closer != nil {
|
||||
closer()
|
||||
|
||||
@@ -22,6 +22,12 @@ func (s *Stream) AddConsumer(cons core.Consumer) (err error) {
|
||||
|
||||
producers:
|
||||
for prodN, prod := range s.producers {
|
||||
// check for loop request, ex. `camera1: ffmpeg:camera1`
|
||||
if info, ok := cons.(core.Info); ok && prod.url == info.GetSource() {
|
||||
log.Trace().Msgf("[streams] skip cons=%d prod=%d", consN, prodN)
|
||||
continue
|
||||
}
|
||||
|
||||
if prodErrors[prodN] != nil {
|
||||
log.Trace().Msgf("[streams] skip cons=%d prod=%d", consN, prodN)
|
||||
continue
|
||||
@@ -129,7 +135,7 @@ func formatError(consMedias, prodMedias []*core.Media, prodErrors []error) error
|
||||
for _, media := range prodMedias {
|
||||
if media.Direction == core.DirectionRecvonly {
|
||||
for _, codec := range media.Codecs {
|
||||
prod = appendString(prod, codec.PrintName())
|
||||
prod = appendString(prod, media.Kind+":"+codec.PrintName())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -137,7 +143,7 @@ func formatError(consMedias, prodMedias []*core.Media, prodErrors []error) error
|
||||
for _, media := range consMedias {
|
||||
if media.Direction == core.DirectionSendonly {
|
||||
for _, codec := range media.Codecs {
|
||||
cons = appendString(cons, codec.PrintName())
|
||||
cons = appendString(cons, media.Kind+":"+codec.PrintName())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package webrtc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/api/ws"
|
||||
pion "github.com/pion/webrtc/v3"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestWebRTCAPIv1(t *testing.T) {
|
||||
raw := `{"type":"webrtc/offer","value":"v=0\n..."}`
|
||||
msg := new(ws.Message)
|
||||
err := json.Unmarshal([]byte(raw), msg)
|
||||
require.Nil(t, err)
|
||||
|
||||
require.Equal(t, "v=0\n...", msg.String())
|
||||
}
|
||||
|
||||
func TestWebRTCAPIv2(t *testing.T) {
|
||||
raw := `{"type":"webrtc","value":{"type":"offer","sdp":"v=0\n...","ice_servers":[{"urls":["stun:stun.l.google.com:19302"]}]}}`
|
||||
msg := new(ws.Message)
|
||||
err := json.Unmarshal([]byte(raw), msg)
|
||||
require.Nil(t, err)
|
||||
|
||||
var offer struct {
|
||||
Type string `json:"type"`
|
||||
SDP string `json:"sdp"`
|
||||
ICEServers []pion.ICEServer `json:"ice_servers"`
|
||||
}
|
||||
err = msg.Unmarshal(&offer)
|
||||
require.Nil(t, err)
|
||||
|
||||
require.Equal(t, "offer", offer.Type)
|
||||
require.Equal(t, "v=0\n...", offer.SDP)
|
||||
require.Equal(t, "stun:stun.l.google.com:19302", offer.ICEServers[0].URLs[0])
|
||||
}
|
||||
@@ -36,7 +36,7 @@ import (
|
||||
)
|
||||
|
||||
func main() {
|
||||
app.Version = "1.9.6"
|
||||
app.Version = "1.9.7"
|
||||
|
||||
// 1. Core modules: app, api/ws, streams
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ type Info interface {
|
||||
SetSource(string)
|
||||
SetURL(string)
|
||||
WithRequest(*http.Request)
|
||||
GetSource() string
|
||||
}
|
||||
|
||||
// Connection just like webrtc.PeerConnection
|
||||
@@ -123,6 +124,10 @@ func (c *Connection) WithRequest(r *http.Request) {
|
||||
c.UserAgent = r.UserAgent()
|
||||
}
|
||||
|
||||
func (c *Connection) GetSource() string {
|
||||
return c.Source
|
||||
}
|
||||
|
||||
// Create like os.Create, init Consumer with existing Transport
|
||||
func Create(w io.Writer) (*Connection, error) {
|
||||
return &Connection{Transport: w}, nil
|
||||
|
||||
+7
-6
@@ -28,8 +28,10 @@ func UnmarshalSDP(rawSDP []byte) ([]*core.Media, error) {
|
||||
sd := &sdp.SessionDescription{}
|
||||
if err := sd.Unmarshal(rawSDP); err != nil {
|
||||
// fix multiple `s=` https://github.com/AlexxIT/WebRTC/issues/417
|
||||
re, _ := regexp.Compile("\ns=[^\n]+")
|
||||
rawSDP = re.ReplaceAll(rawSDP, nil)
|
||||
rawSDP = regexp.MustCompile("\ns=[^\n]+").ReplaceAll(rawSDP, nil)
|
||||
|
||||
// fix broken `c=` https://github.com/AlexxIT/go2rtc/issues/1426
|
||||
rawSDP = regexp.MustCompile("\nc=[^\n]+").ReplaceAll(rawSDP, nil)
|
||||
|
||||
// fix SDP header for some cameras
|
||||
if i := bytes.Index(rawSDP, []byte("\nm=")); i > 0 {
|
||||
@@ -38,12 +40,11 @@ func UnmarshalSDP(rawSDP []byte) ([]*core.Media, error) {
|
||||
|
||||
// Fix invalid media type (errSDPInvalidValue) caused by
|
||||
// some TP-LINK IP camera, e.g. TL-IPC44GW
|
||||
m := regexp.MustCompile("m=[^ ]+ ")
|
||||
for _, i := range m.FindAll(rawSDP, -1) {
|
||||
switch string(i[2 : len(i)-1]) {
|
||||
for _, b := range regexp.MustCompile("m=[^ ]+ ").FindAll(rawSDP, -1) {
|
||||
switch string(b[2 : len(b)-1]) {
|
||||
case "audio", "video", "application":
|
||||
default:
|
||||
rawSDP = bytes.Replace(rawSDP, i, []byte("m=application "), 1)
|
||||
rawSDP = bytes.Replace(rawSDP, b, []byte("m=application "), 1)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -203,6 +203,40 @@ a=control:track5
|
||||
assert.Len(t, medias, 4)
|
||||
}
|
||||
|
||||
func TestBugSDP7(t *testing.T) {
|
||||
// https://github.com/AlexxIT/go2rtc/issues/1426
|
||||
s := `v=0
|
||||
o=- 1001 1 IN
|
||||
s=VCP IPC Realtime stream
|
||||
m=video 0 RTP/AVP 105
|
||||
c=IN
|
||||
a=control:rtsp://1.0.1.113/media/video2/video
|
||||
a=rtpmap:105 H264/90000
|
||||
a=fmtp:105 profile-level-id=640016; packetization-mode=1; sprop-parameter-sets=Z2QAFqw7UFAX/LCAAAH0AABOIEI=,aOqPLA==
|
||||
a=recvonly
|
||||
m=audio 0 RTP/AVP 0
|
||||
c=IN
|
||||
a=fmtp:0 RTCP=0
|
||||
a=control:rtsp://1.0.1.113/media/video2/audio1
|
||||
a=recvonly
|
||||
m=audio 0 RTP/AVP 0
|
||||
c=IN
|
||||
a=control:rtsp://1.0.1.113/media/video2/backchannel
|
||||
a=rtpmap:0 PCMA/8000
|
||||
a=rtpmap:0 PCMU/8000
|
||||
a=sendonly
|
||||
m=application 0 RTP/AVP 107
|
||||
c=IN
|
||||
a=control:rtsp://1.0.1.113/media/video2/metadata
|
||||
a=rtpmap:107 vnd.onvif.metadata/90000
|
||||
a=fmtp:107 DecoderTag=h3c-v3 RTCP=0
|
||||
a=recvonly
|
||||
`
|
||||
medias, err := UnmarshalSDP([]byte(s))
|
||||
assert.Nil(t, err)
|
||||
assert.Len(t, medias, 4)
|
||||
}
|
||||
|
||||
func TestHikvisionPCM(t *testing.T) {
|
||||
s := `v=0
|
||||
o=- 1721969533379665 1721969533379665 IN IP4 192.168.1.12
|
||||
|
||||
@@ -3,6 +3,7 @@ package shell
|
||||
import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"syscall"
|
||||
@@ -51,6 +52,13 @@ func ReplaceEnvVars(text string) string {
|
||||
dok = true
|
||||
}
|
||||
|
||||
if dir, vok := os.LookupEnv("CREDENTIALS_DIRECTORY"); vok {
|
||||
value, err := os.ReadFile(filepath.Join(dir, key))
|
||||
if err == nil {
|
||||
return strings.TrimSpace(string(value))
|
||||
}
|
||||
}
|
||||
|
||||
if value, vok := os.LookupEnv(key); vok {
|
||||
return value
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ func NewConn(pc *webrtc.PeerConnection) *Conn {
|
||||
Connection: core.Connection{
|
||||
ID: core.NewID(),
|
||||
FormatName: "webrtc",
|
||||
Transport: pc,
|
||||
},
|
||||
pc: pc,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user