Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 18d7b9075b | |||
| 7c4497f856 | |||
| befa4ca1e6 | |||
| dd3b326f7a | |||
| e36123bb19 | |||
| 9310343ad3 | |||
| e2d4fa3393 | |||
| 5fea2932c1 | |||
| 1fd110b70d | |||
| 8377cf2655 | |||
| 8f01b08d42 | |||
| 97ce4c3114 | |||
| 4813a64d9d | |||
| 7923ec74a8 | |||
| 1f0a5fb880 | |||
| c6a3ee65b8 | |||
| 12b712426d | |||
| a9af245ef8 | |||
| f251129a2f | |||
| d28debabe9 | |||
| 07bf00f9f6 | |||
| be6ec7dbb9 | |||
| 4e575d1356 | |||
| 4cbacfec0c | |||
| 31e24c6e03 | |||
| 401bf85a10 | |||
| f36851f83a | |||
| 67522dbb19 |
@@ -1,10 +1,10 @@
|
||||
# go2rtc
|
||||
|
||||
**go2rtc** - ultimate camera streaming application with support RTSP, WebRTC, FFmpeg, RTMP, etc.
|
||||
Ultimate camera streaming application with support RTSP, WebRTC, FFmpeg, RTMP, etc.
|
||||
|
||||
- zero-dependency and zero-config small [app for all OS](#installation) (Windows, macOS, Linux, ARM)
|
||||
- zero-dependency and zero-config small [app for all OS](#go2rtc-binary) (Windows, macOS, Linux, ARM)
|
||||
- zero-delay for all supported protocols (lowest possible streaming latency)
|
||||
- zero-load on CPU for supported codecs
|
||||
- low CPU load for supported codecs
|
||||
- on the fly transcoding for unsupported codecs via [FFmpeg](#source-ffmpeg)
|
||||
- multi-source 2-way [codecs negotiation](#codecs-negotiation)
|
||||
- streaming from private networks via [Ngrok](#module-webrtc)
|
||||
@@ -14,7 +14,7 @@
|
||||
- [webrtc](https://github.com/pion/webrtc) go library and whole [@pion](https://github.com/pion) team
|
||||
- series of streaming projects from [@deepch](https://github.com/deepch)
|
||||
- [rtsp-simple-server](https://github.com/aler9/rtsp-simple-server) idea from [@aler9](https://github.com/aler9)
|
||||
- [GStreamer](https://gstreamer.freedesktop.org/) multimedia framework pipeline idea
|
||||
- [GStreamer](https://gstreamer.freedesktop.org/) framework pipeline idea
|
||||
- [MediaSoup](https://mediasoup.org/) framework routing idea
|
||||
|
||||
## Codecs negotiation
|
||||
@@ -76,7 +76,7 @@ Download binary for your OS from [latest release](https://github.com/AlexxIT/go2
|
||||
- `go2rtc_mac_amd64` - Mac with Intel
|
||||
- `go2rtc_mac_arm64` - Mac with M1
|
||||
|
||||
Don't forget to fix the rights `chmod +x go2rtc_linux_xxx` on Linux and Mac.
|
||||
Don't forget to fix the rights `chmod +x go2rtc_xxx_xxx` on Linux and Mac.
|
||||
|
||||
### go2rtc: Home Assistant Add-on
|
||||
|
||||
@@ -95,6 +95,16 @@ Don't forget to fix the rights `chmod +x go2rtc_linux_xxx` on Linux and Mac.
|
||||
|
||||
Container [alexxit/go2rtc](https://hub.docker.com/r/alexxit/go2rtc) with support `amd64`, `386`, `arm64`, `arm`. This container same as [Home Assistant Add-on](#go2rtc-home-assistant-add-on), but can be used separately from the Home Assistant. Container has preinstalled [FFmpeg](#source-ffmpeg) and [Ngrok](#module-ngrok) applications.
|
||||
|
||||
```yaml
|
||||
services:
|
||||
go2rtc:
|
||||
image: alexxit/go2rtc
|
||||
network_mode: host
|
||||
restart: always
|
||||
volumes:
|
||||
- "~/go2rtc.yaml:/config/go2rtc.yaml"
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Create file `go2rtc.yaml` next to the app.
|
||||
@@ -103,7 +113,7 @@ Create file `go2rtc.yaml` next to the app.
|
||||
- `api` server will start on default **1984 port**
|
||||
- `rtsp` server will start on default **8554 port**
|
||||
- `webrtc` will use random UDP port for each connection
|
||||
- `ffmpeg` will use default transcoding options (you need to install it [manually](https://ffmpeg.org/))
|
||||
- `ffmpeg` will use default transcoding options (you may install it [manually](https://ffmpeg.org/))
|
||||
|
||||
Available modules:
|
||||
|
||||
@@ -118,7 +128,7 @@ Available modules:
|
||||
|
||||
### Module: Streams
|
||||
|
||||
**go2rtc** support different stream source types. You can config only one link as stream source or multiple.
|
||||
**go2rtc** support different stream source types. You can config one or multiple links of any type as stream source.
|
||||
|
||||
Available source types:
|
||||
|
||||
@@ -128,12 +138,14 @@ Available source types:
|
||||
- [exec](#source-exec) - advanced FFmpeg and GStreamer integration
|
||||
- [hass](#source-hass) - Home Assistant integration
|
||||
|
||||
**PS.** You can use sources like `MJPEG`, `HLS` and others via FFmpeg integration.
|
||||
|
||||
#### Source: RTSP
|
||||
|
||||
- Support **RTSP and RTSPS** links with multiple video and audio tracks
|
||||
- Support **2-way audio** ONLY for [ONVIF Profile T](https://www.onvif.org/specs/stream/ONVIF-Streaming-Spec.pdf) cameras (back channel connection)
|
||||
|
||||
**Attention:** proprietary 2-way audio standards are not supported!
|
||||
**Attention:** other 2-way audio standards are not supported! ONVIF without Profile T is not supported!
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
@@ -187,28 +199,15 @@ streams:
|
||||
rtsp: ffmpeg:rtsp://rtsp:12345678@192.168.1.123/av_stream/ch0#video=copy#audio=copy
|
||||
```
|
||||
|
||||
All trascoding formats has built-in templates. But you can override them via YAML config. You can also add your own formats to config and use them with source params.
|
||||
All trascoding formats has [built-in templates](https://github.com/AlexxIT/go2rtc/blob/master/cmd/ffmpeg/ffmpeg.go): `h264`, `h264/ultra`, `h264/high`, `h265`, `opus`, `pcmu`, `pcmu/16000`, `pcmu/48000`, `pcma`, `pcma/16000`, `pcma/48000`, `aac/16000`.
|
||||
|
||||
But you can override them via YAML config. You can also add your own formats to config and use them with source params.
|
||||
|
||||
```yaml
|
||||
ffmpeg:
|
||||
bin: ffmpeg # path to ffmpeg binary
|
||||
link: -hide_banner -i {input} # if input is link
|
||||
file: -hide_banner -re -stream_loop -1 -i {input} # if input not link
|
||||
rtsp: -hide_banner -fflags nobuffer -flags low_delay -rtsp_transport tcp -i {input} # if input is RTSP link
|
||||
output: -rtsp_transport tcp -f rtsp {output} # output
|
||||
|
||||
h264: "-codec:v libx264 -g 30 -preset superfast -tune zerolatency -profile main -level 4.1"
|
||||
h264/ultra: "-codec:v libx264 -g 30 -preset ultrafast -tune zerolatency"
|
||||
h264/high: "-codec:v libx264 -g 30 -preset superfast -tune zerolatency"
|
||||
h265: "-codec:v libx265 -g 30 -preset ultrafast -tune zerolatency"
|
||||
opus: "-codec:a libopus -ar 48000 -ac 2"
|
||||
pcmu: "-codec:a pcm_mulaw -ar 8000 -ac 1"
|
||||
pcmu/16000: "-codec:a pcm_mulaw -ar 16000 -ac 1"
|
||||
pcmu/48000: "-codec:a pcm_mulaw -ar 48000 -ac 1"
|
||||
pcma: "-codec:a pcm_alaw -ar 8000 -ac 1"
|
||||
pcma/16000: "-codec:a pcm_alaw -ar 16000 -ac 1"
|
||||
pcma/48000: "-codec:a pcm_alaw -ar 48000 -ac 1"
|
||||
aac/16000: "-codec:a aac -ar 16000 -ac 1"
|
||||
h264: "-codec:v libx264 -g 30 -preset superfast -tune zerolatency -profile main -level 4.1"
|
||||
mycodec: "-any args that support ffmpeg..."
|
||||
```
|
||||
|
||||
#### Source: Exec
|
||||
@@ -274,9 +273,9 @@ rtsp:
|
||||
|
||||
### Module: WebRTC
|
||||
|
||||
WebRTC usually works without problems in the local network. But external access may require additional settings. It depends on what type of internet do you have.
|
||||
WebRTC usually works without problems in the local network. But external access may require additional settings. It depends on what type of Internet do you have.
|
||||
|
||||
- by default, WebRTC use two random UDP ports for each connection (for video and audio)
|
||||
- by default, WebRTC use two random UDP ports for each connection (video and audio)
|
||||
- you can enable one additional TCP port for all connections and use it for external access
|
||||
|
||||
**Static public IP**
|
||||
@@ -405,10 +404,9 @@ In other cases you need to use IP-address of server with **go2rtc** application.
|
||||
2. Add generic camera with RTSP link:
|
||||
- Hass > Settings > Integrations > Add Integration > [Generic Camera](https://my.home-assistant.io/redirect/config_flow_start/?domain=generic) > `rtsp://...` or `rtmp://...`
|
||||
3. Use Picture Entity or Picture Glance lovelace card
|
||||
- you can use either direct RTSP links to cameras or take RTSP streams from **go2rtc**
|
||||
4. Open full screen card - this is should be WebRTC stream
|
||||
|
||||
- you can use either direct RTSP links to cameras or take RTSP streams from **go2rtc**
|
||||
|
||||
PS. Default Home Assistant lovelace cards don't support 2-way audio. You can use 2-way audio from [Add-on Web UI](https://my.home-assistant.io/redirect/supervisor_addon/?addon=a889bffc_go2rtc&repository_url=https%3A%2F%2Fgithub.com%2FAlexxIT%2Fhassio-addons). But you need use HTTPS to access the microphone. This is a browser restriction and cannot be avoided.
|
||||
|
||||
### Module: Log
|
||||
@@ -425,3 +423,29 @@ log:
|
||||
streams: error
|
||||
webrtc: fatal
|
||||
```
|
||||
|
||||
## Security
|
||||
|
||||
By default `go2rtc` start Web interface on port `1984` and RTSP on port `8554`. Both ports are accessible from your local network. So anyone on your local network can watch video from your cameras without authorization. The same rule applies to the Home Assistant Add-on.
|
||||
|
||||
This is not a problem if you trust your local network as much as I do. But you can change this behaviour with a `go2rtc.yaml` config:
|
||||
|
||||
```yaml
|
||||
api:
|
||||
listen: "127.0.0.1:1984" # localhost
|
||||
|
||||
rtsp:
|
||||
listen: "127.0.0.1:8554" # localhost
|
||||
|
||||
webrtc:
|
||||
listen: ":8555" # external TCP port
|
||||
```
|
||||
|
||||
- local access to RTSP is not a problem for [FFmpeg](#source-ffmpeg) integration, because it runs locally on your server
|
||||
- local access to API is not a problem for [Home Assistant Add-on](#go2rtc-home-assistant-add-on), because Hass runs locally on same server and Add-on Web UI protected with Hass authorization ([Ingress feature](https://www.home-assistant.io/blog/2019/04/15/hassio-ingress/))
|
||||
- external access to WebRTC TCP port is not a problem, because it used only for transmit encrypted media data
|
||||
- anyway you need to open this port to your local network and to the Internet in order for WebRTC to work
|
||||
|
||||
If you need Web interface protection without Home Assistant Add-on - you need to use reverse proxy, like [Nginx](https://nginx.org/), [Caddy](https://caddyserver.com/), [Ngrok](https://ngrok.com/), etc.
|
||||
|
||||
PS. Additionally WebRTC opens a lot of random UDP ports for transmit encrypted media. They work without problems on the local network. And sometimes work for external access, even if you haven't opened ports on your router. But for stable external WebRTC access, you need to configure the TCP port.
|
||||
|
||||
@@ -5,16 +5,18 @@ RUN apk add --no-cache git go ffmpeg
|
||||
|
||||
ARG BUILD_ARCH
|
||||
|
||||
WORKDIR app
|
||||
|
||||
RUN git clone https://github.com/AlexxIT/go2rtc .
|
||||
RUN CGO_ENABLED=0 go build -ldflags "-s -w" -trimpath
|
||||
RUN git clone https://github.com/AlexxIT/go2rtc \
|
||||
&& cd go2rtc \
|
||||
&& CGO_ENABLED=0 go build -ldflags "-s -w" -trimpath -o /usr/local/bin
|
||||
|
||||
# https://github.com/home-assistant/docker-base/blob/master/alpine/Dockerfile
|
||||
RUN if [ "${BUILD_ARCH}" = "aarch64" ]; then BUILD_ARCH="arm64"; \
|
||||
elif [ "${BUILD_ARCH}" = "armv7" ]; then BUILD_ARCH="arm"; fi \
|
||||
&& cd go2rtc \
|
||||
&& curl $(curl -s "https://raw.githubusercontent.com/ngrok/docker-ngrok/main/releases.json" | jq -r ".${BUILD_ARCH}.url") -o ngrok.zip \
|
||||
&& unzip ngrok
|
||||
&& unzip ngrok -d /usr/local/bin
|
||||
|
||||
RUN rm -r /go2rtc
|
||||
|
||||
COPY run.sh /
|
||||
RUN chmod a+x /run.sh
|
||||
|
||||
+7
-6
@@ -2,12 +2,13 @@
|
||||
|
||||
set +e
|
||||
|
||||
while true; do
|
||||
if [ -x /config/go2rtc ]; then
|
||||
/config/go2rtc -config /config/go2rtc.yaml
|
||||
else
|
||||
/app/go2rtc -config /config/go2rtc.yaml
|
||||
fi
|
||||
# set cwd for go2rtc (for config file, Hass itegration, etc)
|
||||
cd /config
|
||||
|
||||
# add the feature to override go2rtc binary from Hass config folder
|
||||
export PATH="/config:$PATH"
|
||||
|
||||
while true; do
|
||||
go2rtc
|
||||
sleep 5
|
||||
done
|
||||
+2
-11
@@ -9,8 +9,6 @@ import (
|
||||
"github.com/rs/zerolog"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
@@ -39,15 +37,14 @@ func Init() {
|
||||
|
||||
HandleFunc("/api/frame.mp4", frameHandler)
|
||||
HandleFunc("/api/frame.raw", frameHandler)
|
||||
HandleFunc("/api/stack", stackHandler)
|
||||
HandleFunc("/api/streams", streamsHandler)
|
||||
HandleFunc("/api/exit", exitHandler)
|
||||
HandleFunc("/api/ws", apiWS)
|
||||
|
||||
// ensure we can listen without errors
|
||||
listener, err := net.Listen("tcp", cfg.Mod.Listen)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("[api] listen")
|
||||
return
|
||||
}
|
||||
|
||||
log.Info().Str("addr", cfg.Mod.Listen).Msg("[api] listen")
|
||||
@@ -55,7 +52,7 @@ func Init() {
|
||||
go func() {
|
||||
s := http.Server{}
|
||||
if err = s.Serve(listener); err != nil {
|
||||
log.Fatal().Err(err).Msg("[api] Serve")
|
||||
log.Fatal().Err(err).Msg("[api] serve")
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -99,12 +96,6 @@ func streamsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
func exitHandler(w http.ResponseWriter, r *http.Request) {
|
||||
s := r.URL.Query().Get("code")
|
||||
code, _ := strconv.Atoi(s)
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
func apiWS(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := new(Context)
|
||||
if err := ctx.Upgrade(w, r); err != nil {
|
||||
|
||||
+2
-2
@@ -8,8 +8,8 @@ import (
|
||||
)
|
||||
|
||||
func frameHandler(w http.ResponseWriter, r *http.Request) {
|
||||
url := r.URL.Query().Get("url")
|
||||
stream := streams.Get(url)
|
||||
src := r.URL.Query().Get("src")
|
||||
stream := streams.Get(src)
|
||||
if stream == nil {
|
||||
return
|
||||
}
|
||||
|
||||
+8
-2
@@ -24,7 +24,11 @@ func Init() {
|
||||
Mod map[string]string `yaml:"log"`
|
||||
}
|
||||
|
||||
LoadConfig(&cfg)
|
||||
if data != nil {
|
||||
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
||||
println("ERROR: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
var writer io.Writer = os.Stdout
|
||||
|
||||
@@ -48,7 +52,9 @@ func Init() {
|
||||
|
||||
modules = cfg.Mod
|
||||
|
||||
log.Info().Msgf("go2rtc %s/%s", runtime.GOOS, runtime.GOARCH)
|
||||
path, _ := os.Getwd()
|
||||
log.Debug().Str("os", runtime.GOOS).Str("arch", runtime.GOARCH).
|
||||
Str("cwd", path).Int("conf_size", len(data)).Msgf("[app]")
|
||||
}
|
||||
|
||||
func LoadConfig(v interface{}) {
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package debug
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/cmd/api"
|
||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
api.HandleFunc("/api/stack", stackHandler)
|
||||
api.HandleFunc("/api/exit", exitHandler)
|
||||
|
||||
streams.HandleFunc("null", nullHandler)
|
||||
}
|
||||
|
||||
func exitHandler(_ http.ResponseWriter, r *http.Request) {
|
||||
s := r.URL.Query().Get("code")
|
||||
code, _ := strconv.Atoi(s)
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
func nullHandler(string) (streamer.Producer, error) {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package api
|
||||
package debug
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
+28
-11
@@ -41,28 +41,45 @@ func Init() {
|
||||
return
|
||||
}
|
||||
|
||||
ent := new(entries)
|
||||
if err = json.Unmarshal(data, ent); err != nil {
|
||||
storage := new(entries)
|
||||
if err = json.Unmarshal(data, storage); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
urls := map[string]string{}
|
||||
|
||||
for _, entrie := range ent.Data.Entries {
|
||||
switch entrie.Domain {
|
||||
case "generic":
|
||||
if entrie.Options.StreamSource != "" {
|
||||
urls[entrie.Title] = entrie.Options.StreamSource
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
streams.HandleFunc("hass", func(url string) (streamer.Producer, error) {
|
||||
if hurl := urls[url[5:]]; hurl != "" {
|
||||
return streams.GetProducer(hurl)
|
||||
}
|
||||
return nil, fmt.Errorf("can't get url: %s", url)
|
||||
})
|
||||
|
||||
for _, entrie := range storage.Data.Entries {
|
||||
switch entrie.Domain {
|
||||
case "generic":
|
||||
if entrie.Options.StreamSource == "" {
|
||||
continue
|
||||
}
|
||||
urls[entrie.Title] = entrie.Options.StreamSource
|
||||
|
||||
//case "homekit_controller":
|
||||
// if entrie.Data.ClientID == "" {
|
||||
// continue
|
||||
// }
|
||||
// urls[entrie.Title] = fmt.Sprintf(
|
||||
// "homekit://%s:%d?client_id=%s&client_private=%s%s&device_id=%s&device_public=%s",
|
||||
// entrie.Data.DeviceHost, entrie.Data.DevicePort,
|
||||
// entrie.Data.ClientID, entrie.Data.ClientPrivate, entrie.Data.ClientPublic,
|
||||
// entrie.Data.DeviceID, entrie.Data.DevicePublic,
|
||||
// )
|
||||
|
||||
default:
|
||||
continue
|
||||
}
|
||||
|
||||
streams.Get("hass:" + entrie.Title)
|
||||
}
|
||||
}
|
||||
|
||||
var log zerolog.Logger
|
||||
|
||||
+2
-2
@@ -13,8 +13,8 @@ func Init() {
|
||||
}
|
||||
|
||||
func handler(ctx *api.Context, msg *streamer.Message) {
|
||||
url := ctx.Request.URL.Query().Get("url")
|
||||
stream := streams.Get(url)
|
||||
src := ctx.Request.URL.Query().Get("src")
|
||||
stream := streams.Get(src)
|
||||
if stream == nil {
|
||||
return
|
||||
}
|
||||
|
||||
+10
-1
@@ -65,8 +65,17 @@ func rtspHandler(url string) (streamer.Producer, error) {
|
||||
if err = conn.Dial(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
conn.Backchannel = true
|
||||
if err = conn.Describe(); err != nil {
|
||||
return nil, err
|
||||
// second try without backchannel, we need to reconnect
|
||||
if err = conn.Dial(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
conn.Backchannel = false
|
||||
if err = conn.Describe(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return conn, nil
|
||||
|
||||
+22
-3
@@ -2,6 +2,7 @@ package streams
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type state byte
|
||||
@@ -21,15 +22,19 @@ type Producer struct {
|
||||
tracks []*streamer.Track
|
||||
|
||||
state state
|
||||
mx sync.Mutex
|
||||
}
|
||||
|
||||
func (p *Producer) GetMedias() []*streamer.Media {
|
||||
p.mx.Lock()
|
||||
defer p.mx.Unlock()
|
||||
|
||||
if p.state == stateNone {
|
||||
log.Debug().Str("url", p.url).Msg("[streams] probe producer")
|
||||
|
||||
var err error
|
||||
p.element, err = GetProducer(p.url)
|
||||
if err != nil {
|
||||
if err != nil || p.element == nil {
|
||||
log.Error().Err(err).Str("url", p.url).Msg("[streams] probe producer")
|
||||
return nil
|
||||
}
|
||||
@@ -41,6 +46,9 @@ func (p *Producer) GetMedias() []*streamer.Media {
|
||||
}
|
||||
|
||||
func (p *Producer) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.Track {
|
||||
p.mx.Lock()
|
||||
defer p.mx.Unlock()
|
||||
|
||||
if p.state == stateMedias {
|
||||
p.state = stateTracks
|
||||
}
|
||||
@@ -61,6 +69,9 @@ func (p *Producer) GetTrack(media *streamer.Media, codec *streamer.Codec) *strea
|
||||
// internals
|
||||
|
||||
func (p *Producer) start() {
|
||||
p.mx.Lock()
|
||||
defer p.mx.Unlock()
|
||||
|
||||
if p.state != stateTracks {
|
||||
return
|
||||
}
|
||||
@@ -72,10 +83,18 @@ func (p *Producer) start() {
|
||||
}
|
||||
|
||||
func (p *Producer) stop() {
|
||||
p.mx.Lock()
|
||||
|
||||
log.Debug().Str("url", p.url).Msg("[streams] stop producer")
|
||||
|
||||
_ = p.element.Stop()
|
||||
p.element = nil
|
||||
if p.element != nil {
|
||||
_ = p.element.Stop()
|
||||
p.element = nil
|
||||
} else {
|
||||
log.Warn().Str("url", p.url).Msg("[streams] stop empty producer")
|
||||
}
|
||||
p.tracks = nil
|
||||
p.state = stateNone
|
||||
|
||||
p.mx.Unlock()
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package streams
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
)
|
||||
|
||||
@@ -78,7 +79,7 @@ func (s *Stream) AddConsumer(cons streamer.Consumer) (err error) {
|
||||
|
||||
// can't match tracks for consumer
|
||||
if len(consumer.tracks) == 0 {
|
||||
return nil
|
||||
return errors.New("couldn't find the matching tracks")
|
||||
}
|
||||
|
||||
s.consumers = append(s.consumers, consumer)
|
||||
|
||||
@@ -2,6 +2,7 @@ package streams
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/fake"
|
||||
"github.com/AlexxIT/go2rtc/pkg/rtsp"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
@@ -103,7 +104,7 @@ a=control:streamid=0
|
||||
|
||||
func TestRouting(t *testing.T) {
|
||||
prod := &fake.Producer{}
|
||||
prod.Medias, _ = streamer.UnmarshalRTSPSDP([]byte(dahuaSimple))
|
||||
prod.Medias, _ = rtsp.UnmarshalSDP([]byte(dahuaSimple))
|
||||
assert.Len(t, prod.Medias, 3)
|
||||
|
||||
HandleFunc("fake", func(url string) (streamer.Producer, error) {
|
||||
|
||||
@@ -63,13 +63,13 @@ var log zerolog.Logger
|
||||
var NewPConn func() (*pion.PeerConnection, error)
|
||||
|
||||
func offerHandler(ctx *api.Context, msg *streamer.Message) {
|
||||
name := ctx.Request.URL.Query().Get("url")
|
||||
stream := streams.Get(name)
|
||||
src := ctx.Request.URL.Query().Get("src")
|
||||
stream := streams.Get(src)
|
||||
if stream == nil {
|
||||
return
|
||||
}
|
||||
|
||||
log.Debug().Str("stream", name).Msg("[webrtc] new consumer")
|
||||
log.Debug().Str("src", src).Msg("[webrtc] new consumer")
|
||||
|
||||
var err error
|
||||
|
||||
@@ -108,6 +108,7 @@ func offerHandler(ctx *api.Context, msg *streamer.Message) {
|
||||
// 2. AddConsumer, so we get new tracks
|
||||
if err = stream.AddConsumer(conn); err != nil {
|
||||
log.Warn().Err(err).Msg("[api.webrtc] add consumer")
|
||||
_ = conn.Conn.Close()
|
||||
ctx.Error(err)
|
||||
return
|
||||
}
|
||||
@@ -168,6 +169,7 @@ func ExchangeSDP(
|
||||
// 2. AddConsumer, so we get new tracks
|
||||
if err = stream.AddConsumer(conn); err != nil {
|
||||
log.Warn().Err(err).Msg("[api.webrtc] add consumer")
|
||||
_ = conn.Conn.Close()
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/cmd/api"
|
||||
"github.com/AlexxIT/go2rtc/cmd/app"
|
||||
"github.com/AlexxIT/go2rtc/cmd/debug"
|
||||
"github.com/AlexxIT/go2rtc/cmd/exec"
|
||||
"github.com/AlexxIT/go2rtc/cmd/ffmpeg"
|
||||
"github.com/AlexxIT/go2rtc/cmd/hass"
|
||||
@@ -33,6 +34,7 @@ func main() {
|
||||
mse.Init()
|
||||
|
||||
ngrok.Init()
|
||||
debug.Init()
|
||||
|
||||
sigs := make(chan os.Signal, 1)
|
||||
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
+52
-33
@@ -50,6 +50,8 @@ type Conn struct {
|
||||
|
||||
// public
|
||||
|
||||
Backchannel bool
|
||||
|
||||
Medias []*streamer.Media
|
||||
Session string
|
||||
UserAgent string
|
||||
@@ -106,6 +108,9 @@ func (c *Conn) Dial() (err error) {
|
||||
//if c.state != StateClientInit {
|
||||
// panic("wrong state")
|
||||
//}
|
||||
if c.conn != nil && c.auth != nil {
|
||||
c.auth.Reset()
|
||||
}
|
||||
|
||||
c.conn, err = net.DialTimeout(
|
||||
"tcp", c.URL.Host, 10*time.Second,
|
||||
@@ -146,7 +151,9 @@ func (c *Conn) Request(req *tcp.Request) error {
|
||||
}
|
||||
|
||||
c.sequence++
|
||||
req.Header.Set("CSeq", strconv.Itoa(c.sequence))
|
||||
// important to send case sensitive CSeq
|
||||
// https://github.com/AlexxIT/go2rtc/issues/7
|
||||
req.Header["CSeq"] = []string{strconv.Itoa(c.sequence)}
|
||||
|
||||
c.auth.Write(req)
|
||||
|
||||
@@ -256,21 +263,17 @@ func (c *Conn) Describe() error {
|
||||
Method: MethodDescribe,
|
||||
URL: c.URL,
|
||||
Header: map[string][]string{
|
||||
"Accept": {"application/sdp"},
|
||||
"Require": {"www.onvif.org/ver20/backchannel"},
|
||||
"Accept": {"application/sdp"},
|
||||
},
|
||||
}
|
||||
|
||||
if c.Backchannel {
|
||||
req.Header.Set("Require", "www.onvif.org/ver20/backchannel")
|
||||
}
|
||||
|
||||
res, err := c.Do(req)
|
||||
if err != nil {
|
||||
if res != nil {
|
||||
// if we have answer - give second chanse without onvif header
|
||||
req.Header.Del("Require")
|
||||
res, err = c.Do(req)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if val := res.Header.Get("Content-Base"); val != "" {
|
||||
@@ -280,13 +283,7 @@ func (c *Conn) Describe() error {
|
||||
}
|
||||
}
|
||||
|
||||
// fix bug in Sonoff camera SDP "o=- 1 1 IN IP4 rom t_rtsplin"
|
||||
// TODO: make some universal fix
|
||||
if i := bytes.Index(res.Body, []byte("rom t_rtsplin")); i > 0 {
|
||||
res.Body[i+3] = '_'
|
||||
}
|
||||
|
||||
c.Medias, err = streamer.UnmarshalRTSPSDP(res.Body)
|
||||
c.Medias, err = UnmarshalSDP(res.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -372,9 +369,10 @@ func (c *Conn) SetupMedia(
|
||||
|
||||
// Transport: RTP/AVP/TCP;unicast;interleaved=10-11;ssrc=10117CB7
|
||||
// Transport: RTP/AVP/TCP;unicast;destination=192.168.1.123;source=192.168.10.12;interleaved=0
|
||||
// Transport: RTP/AVP/TCP;ssrc=22345682;interleaved=0-1
|
||||
s := res.Header.Get("Transport")
|
||||
// TODO: rewrite
|
||||
if !strings.HasPrefix(s, "RTP/AVP/TCP;unicast") {
|
||||
if !strings.HasPrefix(s, "RTP/AVP/TCP;") {
|
||||
return nil, fmt.Errorf("wrong transport: %s", s)
|
||||
}
|
||||
|
||||
@@ -453,15 +451,17 @@ func (c *Conn) Accept() error {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.URL == nil {
|
||||
c.URL = req.URL
|
||||
c.UserAgent = req.Header.Get("User-Agent")
|
||||
}
|
||||
|
||||
c.Fire(req)
|
||||
|
||||
// Receiver: OPTIONS > DESCRIBE > SETUP... > PLAY > TEARDOWN
|
||||
// Sender: OPTIONS > ANNOUNCE > SETUP... > RECORD > TEARDOWN
|
||||
switch req.Method {
|
||||
case MethodOptions:
|
||||
c.URL = req.URL
|
||||
c.UserAgent = req.Header.Get("User-Agent")
|
||||
|
||||
res := &tcp.Response{
|
||||
Header: map[string][]string{
|
||||
"Public": {"OPTIONS, SETUP, TEARDOWN, DESCRIBE, PLAY, PAUSE, ANNOUNCE, RECORD"},
|
||||
@@ -477,7 +477,7 @@ func (c *Conn) Accept() error {
|
||||
return errors.New("wrong content type")
|
||||
}
|
||||
|
||||
c.Medias, err = streamer.UnmarshalRTSPSDP(req.Body)
|
||||
c.Medias, err = UnmarshalSDP(req.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -640,7 +640,8 @@ func (c *Conn) Handle() (err error) {
|
||||
_ = track.WriteRTP(packet)
|
||||
//return fmt.Errorf("wrong channelID: %d", channelID)
|
||||
} else {
|
||||
panic("wrong channelID")
|
||||
continue // TODO: maybe fix this
|
||||
//panic("wrong channelID")
|
||||
}
|
||||
} else {
|
||||
msg := &RTCP{Channel: channelID}
|
||||
@@ -726,17 +727,35 @@ type RTCP struct {
|
||||
Packets []rtcp.Packet
|
||||
}
|
||||
|
||||
func between(s, sub1, sub2 string) (res string, ok1 bool, ok2 bool) {
|
||||
i := strings.Index(s, sub1)
|
||||
if i >= 0 {
|
||||
ok1 = true
|
||||
s = s[i+len(sub1):]
|
||||
const sdpHeader = `v=0
|
||||
o=- 0 0 IN IP4 0.0.0.0
|
||||
s=-
|
||||
t=0 0`
|
||||
|
||||
func UnmarshalSDP(rawSDP []byte) ([]*streamer.Media, error) {
|
||||
medias, err := streamer.UnmarshalSDP(rawSDP)
|
||||
if err != nil {
|
||||
// fix SDP header for some cameras
|
||||
i := bytes.Index(rawSDP, []byte("\nm="))
|
||||
if i > 0 {
|
||||
rawSDP = append([]byte(sdpHeader), rawSDP[i:]...)
|
||||
medias, err = streamer.UnmarshalSDP(rawSDP)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
i = strings.Index(s, sub2)
|
||||
if i >= 0 {
|
||||
return s[:i], ok1, true
|
||||
// fix bug in ONVIF spec
|
||||
// https://www.onvif.org/specs/stream/ONVIF-Streaming-Spec-v241.pdf
|
||||
for _, media := range medias {
|
||||
switch media.Direction {
|
||||
case streamer.DirectionRecvonly, "":
|
||||
media.Direction = streamer.DirectionSendonly
|
||||
case streamer.DirectionSendonly:
|
||||
media.Direction = streamer.DirectionRecvonly
|
||||
}
|
||||
}
|
||||
|
||||
return s, ok1, false
|
||||
return medias, nil
|
||||
}
|
||||
|
||||
@@ -180,26 +180,6 @@ func UnmarshalSDP(rawSDP []byte) ([]*Media, error) {
|
||||
return medias, nil
|
||||
}
|
||||
|
||||
func UnmarshalRTSPSDP(rawSDP []byte) ([]*Media, error) {
|
||||
medias, err := UnmarshalSDP(rawSDP)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// fix bug in ONVIF spec
|
||||
// https://www.onvif.org/specs/stream/ONVIF-Streaming-Spec-v241.pdf
|
||||
for _, media := range medias {
|
||||
switch media.Direction {
|
||||
case DirectionRecvonly, "":
|
||||
media.Direction = DirectionSendonly
|
||||
case DirectionSendonly:
|
||||
media.Direction = DirectionRecvonly
|
||||
}
|
||||
}
|
||||
|
||||
return medias, nil
|
||||
}
|
||||
|
||||
func MarshalSDP(medias []*Media) ([]byte, error) {
|
||||
sd := &sdp.SessionDescription{}
|
||||
|
||||
|
||||
@@ -80,6 +80,12 @@ func (a *Auth) Write(req *Request) {
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Auth) Reset() {
|
||||
if a.Method == AuthDigest {
|
||||
a.Method = AuthUnknown
|
||||
}
|
||||
}
|
||||
|
||||
func Between(s, sub1, sub2 string) string {
|
||||
i := strings.Index(s, sub1)
|
||||
if i < 0 {
|
||||
|
||||
@@ -47,10 +47,13 @@ func ReadResponse(r *bufio.Reader) (*Response, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if line == "" {
|
||||
return nil, errors.New("empty response on RTSP request")
|
||||
}
|
||||
|
||||
ss := strings.SplitN(line, " ", 3)
|
||||
if len(ss) != 3 {
|
||||
return nil, errors.New("malformed response")
|
||||
return nil, fmt.Errorf("malformed response: %s", line)
|
||||
}
|
||||
|
||||
res := &Response{
|
||||
|
||||
+20
-20
@@ -1,6 +1,7 @@
|
||||
package webrtc
|
||||
|
||||
import (
|
||||
"github.com/pion/ice/v2"
|
||||
"github.com/pion/interceptor"
|
||||
"github.com/pion/webrtc/v3"
|
||||
"net"
|
||||
@@ -21,31 +22,30 @@ func NewAPI(address string) (*webrtc.API, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if address == "" {
|
||||
return webrtc.NewAPI(
|
||||
webrtc.WithMediaEngine(m),
|
||||
webrtc.WithInterceptorRegistry(i),
|
||||
), nil
|
||||
}
|
||||
|
||||
ln, err := net.Listen("tcp", address)
|
||||
if err != nil {
|
||||
return webrtc.NewAPI(
|
||||
webrtc.WithMediaEngine(m),
|
||||
webrtc.WithInterceptorRegistry(i),
|
||||
), err
|
||||
}
|
||||
|
||||
s := webrtc.SettingEngine{
|
||||
//LoggerFactory: customLoggerFactory{},
|
||||
}
|
||||
s.SetNetworkTypes([]webrtc.NetworkType{
|
||||
webrtc.NetworkTypeUDP4, webrtc.NetworkTypeUDP6,
|
||||
webrtc.NetworkTypeTCP4, webrtc.NetworkTypeTCP6,
|
||||
|
||||
// disable listen on Hassio docker interfaces
|
||||
s.SetInterfaceFilter(func(name string) bool {
|
||||
return name != "hassio" && name != "docker0"
|
||||
})
|
||||
|
||||
tcpMux := webrtc.NewICETCPMux(nil, ln, 8)
|
||||
s.SetICETCPMux(tcpMux)
|
||||
// disable mDNS listener
|
||||
s.SetICEMulticastDNSMode(ice.MulticastDNSModeDisabled)
|
||||
|
||||
if address != "" {
|
||||
ln, err := net.Listen("tcp", address)
|
||||
if err == nil {
|
||||
s.SetNetworkTypes([]webrtc.NetworkType{
|
||||
webrtc.NetworkTypeUDP4, webrtc.NetworkTypeUDP6,
|
||||
webrtc.NetworkTypeTCP4, webrtc.NetworkTypeTCP6,
|
||||
})
|
||||
|
||||
tcpMux := webrtc.NewICETCPMux(nil, ln, 8)
|
||||
s.SetICETCPMux(tcpMux)
|
||||
}
|
||||
}
|
||||
|
||||
return webrtc.NewAPI(
|
||||
webrtc.WithMediaEngine(m),
|
||||
|
||||
@@ -2,11 +2,18 @@
|
||||
|
||||
- UPX-3.96 pack broken bin for `linux_mipsel`
|
||||
- UPX-3.95 pack broken bin for `mac_amd64`
|
||||
- UPX windows pack is recognised by anti-viruses as malicious
|
||||
- `aarch64` = `arm64`
|
||||
- `armv7` = `arm`
|
||||
|
||||
## Virus
|
||||
|
||||
- https://go.dev/doc/faq#virus
|
||||
- https://groups.google.com/g/golang-nuts/c/lPwiWYaApSU
|
||||
|
||||
## Useful links
|
||||
|
||||
- https://github.com/golang/go/wiki/GoArm
|
||||
- https://gist.github.com/asukakenji/f15ba7e588ac42795f421b48b8aede63
|
||||
- https://en.wikipedia.org/wiki/AArch64
|
||||
- https://stackoverflow.com/questions/22267189/what-does-the-w-flag-mean-when-passed-in-via-the-ldflags-option-to-the-go-comman
|
||||
|
||||
+4
-4
@@ -2,13 +2,13 @@
|
||||
|
||||
@SET GOOS=windows
|
||||
@SET GOARCH=amd64
|
||||
@SET FILENAME=go2rtc_win64.exe
|
||||
go build -ldflags "-s -w" -trimpath -o %FILENAME% && upx-3.96 %FILENAME%
|
||||
@SET FILENAME=go2rtc_win64.zip
|
||||
go build -ldflags "-s -w" -trimpath && 7z a -sdel %FILENAME% go2rtc.exe
|
||||
|
||||
@SET GOOS=windows
|
||||
@SET GOARCH=386
|
||||
@SET FILENAME=go2rtc_win32.exe
|
||||
go build -ldflags "-s -w" -trimpath -o %FILENAME% && upx-3.96 %FILENAME%
|
||||
@SET FILENAME=go2rtc_win32.zip
|
||||
go build -ldflags "-s -w" -trimpath && 7z a -sdel %FILENAME% go2rtc.exe
|
||||
|
||||
@SET GOOS=linux
|
||||
@SET GOARCH=amd64
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
@ECHO OFF
|
||||
@SET GOOS=linux
|
||||
@SET GOARCH=mipsle
|
||||
cd ..
|
||||
go build -ldflags "-s -w" -trimpath && upx-3.95 go2rtc
|
||||
@@ -0,0 +1,4 @@
|
||||
@SET GOOS=windows
|
||||
@SET GOARCH=amd64
|
||||
cd ..
|
||||
go build -ldflags "-w -s" -trimpath
|
||||
+3
-3
@@ -63,9 +63,9 @@
|
||||
);
|
||||
|
||||
const links = [
|
||||
'<a href="webrtc.html?url={name}">webrtc</a>',
|
||||
'<a href="mse.html?url={name}">mse</a>',
|
||||
'<a href="api/frame.mp4?url={name}">frame.mp4</a>',
|
||||
'<a href="webrtc.html?src={name}">webrtc</a>',
|
||||
'<a href="mse.html?src={name}">mse</a>',
|
||||
'<a href="api/frame.mp4?src={name}">frame.mp4</a>',
|
||||
'<a href="api/streams?src={name}">info</a>',
|
||||
];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user