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.
|
||||
|
||||
- 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
|
||||
- on the fly transcoding for unsupported codecs [via FFmpeg](#source-ffmpeg)
|
||||
- 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 or SSH-tunnels](#module-webrtc)
|
||||
- streaming from private networks via [Ngrok](#module-webrtc)
|
||||
|
||||
**Inspired by:**
|
||||
|
||||
- [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
|
||||
@@ -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/):
|
||||
|
||||
@@ -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_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
|
||||
|
||||
@@ -69,14 +103,14 @@ 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:
|
||||
|
||||
- [streams](#module-streams)
|
||||
- [api](#module-api) - HTTP API (important for WebRTC 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)
|
||||
- [ffmpeg](#source-ffmpeg) - FFmpeg integration
|
||||
- [hass](#module-hass) - Home Assistant integration
|
||||
@@ -84,7 +118,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 link as stream source.
|
||||
|
||||
Available source types:
|
||||
|
||||
@@ -94,6 +128,8 @@ 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
|
||||
@@ -202,7 +238,7 @@ streams:
|
||||
|
||||
### 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 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
|
||||
api:
|
||||
listen: ":1984" # HTTP API port ("" - disabled)
|
||||
base_path: "" # API prefix for serve on suburl
|
||||
static_dir: "www" # folder for static files ("" - disabled)
|
||||
listen: ":1984" # HTTP API port ("" - disabled)
|
||||
base_path: "" # API prefix for serve on suburl
|
||||
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
|
||||
|
||||
You can get any stream as RTSP-stream with codecs filter:
|
||||
@@ -236,9 +276,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**
|
||||
@@ -353,14 +393,24 @@ tunnels:
|
||||
|
||||
### 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:
|
||||
- Hass > Settings > Integrations > Add Integration > RTSPtoWebRTC > `http://192.168.1.123:1984/`
|
||||
- add generic camera with RTSP link:
|
||||
- Hass > Settings > Integrations > Add Integration > Generic Camera > `rtsp://...`
|
||||
- use Picture Entity or Picture Glance lovelace card
|
||||
- open full screen card - this is should be WebRTC stream
|
||||
If you install **go2rtc** as [Hass Add-on](#go2rtc-home-assistant-add-on) - you need to use localhost IP-address, example:
|
||||
|
||||
- `http://127.0.0.1:1984/` to web interface
|
||||
- `rtsp://127.0.0.1:8554/camera1` to RTSP streams
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -9,8 +9,6 @@ import (
|
||||
"github.com/rs/zerolog"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
@@ -39,9 +37,7 @@ 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
|
||||
@@ -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) {
|
||||
ctx := new(Context)
|
||||
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 (
|
||||
"bytes"
|
||||
+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
|
||||
|
||||
@@ -19,6 +19,9 @@ func HandleFunc(scheme string, handler Handler) {
|
||||
|
||||
func HasProducer(url string) bool {
|
||||
i := strings.IndexByte(url, ':')
|
||||
if i <= 0 { // TODO: i < 4 ?
|
||||
return false
|
||||
}
|
||||
return handlers[url[:i]] != 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"
|
||||
)
|
||||
|
||||
@@ -61,6 +62,10 @@ func (s *Stream) AddConsumer(cons streamer.Consumer) (err error) {
|
||||
|
||||
// Step 4. Get producer track
|
||||
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
|
||||
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
|
||||
if len(consumer.tracks) == 0 {
|
||||
return nil
|
||||
return errors.New("couldn't find the matching tracks")
|
||||
}
|
||||
|
||||
s.consumers = append(s.consumers, consumer)
|
||||
@@ -121,7 +126,7 @@ func (s *Stream) RemoveProducer(prod streamer.Producer) {
|
||||
}
|
||||
|
||||
func (s *Stream) Active() bool {
|
||||
if len(s.consumers) > 0{
|
||||
if len(s.consumers) > 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
+32
-2
@@ -3,9 +3,12 @@ package rtmp
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/deepch/vdk/av"
|
||||
"github.com/deepch/vdk/codec/aacparser"
|
||||
"github.com/deepch/vdk/codec/h264parser"
|
||||
"github.com/deepch/vdk/format/rtmp"
|
||||
"github.com/pion/rtp"
|
||||
@@ -70,9 +73,36 @@ func (c *Client) Dial() (err error) {
|
||||
c.tracks = append(c.tracks, track)
|
||||
|
||||
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:
|
||||
panic("unsupported codec")
|
||||
fmt.Printf("[rtmp] unsupported codec %+v\n", stream)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+81
-25
@@ -43,11 +43,15 @@ const (
|
||||
ModeServerConsumer
|
||||
)
|
||||
|
||||
const KeepAlive = time.Second * 25
|
||||
|
||||
type Conn struct {
|
||||
streamer.Element
|
||||
|
||||
// public
|
||||
|
||||
Backchannel bool
|
||||
|
||||
Medias []*streamer.Media
|
||||
Session string
|
||||
UserAgent string
|
||||
@@ -104,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,
|
||||
@@ -144,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)
|
||||
|
||||
@@ -189,7 +198,7 @@ func (c *Conn) Do(req *tcp.Request) (*tcp.Response, error) {
|
||||
}
|
||||
|
||||
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
|
||||
@@ -254,23 +263,27 @@ 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 {
|
||||
return err
|
||||
}
|
||||
|
||||
// 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] = '_'
|
||||
if val := res.Header.Get("Content-Base"); val != "" {
|
||||
c.URL, err = url.Parse(val)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
c.Medias, err = streamer.UnmarshalRTSPSDP(res.Body)
|
||||
c.Medias, err = UnmarshalSDP(res.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -355,10 +368,23 @@ func (c *Conn) SetupMedia(
|
||||
// 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;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, ok1, ok2 := between(s, "RTP/AVP/TCP;unicast;interleaved=", "-")
|
||||
if !ok1 || !ok2 {
|
||||
panic("wrong response")
|
||||
// TODO: rewrite
|
||||
if !strings.HasPrefix(s, "RTP/AVP/TCP;") {
|
||||
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)
|
||||
@@ -449,7 +475,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
|
||||
}
|
||||
@@ -549,6 +575,7 @@ func (c *Conn) Handle() (err error) {
|
||||
}()
|
||||
|
||||
//c.Fire(streamer.StatePlaying)
|
||||
ts := time.Now().Add(KeepAlive)
|
||||
|
||||
for {
|
||||
// we can read:
|
||||
@@ -603,7 +630,7 @@ func (c *Conn) Handle() (err error) {
|
||||
if channelID&1 == 0 {
|
||||
packet := &rtp.Packet{}
|
||||
if err = packet.Unmarshal(buf); err != nil {
|
||||
return errors.New("wrong RTP data")
|
||||
return
|
||||
}
|
||||
|
||||
track := c.channels[channelID]
|
||||
@@ -617,16 +644,27 @@ func (c *Conn) Handle() (err error) {
|
||||
msg := &RTCP{Channel: channelID}
|
||||
|
||||
if err = msg.Header.Unmarshal(buf); err != nil {
|
||||
return errors.New("wrong RTCP data")
|
||||
return
|
||||
}
|
||||
|
||||
msg.Packets, err = rtcp.Unmarshal(buf)
|
||||
if err != nil {
|
||||
return errors.New("wrong RTCP data")
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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{}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package streamer
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/pion/rtp"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type WriterFunc func(packet *rtp.Packet) error
|
||||
@@ -12,6 +13,7 @@ type Track struct {
|
||||
Codec *Codec
|
||||
Direction string
|
||||
Sink map[*Track]WriterFunc
|
||||
mx sync.Mutex
|
||||
}
|
||||
|
||||
func (t *Track) String() string {
|
||||
@@ -21,9 +23,11 @@ func (t *Track) String() string {
|
||||
}
|
||||
|
||||
func (t *Track) WriteRTP(p *rtp.Packet) error {
|
||||
t.mx.Lock()
|
||||
for _, f := range t.Sink {
|
||||
_ = f(p)
|
||||
}
|
||||
t.mx.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -35,10 +39,14 @@ func (t *Track) Bind(w WriterFunc) *Track {
|
||||
clone := &Track{
|
||||
Codec: t.Codec, Direction: t.Direction, Sink: t.Sink,
|
||||
}
|
||||
t.mx.Lock()
|
||||
t.Sink[clone] = w
|
||||
t.mx.Unlock()
|
||||
return clone
|
||||
}
|
||||
|
||||
func (t *Track) Unbind() {
|
||||
t.mx.Lock()
|
||||
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 {
|
||||
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{
|
||||
|
||||
+3
-1
@@ -130,7 +130,9 @@ func (c *Conn) GetCompleteAnswer() (answer string, err error) {
|
||||
func (c *Conn) remote() string {
|
||||
for _, trans := range c.Conn.GetTransceivers() {
|
||||
pair, _ := trans.Receiver().Transport().ICETransport().GetSelectedCandidatePair()
|
||||
return pair.Remote.String()
|
||||
if pair.Remote != nil {
|
||||
return pair.Remote.String()
|
||||
}
|
||||
}
|
||||
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