Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 12b712426d | |||
| a9af245ef8 | |||
| f251129a2f | |||
| d28debabe9 | |||
| 07bf00f9f6 | |||
| be6ec7dbb9 | |||
| 4e575d1356 | |||
| 4cbacfec0c | |||
| 31e24c6e03 | |||
| 401bf85a10 | |||
| f36851f83a | |||
| 67522dbb19 | |||
| 26b5745f0a | |||
| 46f6a5d8e1 | |||
| 48f58d0669 | |||
| fd0b8f3c39 | |||
| 863bf503e2 | |||
| 7a3a1a5336 | |||
| b851041caa | |||
| a4acde6d95 | |||
| 1139d4fcad | |||
| 159ad52277 | |||
| 87bc07e404 |
@@ -2,19 +2,19 @@
|
|||||||
|
|
||||||
**go2rtc** - ultimate camera streaming application with support RTSP, WebRTC, FFmpeg, RTMP, etc.
|
**go2rtc** - 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-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)
|
- on the fly transcoding for unsupported codecs via [FFmpeg](#source-ffmpeg)
|
||||||
- multi-source 2-way [codecs negotiation](#codecs-negotiation)
|
- multi-source 2-way [codecs negotiation](#codecs-negotiation)
|
||||||
- streaming from private networks via [Ngrok or SSH-tunnels](#module-webrtc)
|
- streaming from private networks via [Ngrok](#module-webrtc)
|
||||||
|
|
||||||
**Inspired by:**
|
**Inspired by:**
|
||||||
|
|
||||||
- [webrtc](https://github.com/pion/webrtc) go library and whole [@pion](https://github.com/pion) team
|
- [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)
|
- 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)
|
- [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
|
- [MediaSoup](https://mediasoup.org/) framework routing idea
|
||||||
|
|
||||||
## Codecs negotiation
|
## Codecs negotiation
|
||||||
@@ -45,7 +45,24 @@ streams:
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Installation
|
## Fast start
|
||||||
|
|
||||||
|
1. Download [binary](#go2rtc-binary) or use [Docker](#go2rtc-docker) or [Home Assistant Add-on](#go2rtc-home-assistant-add-on)
|
||||||
|
2. Open web interface [http://localhost:1984/](http://localhost:1984/)
|
||||||
|
|
||||||
|
**Optionally:**
|
||||||
|
|
||||||
|
- add your [streams](#module-streams) to [config](#configuration) file
|
||||||
|
- setup [external access](#module-webrtc) to webrtc
|
||||||
|
- setup [external access](#module-ngrok) to web interface
|
||||||
|
- install [ffmpeg](#source-ffmpeg) for transcoding
|
||||||
|
|
||||||
|
**Developers:**
|
||||||
|
|
||||||
|
- write your own [web interface](#module-api)
|
||||||
|
- integrate [web api](#module-api) into your smart home platform
|
||||||
|
|
||||||
|
### go2rtc: Binary
|
||||||
|
|
||||||
Download binary for your OS from [latest release](https://github.com/AlexxIT/go2rtc/releases/):
|
Download binary for your OS from [latest release](https://github.com/AlexxIT/go2rtc/releases/):
|
||||||
|
|
||||||
@@ -59,7 +76,24 @@ Download binary for your OS from [latest release](https://github.com/AlexxIT/go2
|
|||||||
- `go2rtc_mac_amd64` - Mac with Intel
|
- `go2rtc_mac_amd64` - Mac with Intel
|
||||||
- `go2rtc_mac_arm64` - Mac with M1
|
- `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
|
||||||
|
|
||||||
|
[](https://my.home-assistant.io/redirect/supervisor_addon/?addon=a889bffc_go2rtc&repository_url=https%3A%2F%2Fgithub.com%2FAlexxIT%2Fhassio-addons)
|
||||||
|
|
||||||
|
1. Install Add-On:
|
||||||
|
- Settings > Add-ons > Plus > Repositories > Add `https://github.com/AlexxIT/hassio-addons`
|
||||||
|
- go2rtc > Install > Start
|
||||||
|
2. Setup [Integration](#module-hass)
|
||||||
|
|
||||||
|
**Optionally:**
|
||||||
|
|
||||||
|
- create `go2rtc.yaml` in your Home Assistant [config](https://www.home-assistant.io/docs/configuration) folder
|
||||||
|
|
||||||
|
### go2rtc: Docker
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
@@ -69,14 +103,14 @@ Create file `go2rtc.yaml` next to the app.
|
|||||||
- `api` server will start on default **1984 port**
|
- `api` server will start on default **1984 port**
|
||||||
- `rtsp` server will start on default **8554 port**
|
- `rtsp` server will start on default **8554 port**
|
||||||
- `webrtc` will use random UDP port for each connection
|
- `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:
|
Available modules:
|
||||||
|
|
||||||
- [streams](#module-streams)
|
- [streams](#module-streams)
|
||||||
- [api](#module-api) - HTTP API (important for WebRTC support)
|
- [api](#module-api) - HTTP API (important for WebRTC support)
|
||||||
- [rtsp](#module-rtsp) - RTSP Server (important for FFmpeg support)
|
- [rtsp](#module-rtsp) - RTSP Server (important for FFmpeg support)
|
||||||
- [webrtc](#module-webrtc) - WebRTC Server (important for external access)
|
- [webrtc](#module-webrtc) - WebRTC Server
|
||||||
- [ngrok](#module-ngrok) - Ngrok integration (external access for private network)
|
- [ngrok](#module-ngrok) - Ngrok integration (external access for private network)
|
||||||
- [ffmpeg](#source-ffmpeg) - FFmpeg integration
|
- [ffmpeg](#source-ffmpeg) - FFmpeg integration
|
||||||
- [hass](#module-hass) - Home Assistant integration
|
- [hass](#module-hass) - Home Assistant integration
|
||||||
@@ -84,7 +118,7 @@ Available modules:
|
|||||||
|
|
||||||
### Module: Streams
|
### 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 link as stream source.
|
||||||
|
|
||||||
Available source types:
|
Available source types:
|
||||||
|
|
||||||
@@ -94,6 +128,8 @@ Available source types:
|
|||||||
- [exec](#source-exec) - advanced FFmpeg and GStreamer integration
|
- [exec](#source-exec) - advanced FFmpeg and GStreamer integration
|
||||||
- [hass](#source-hass) - Home Assistant integration
|
- [hass](#source-hass) - Home Assistant integration
|
||||||
|
|
||||||
|
**PS.** You can use sources like `MJPEG`, `HLS` and others via FFmpeg integration.
|
||||||
|
|
||||||
#### Source: RTSP
|
#### Source: RTSP
|
||||||
|
|
||||||
- Support **RTSP and RTSPS** links with multiple video and audio tracks
|
- Support **RTSP and RTSPS** links with multiple video and audio tracks
|
||||||
@@ -202,7 +238,7 @@ streams:
|
|||||||
|
|
||||||
### Module: API
|
### Module: API
|
||||||
|
|
||||||
The HTTP API is the main part for interacting with the application.
|
The HTTP API is the main part for interacting with the application. Default address: `http://127.0.0.1:1984/`.
|
||||||
|
|
||||||
- you can use WebRTC only when HTTP API enabled
|
- you can use WebRTC only when HTTP API enabled
|
||||||
- you can disable HTTP API with `listen: ""` and use, for example, only RTSP client/server protocol
|
- you can disable HTTP API with `listen: ""` and use, for example, only RTSP client/server protocol
|
||||||
@@ -212,11 +248,15 @@ The HTTP API is the main part for interacting with the application.
|
|||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
api:
|
api:
|
||||||
listen: ":1984" # HTTP API port ("" - disabled)
|
listen: ":1984" # HTTP API port ("" - disabled)
|
||||||
base_path: "" # API prefix for serve on suburl
|
base_path: "" # API prefix for serve on suburl
|
||||||
static_dir: "www" # folder for static files ("" - disabled)
|
static_dir: "" # folder for static files (custom web interface)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**PS. go2rtc** don't provide HTTPS or password protection. Use [Nginx](https://nginx.org/) or [Ngrok](#module-ngrok) or [Home Assistant Add-on](#go2rtc-home-assistant-add-on) for this tasks.
|
||||||
|
|
||||||
|
**PS2.** You can access microphone (for 2-way audio) only with HTTPS
|
||||||
|
|
||||||
### Module: RTSP
|
### Module: RTSP
|
||||||
|
|
||||||
You can get any stream as RTSP-stream with codecs filter:
|
You can get any stream as RTSP-stream with codecs filter:
|
||||||
@@ -236,9 +276,9 @@ rtsp:
|
|||||||
|
|
||||||
### Module: WebRTC
|
### 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
|
- you can enable one additional TCP port for all connections and use it for external access
|
||||||
|
|
||||||
**Static public IP**
|
**Static public IP**
|
||||||
@@ -353,14 +393,24 @@ tunnels:
|
|||||||
|
|
||||||
### Module: Hass
|
### Module: Hass
|
||||||
|
|
||||||
go2rtc compatible with Home Assistant [RTSPtoWebRTC](https://www.home-assistant.io/integrations/rtsp_to_webrtc/) integration API.
|
**go2rtc** compatible with Home Assistant [RTSPtoWebRTC](https://www.home-assistant.io/integrations/rtsp_to_webrtc/) integration.
|
||||||
|
|
||||||
- add integration with link to go2rtc HTTP API:
|
If you install **go2rtc** as [Hass Add-on](#go2rtc-home-assistant-add-on) - you need to use localhost IP-address, example:
|
||||||
- Hass > Settings > Integrations > Add Integration > RTSPtoWebRTC > `http://192.168.1.123:1984/`
|
|
||||||
- add generic camera with RTSP link:
|
- `http://127.0.0.1:1984/` to web interface
|
||||||
- Hass > Settings > Integrations > Add Integration > Generic Camera > `rtsp://...`
|
- `rtsp://127.0.0.1:8554/camera1` to RTSP streams
|
||||||
- use Picture Entity or Picture Glance lovelace card
|
|
||||||
- open full screen card - this is should be WebRTC stream
|
In other cases you need to use IP-address of server with **go2rtc** application.
|
||||||
|
|
||||||
|
1. Add integration with link to go2rtc HTTP API:
|
||||||
|
- Hass > Settings > Integrations > Add Integration > [RTSPtoWebRTC](https://my.home-assistant.io/redirect/config_flow_start/?domain=rtsp_to_webrtc) > `http://127.0.0.1:1984/`
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
### Module: Log
|
||||||
|
|
||||||
|
|||||||
@@ -9,8 +9,6 @@ import (
|
|||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
@@ -39,9 +37,7 @@ func Init() {
|
|||||||
|
|
||||||
HandleFunc("/api/frame.mp4", frameHandler)
|
HandleFunc("/api/frame.mp4", frameHandler)
|
||||||
HandleFunc("/api/frame.raw", frameHandler)
|
HandleFunc("/api/frame.raw", frameHandler)
|
||||||
HandleFunc("/api/stack", stackHandler)
|
|
||||||
HandleFunc("/api/streams", streamsHandler)
|
HandleFunc("/api/streams", streamsHandler)
|
||||||
HandleFunc("/api/exit", exitHandler)
|
|
||||||
HandleFunc("/api/ws", apiWS)
|
HandleFunc("/api/ws", apiWS)
|
||||||
|
|
||||||
// ensure we can listen without errors
|
// ensure we can listen without errors
|
||||||
@@ -99,12 +95,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) {
|
func apiWS(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := new(Context)
|
ctx := new(Context)
|
||||||
if err := ctx.Upgrade(w, r); err != nil {
|
if err := ctx.Upgrade(w, r); err != nil {
|
||||||
|
|||||||
@@ -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 (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
+10
-1
@@ -65,8 +65,17 @@ func rtspHandler(url string) (streamer.Producer, error) {
|
|||||||
if err = conn.Dial(); err != nil {
|
if err = conn.Dial(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
conn.Backchannel = true
|
||||||
if err = conn.Describe(); err != nil {
|
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
|
return conn, nil
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ func HandleFunc(scheme string, handler Handler) {
|
|||||||
|
|
||||||
func HasProducer(url string) bool {
|
func HasProducer(url string) bool {
|
||||||
i := strings.IndexByte(url, ':')
|
i := strings.IndexByte(url, ':')
|
||||||
|
if i <= 0 { // TODO: i < 4 ?
|
||||||
|
return false
|
||||||
|
}
|
||||||
return handlers[url[:i]] != nil
|
return handlers[url[:i]] != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+22
-3
@@ -2,6 +2,7 @@ package streams
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||||
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
type state byte
|
type state byte
|
||||||
@@ -21,15 +22,19 @@ type Producer struct {
|
|||||||
tracks []*streamer.Track
|
tracks []*streamer.Track
|
||||||
|
|
||||||
state state
|
state state
|
||||||
|
mx sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Producer) GetMedias() []*streamer.Media {
|
func (p *Producer) GetMedias() []*streamer.Media {
|
||||||
|
p.mx.Lock()
|
||||||
|
defer p.mx.Unlock()
|
||||||
|
|
||||||
if p.state == stateNone {
|
if p.state == stateNone {
|
||||||
log.Debug().Str("url", p.url).Msg("[streams] probe producer")
|
log.Debug().Str("url", p.url).Msg("[streams] probe producer")
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
p.element, err = GetProducer(p.url)
|
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")
|
log.Error().Err(err).Str("url", p.url).Msg("[streams] probe producer")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -41,6 +46,9 @@ func (p *Producer) GetMedias() []*streamer.Media {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *Producer) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.Track {
|
func (p *Producer) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.Track {
|
||||||
|
p.mx.Lock()
|
||||||
|
defer p.mx.Unlock()
|
||||||
|
|
||||||
if p.state == stateMedias {
|
if p.state == stateMedias {
|
||||||
p.state = stateTracks
|
p.state = stateTracks
|
||||||
}
|
}
|
||||||
@@ -61,6 +69,9 @@ func (p *Producer) GetTrack(media *streamer.Media, codec *streamer.Codec) *strea
|
|||||||
// internals
|
// internals
|
||||||
|
|
||||||
func (p *Producer) start() {
|
func (p *Producer) start() {
|
||||||
|
p.mx.Lock()
|
||||||
|
defer p.mx.Unlock()
|
||||||
|
|
||||||
if p.state != stateTracks {
|
if p.state != stateTracks {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -72,10 +83,18 @@ func (p *Producer) start() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *Producer) stop() {
|
func (p *Producer) stop() {
|
||||||
|
p.mx.Lock()
|
||||||
|
|
||||||
log.Debug().Str("url", p.url).Msg("[streams] stop producer")
|
log.Debug().Str("url", p.url).Msg("[streams] stop producer")
|
||||||
|
|
||||||
_ = p.element.Stop()
|
if p.element != nil {
|
||||||
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.tracks = nil
|
||||||
p.state = stateNone
|
p.state = stateNone
|
||||||
|
|
||||||
|
p.mx.Unlock()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package streams
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -61,6 +62,10 @@ func (s *Stream) AddConsumer(cons streamer.Consumer) (err error) {
|
|||||||
|
|
||||||
// Step 4. Get producer track
|
// Step 4. Get producer track
|
||||||
prodTrack := prod.GetTrack(prodMedia, prodCodec)
|
prodTrack := prod.GetTrack(prodMedia, prodCodec)
|
||||||
|
if prodTrack == nil {
|
||||||
|
log.Warn().Msg("[stream] can't get track")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
// Step 5. Add track to consumer and get new track
|
// Step 5. Add track to consumer and get new track
|
||||||
consTrack := consumer.element.AddTrack(consMedia, prodTrack)
|
consTrack := consumer.element.AddTrack(consMedia, prodTrack)
|
||||||
@@ -74,7 +79,7 @@ func (s *Stream) AddConsumer(cons streamer.Consumer) (err error) {
|
|||||||
|
|
||||||
// can't match tracks for consumer
|
// can't match tracks for consumer
|
||||||
if len(consumer.tracks) == 0 {
|
if len(consumer.tracks) == 0 {
|
||||||
return nil
|
return errors.New("couldn't find the matching tracks")
|
||||||
}
|
}
|
||||||
|
|
||||||
s.consumers = append(s.consumers, consumer)
|
s.consumers = append(s.consumers, consumer)
|
||||||
@@ -121,7 +126,7 @@ func (s *Stream) RemoveProducer(prod streamer.Producer) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Stream) Active() bool {
|
func (s *Stream) Active() bool {
|
||||||
if len(s.consumers) > 0{
|
if len(s.consumers) > 0 {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package streams
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/AlexxIT/go2rtc/pkg/fake"
|
"github.com/AlexxIT/go2rtc/pkg/fake"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/rtsp"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -103,7 +104,7 @@ a=control:streamid=0
|
|||||||
|
|
||||||
func TestRouting(t *testing.T) {
|
func TestRouting(t *testing.T) {
|
||||||
prod := &fake.Producer{}
|
prod := &fake.Producer{}
|
||||||
prod.Medias, _ = streamer.UnmarshalRTSPSDP([]byte(dahuaSimple))
|
prod.Medias, _ = rtsp.UnmarshalSDP([]byte(dahuaSimple))
|
||||||
assert.Len(t, prod.Medias, 3)
|
assert.Len(t, prod.Medias, 3)
|
||||||
|
|
||||||
HandleFunc("fake", func(url string) (streamer.Producer, error) {
|
HandleFunc("fake", func(url string) (streamer.Producer, error) {
|
||||||
|
|||||||
@@ -108,6 +108,7 @@ func offerHandler(ctx *api.Context, msg *streamer.Message) {
|
|||||||
// 2. AddConsumer, so we get new tracks
|
// 2. AddConsumer, so we get new tracks
|
||||||
if err = stream.AddConsumer(conn); err != nil {
|
if err = stream.AddConsumer(conn); err != nil {
|
||||||
log.Warn().Err(err).Msg("[api.webrtc] add consumer")
|
log.Warn().Err(err).Msg("[api.webrtc] add consumer")
|
||||||
|
_ = conn.Conn.Close()
|
||||||
ctx.Error(err)
|
ctx.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"github.com/AlexxIT/go2rtc/cmd/api"
|
"github.com/AlexxIT/go2rtc/cmd/api"
|
||||||
"github.com/AlexxIT/go2rtc/cmd/app"
|
"github.com/AlexxIT/go2rtc/cmd/app"
|
||||||
|
"github.com/AlexxIT/go2rtc/cmd/debug"
|
||||||
"github.com/AlexxIT/go2rtc/cmd/exec"
|
"github.com/AlexxIT/go2rtc/cmd/exec"
|
||||||
"github.com/AlexxIT/go2rtc/cmd/ffmpeg"
|
"github.com/AlexxIT/go2rtc/cmd/ffmpeg"
|
||||||
"github.com/AlexxIT/go2rtc/cmd/hass"
|
"github.com/AlexxIT/go2rtc/cmd/hass"
|
||||||
@@ -33,6 +34,7 @@ func main() {
|
|||||||
mse.Init()
|
mse.Init()
|
||||||
|
|
||||||
ngrok.Init()
|
ngrok.Init()
|
||||||
|
debug.Init()
|
||||||
|
|
||||||
sigs := make(chan os.Signal, 1)
|
sigs := make(chan os.Signal, 1)
|
||||||
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
|||||||
+32
-2
@@ -3,9 +3,12 @@ package rtmp
|
|||||||
import (
|
import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/h264"
|
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||||
"github.com/deepch/vdk/av"
|
"github.com/deepch/vdk/av"
|
||||||
|
"github.com/deepch/vdk/codec/aacparser"
|
||||||
"github.com/deepch/vdk/codec/h264parser"
|
"github.com/deepch/vdk/codec/h264parser"
|
||||||
"github.com/deepch/vdk/format/rtmp"
|
"github.com/deepch/vdk/format/rtmp"
|
||||||
"github.com/pion/rtp"
|
"github.com/pion/rtp"
|
||||||
@@ -70,9 +73,36 @@ func (c *Client) Dial() (err error) {
|
|||||||
c.tracks = append(c.tracks, track)
|
c.tracks = append(c.tracks, track)
|
||||||
|
|
||||||
case av.AAC:
|
case av.AAC:
|
||||||
panic("not implemented")
|
// TODO: fix support
|
||||||
|
cd := stream.(aacparser.CodecData)
|
||||||
|
|
||||||
|
// a=fmtp:97 streamtype=5;profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=1588
|
||||||
|
fmtp := fmt.Sprintf(
|
||||||
|
"config=%s",
|
||||||
|
hex.EncodeToString(cd.ConfigBytes),
|
||||||
|
)
|
||||||
|
|
||||||
|
codec := &streamer.Codec{
|
||||||
|
Name: streamer.CodecAAC,
|
||||||
|
ClockRate: uint32(cd.Config.SampleRate),
|
||||||
|
Channels: uint16(cd.Config.ChannelConfig),
|
||||||
|
FmtpLine: fmtp,
|
||||||
|
}
|
||||||
|
|
||||||
|
media := &streamer.Media{
|
||||||
|
Kind: streamer.KindAudio,
|
||||||
|
Direction: streamer.DirectionSendonly,
|
||||||
|
Codecs: []*streamer.Codec{codec},
|
||||||
|
}
|
||||||
|
c.medias = append(c.medias, media)
|
||||||
|
|
||||||
|
track := &streamer.Track{
|
||||||
|
Codec: codec, Direction: media.Direction,
|
||||||
|
}
|
||||||
|
c.tracks = append(c.tracks, track)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
panic("unsupported codec")
|
fmt.Printf("[rtmp] unsupported codec %+v\n", stream)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+81
-25
@@ -43,11 +43,15 @@ const (
|
|||||||
ModeServerConsumer
|
ModeServerConsumer
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const KeepAlive = time.Second * 25
|
||||||
|
|
||||||
type Conn struct {
|
type Conn struct {
|
||||||
streamer.Element
|
streamer.Element
|
||||||
|
|
||||||
// public
|
// public
|
||||||
|
|
||||||
|
Backchannel bool
|
||||||
|
|
||||||
Medias []*streamer.Media
|
Medias []*streamer.Media
|
||||||
Session string
|
Session string
|
||||||
UserAgent string
|
UserAgent string
|
||||||
@@ -104,6 +108,9 @@ func (c *Conn) Dial() (err error) {
|
|||||||
//if c.state != StateClientInit {
|
//if c.state != StateClientInit {
|
||||||
// panic("wrong state")
|
// panic("wrong state")
|
||||||
//}
|
//}
|
||||||
|
if c.conn != nil && c.auth != nil {
|
||||||
|
c.auth.Reset()
|
||||||
|
}
|
||||||
|
|
||||||
c.conn, err = net.DialTimeout(
|
c.conn, err = net.DialTimeout(
|
||||||
"tcp", c.URL.Host, 10*time.Second,
|
"tcp", c.URL.Host, 10*time.Second,
|
||||||
@@ -144,7 +151,9 @@ func (c *Conn) Request(req *tcp.Request) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
c.sequence++
|
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)
|
c.auth.Write(req)
|
||||||
|
|
||||||
@@ -189,7 +198,7 @@ func (c *Conn) Do(req *tcp.Request) (*tcp.Response, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
return nil, fmt.Errorf("wrong response on %s", req.Method)
|
return res, fmt.Errorf("wrong response on %s", req.Method)
|
||||||
}
|
}
|
||||||
|
|
||||||
return res, nil
|
return res, nil
|
||||||
@@ -254,23 +263,27 @@ func (c *Conn) Describe() error {
|
|||||||
Method: MethodDescribe,
|
Method: MethodDescribe,
|
||||||
URL: c.URL,
|
URL: c.URL,
|
||||||
Header: map[string][]string{
|
Header: map[string][]string{
|
||||||
"Accept": {"application/sdp"},
|
"Accept": {"application/sdp"},
|
||||||
"Require": {"www.onvif.org/ver20/backchannel"},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if c.Backchannel {
|
||||||
|
req.Header.Set("Require", "www.onvif.org/ver20/backchannel")
|
||||||
|
}
|
||||||
|
|
||||||
res, err := c.Do(req)
|
res, err := c.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// fix bug in Sonoff camera SDP "o=- 1 1 IN IP4 rom t_rtsplin"
|
if val := res.Header.Get("Content-Base"); val != "" {
|
||||||
// TODO: make some universal fix
|
c.URL, err = url.Parse(val)
|
||||||
if i := bytes.Index(res.Body, []byte("rom t_rtsplin")); i > 0 {
|
if err != nil {
|
||||||
res.Body[i+3] = '_'
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Medias, err = streamer.UnmarshalRTSPSDP(res.Body)
|
c.Medias, err = UnmarshalSDP(res.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -355,10 +368,23 @@ func (c *Conn) SetupMedia(
|
|||||||
// we send our `interleaved`, but camera can answer with another
|
// we send our `interleaved`, but camera can answer with another
|
||||||
|
|
||||||
// Transport: RTP/AVP/TCP;unicast;interleaved=10-11;ssrc=10117CB7
|
// 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")
|
s := res.Header.Get("Transport")
|
||||||
s, ok1, ok2 := between(s, "RTP/AVP/TCP;unicast;interleaved=", "-")
|
// TODO: rewrite
|
||||||
if !ok1 || !ok2 {
|
if !strings.HasPrefix(s, "RTP/AVP/TCP;") {
|
||||||
panic("wrong response")
|
return nil, fmt.Errorf("wrong transport: %s", s)
|
||||||
|
}
|
||||||
|
|
||||||
|
i := strings.Index(s, "interleaved=")
|
||||||
|
if i < 0 {
|
||||||
|
return nil, fmt.Errorf("wrong transport: %s", s)
|
||||||
|
}
|
||||||
|
|
||||||
|
s = s[i+len("interleaved="):]
|
||||||
|
i = strings.IndexAny(s, "-;")
|
||||||
|
if i > 0 {
|
||||||
|
s = s[:i]
|
||||||
}
|
}
|
||||||
|
|
||||||
ch, err = strconv.Atoi(s)
|
ch, err = strconv.Atoi(s)
|
||||||
@@ -449,7 +475,7 @@ func (c *Conn) Accept() error {
|
|||||||
return errors.New("wrong content type")
|
return errors.New("wrong content type")
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Medias, err = streamer.UnmarshalRTSPSDP(req.Body)
|
c.Medias, err = UnmarshalSDP(req.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -549,6 +575,7 @@ func (c *Conn) Handle() (err error) {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
//c.Fire(streamer.StatePlaying)
|
//c.Fire(streamer.StatePlaying)
|
||||||
|
ts := time.Now().Add(KeepAlive)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
// we can read:
|
// we can read:
|
||||||
@@ -603,7 +630,7 @@ func (c *Conn) Handle() (err error) {
|
|||||||
if channelID&1 == 0 {
|
if channelID&1 == 0 {
|
||||||
packet := &rtp.Packet{}
|
packet := &rtp.Packet{}
|
||||||
if err = packet.Unmarshal(buf); err != nil {
|
if err = packet.Unmarshal(buf); err != nil {
|
||||||
return errors.New("wrong RTP data")
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
track := c.channels[channelID]
|
track := c.channels[channelID]
|
||||||
@@ -617,16 +644,27 @@ func (c *Conn) Handle() (err error) {
|
|||||||
msg := &RTCP{Channel: channelID}
|
msg := &RTCP{Channel: channelID}
|
||||||
|
|
||||||
if err = msg.Header.Unmarshal(buf); err != nil {
|
if err = msg.Header.Unmarshal(buf); err != nil {
|
||||||
return errors.New("wrong RTCP data")
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
msg.Packets, err = rtcp.Unmarshal(buf)
|
msg.Packets, err = rtcp.Unmarshal(buf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.New("wrong RTCP data")
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Fire(msg)
|
c.Fire(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// keep-alive
|
||||||
|
now := time.Now()
|
||||||
|
if now.After(ts) {
|
||||||
|
req := &tcp.Request{Method: MethodOptions, URL: c.URL}
|
||||||
|
// don't need to wait respose on this request
|
||||||
|
if err = c.Request(req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ts = now.Add(KeepAlive)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -686,17 +724,35 @@ type RTCP struct {
|
|||||||
Packets []rtcp.Packet
|
Packets []rtcp.Packet
|
||||||
}
|
}
|
||||||
|
|
||||||
func between(s, sub1, sub2 string) (res string, ok1 bool, ok2 bool) {
|
const sdpHeader = `v=0
|
||||||
i := strings.Index(s, sub1)
|
o=- 0 0 IN IP4 0.0.0.0
|
||||||
if i >= 0 {
|
s=-
|
||||||
ok1 = true
|
t=0 0`
|
||||||
s = s[i+len(sub1):]
|
|
||||||
|
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)
|
// fix bug in ONVIF spec
|
||||||
if i >= 0 {
|
// https://www.onvif.org/specs/stream/ONVIF-Streaming-Spec-v241.pdf
|
||||||
return s[:i], ok1, true
|
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
|
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) {
|
func MarshalSDP(medias []*Media) ([]byte, error) {
|
||||||
sd := &sdp.SessionDescription{}
|
sd := &sdp.SessionDescription{}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package streamer
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/pion/rtp"
|
"github.com/pion/rtp"
|
||||||
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
type WriterFunc func(packet *rtp.Packet) error
|
type WriterFunc func(packet *rtp.Packet) error
|
||||||
@@ -12,6 +13,7 @@ type Track struct {
|
|||||||
Codec *Codec
|
Codec *Codec
|
||||||
Direction string
|
Direction string
|
||||||
Sink map[*Track]WriterFunc
|
Sink map[*Track]WriterFunc
|
||||||
|
mx sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Track) String() string {
|
func (t *Track) String() string {
|
||||||
@@ -21,9 +23,11 @@ func (t *Track) String() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *Track) WriteRTP(p *rtp.Packet) error {
|
func (t *Track) WriteRTP(p *rtp.Packet) error {
|
||||||
|
t.mx.Lock()
|
||||||
for _, f := range t.Sink {
|
for _, f := range t.Sink {
|
||||||
_ = f(p)
|
_ = f(p)
|
||||||
}
|
}
|
||||||
|
t.mx.Unlock()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,10 +39,14 @@ func (t *Track) Bind(w WriterFunc) *Track {
|
|||||||
clone := &Track{
|
clone := &Track{
|
||||||
Codec: t.Codec, Direction: t.Direction, Sink: t.Sink,
|
Codec: t.Codec, Direction: t.Direction, Sink: t.Sink,
|
||||||
}
|
}
|
||||||
|
t.mx.Lock()
|
||||||
t.Sink[clone] = w
|
t.Sink[clone] = w
|
||||||
|
t.mx.Unlock()
|
||||||
return clone
|
return clone
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Track) Unbind() {
|
func (t *Track) Unbind() {
|
||||||
|
t.mx.Lock()
|
||||||
delete(t.Sink, t)
|
delete(t.Sink, t)
|
||||||
|
t.mx.Unlock()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
func Between(s, sub1, sub2 string) string {
|
||||||
i := strings.Index(s, sub1)
|
i := strings.Index(s, sub1)
|
||||||
if i < 0 {
|
if i < 0 {
|
||||||
|
|||||||
@@ -47,10 +47,13 @@ func ReadResponse(r *bufio.Reader) (*Response, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if line == "" {
|
||||||
|
return nil, errors.New("empty response on RTSP request")
|
||||||
|
}
|
||||||
|
|
||||||
ss := strings.SplitN(line, " ", 3)
|
ss := strings.SplitN(line, " ", 3)
|
||||||
if len(ss) != 3 {
|
if len(ss) != 3 {
|
||||||
return nil, errors.New("malformed response")
|
return nil, fmt.Errorf("malformed response: %s", line)
|
||||||
}
|
}
|
||||||
|
|
||||||
res := &Response{
|
res := &Response{
|
||||||
|
|||||||
+3
-1
@@ -130,7 +130,9 @@ func (c *Conn) GetCompleteAnswer() (answer string, err error) {
|
|||||||
func (c *Conn) remote() string {
|
func (c *Conn) remote() string {
|
||||||
for _, trans := range c.Conn.GetTransceivers() {
|
for _, trans := range c.Conn.GetTransceivers() {
|
||||||
pair, _ := trans.Receiver().Transport().ICETransport().GetSelectedCandidatePair()
|
pair, _ := trans.Receiver().Transport().ICETransport().GetSelectedCandidatePair()
|
||||||
return pair.Remote.String()
|
if pair.Remote != nil {
|
||||||
|
return pair.Remote.String()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
@SET GOOS=windows
|
||||||
|
@SET GOARCH=amd64
|
||||||
|
cd ..
|
||||||
|
go build -ldflags "-s -w" -trimpath && upx-3.96 go2rtc.exe
|
||||||
Reference in New Issue
Block a user