Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 945b486fe0 | |||
| d72d7b089c | |||
| d339fbe712 | |||
| 3aeb278c47 | |||
| c92c1fc3e9 | |||
| def57119f4 | |||
| b20275d2b5 | |||
| a11ca1da6e | |||
| 0fb7132947 | |||
| 0f9e3c97c5 | |||
| e049a17216 | |||
| 217c8c2bf6 |
@@ -5,11 +5,10 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg
|
|||||||

|

|
||||||
|
|
||||||
- zero-dependency and zero-config [small app](#go2rtc-binary) for all OS (Windows, macOS, Linux, ARM)
|
- zero-dependency and zero-config [small app](#go2rtc-binary) for all OS (Windows, macOS, Linux, ARM)
|
||||||
- zero-delay for all supported protocols (lowest possible streaming latency)
|
- zero-delay for many supported protocols (lowest possible streaming latency)
|
||||||
- streaming from [RTSP](#source-rtsp), [RTMP](#source-rtmp), [MJPEG](#source-ffmpeg), [HLS](#source-ffmpeg), [USB Cameras](#source-ffmpeg-device), [files](#source-ffmpeg) and [other sources](#module-streams)
|
- streaming from [RTSP](#source-rtsp), [RTMP](#source-rtmp), [MJPEG](#source-ffmpeg), [HLS](#source-ffmpeg), [USB Cameras](#source-ffmpeg-device), [files](#source-ffmpeg) and [other sources](#module-streams)
|
||||||
- streaming to [RTSP](#module-rtsp), [WebRTC](#module-webrtc) or [MSE](#module-api)
|
- streaming to [RTSP](#module-rtsp), [WebRTC](#module-webrtc) or [MSE](#module-mp4)
|
||||||
- first project in the World with support streaming from [HomeKit Cameras](#source-homekit)
|
- first project in the World with support streaming from [HomeKit Cameras](#source-homekit)
|
||||||
- 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)
|
||||||
- mixing tracks from different sources to single stream
|
- mixing tracks from different sources to single stream
|
||||||
@@ -104,7 +103,7 @@ Don't forget to fix the rights `chmod +x go2rtc_xxx_xxx` on Linux and Mac.
|
|||||||
|
|
||||||
### go2rtc: Docker
|
### 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.
|
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), [Ngrok](#module-ngrok) and [Python](#source-echo).
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
services:
|
services:
|
||||||
@@ -148,6 +147,7 @@ Available source types:
|
|||||||
- [ffmpeg](#source-ffmpeg) - FFmpeg integration (`MJPEG`, `HLS`, `files` and source types)
|
- [ffmpeg](#source-ffmpeg) - FFmpeg integration (`MJPEG`, `HLS`, `files` and source types)
|
||||||
- [ffmpeg:device](#source-ffmpeg-device) - local USB Camera or Webcam
|
- [ffmpeg:device](#source-ffmpeg-device) - local USB Camera or Webcam
|
||||||
- [exec](#source-exec) - advanced FFmpeg and GStreamer integration
|
- [exec](#source-exec) - advanced FFmpeg and GStreamer integration
|
||||||
|
- [echo](#source-echo) - get stream link via bash or python
|
||||||
- [homekit](#source-homekit) - streaming from HomeKit Camera
|
- [homekit](#source-homekit) - streaming from HomeKit Camera
|
||||||
- [hass](#source-hass) - Home Assistant integration
|
- [hass](#source-hass) - Home Assistant integration
|
||||||
|
|
||||||
@@ -254,6 +254,19 @@ streams:
|
|||||||
stream1: exec:ffmpeg -hide_banner -re -stream_loop -1 -i /media/BigBuckBunny.mp4 -c copy -rtsp_transport tcp -f rtsp {output}
|
stream1: exec:ffmpeg -hide_banner -re -stream_loop -1 -i /media/BigBuckBunny.mp4 -c copy -rtsp_transport tcp -f rtsp {output}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Source: Echo
|
||||||
|
|
||||||
|
Some sources may have a dynamic link. And you will need to get it using a bash or python script. Your script should echo a link to the source. RTSP, FFmpeg or any of the [supported sources](#module-streams).
|
||||||
|
|
||||||
|
**Docker** and **Hass Add-on** users has preinstalled `python3`, `curl`, `jq`.
|
||||||
|
|
||||||
|
Check examples in [wiki](https://github.com/AlexxIT/go2rtc/wiki/Source-Echo-examples).
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
streams:
|
||||||
|
apple_hls: echo:python3 hls.py https://developer.apple.com/streaming/examples/basic-stream-osx-ios5.html
|
||||||
|
```
|
||||||
|
|
||||||
#### Source: HomeKit
|
#### Source: HomeKit
|
||||||
|
|
||||||
**Important:**
|
**Important:**
|
||||||
@@ -463,6 +476,14 @@ In other cases you need to use IP-address of server with **go2rtc** application.
|
|||||||
|
|
||||||
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.
|
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: MP4
|
||||||
|
|
||||||
|
Provides several features:
|
||||||
|
|
||||||
|
1. MSE stream (fMP4 over WebSocket)
|
||||||
|
2. Camera snapshots in MP4 format (single frame), can be sent to [Telegram](https://www.telegram.org/)
|
||||||
|
3. Progressive MP4 stream - bad format for streaming because of high latency, doesn't work in Safari
|
||||||
|
|
||||||
### Module: Log
|
### Module: Log
|
||||||
|
|
||||||
You can set different log levels for different modules.
|
You can set different log levels for different modules.
|
||||||
|
|||||||
+23
-6
@@ -1,23 +1,40 @@
|
|||||||
ARG BUILD_FROM
|
ARG BUILD_FROM
|
||||||
FROM $BUILD_FROM
|
|
||||||
|
|
||||||
RUN apk add --no-cache git go ffmpeg
|
FROM $BUILD_FROM as build
|
||||||
|
|
||||||
ARG BUILD_ARCH
|
# 1. Build go2rtc
|
||||||
|
RUN apk add --no-cache git go
|
||||||
|
|
||||||
RUN git clone https://github.com/AlexxIT/go2rtc \
|
RUN git clone https://github.com/AlexxIT/go2rtc \
|
||||||
&& cd go2rtc \
|
&& cd go2rtc \
|
||||||
&& CGO_ENABLED=0 go build -ldflags "-s -w" -trimpath -o /usr/local/bin
|
&& CGO_ENABLED=0 go build -ldflags "-s -w" -trimpath
|
||||||
|
|
||||||
|
# 2. Download ngrok
|
||||||
|
ARG BUILD_ARCH
|
||||||
|
|
||||||
# https://github.com/home-assistant/docker-base/blob/master/alpine/Dockerfile
|
# https://github.com/home-assistant/docker-base/blob/master/alpine/Dockerfile
|
||||||
RUN if [ "${BUILD_ARCH}" = "aarch64" ]; then BUILD_ARCH="arm64"; \
|
RUN if [ "${BUILD_ARCH}" = "aarch64" ]; then BUILD_ARCH="arm64"; \
|
||||||
elif [ "${BUILD_ARCH}" = "armv7" ]; then BUILD_ARCH="arm"; fi \
|
elif [ "${BUILD_ARCH}" = "armv7" ]; then BUILD_ARCH="arm"; fi \
|
||||||
&& cd go2rtc \
|
&& cd go2rtc \
|
||||||
&& curl $(curl -s "https://raw.githubusercontent.com/ngrok/docker-ngrok/main/releases.json" | jq -r ".${BUILD_ARCH}.url") -o ngrok.zip \
|
&& curl $(curl -s "https://raw.githubusercontent.com/ngrok/docker-ngrok/main/releases.json" | jq -r ".${BUILD_ARCH}.url") -o ngrok.zip \
|
||||||
&& unzip ngrok -d /usr/local/bin
|
&& unzip ngrok
|
||||||
|
|
||||||
RUN rm -r /go2rtc
|
|
||||||
|
|
||||||
|
|
||||||
|
# https://devopscube.com/reduce-docker-image-size/
|
||||||
|
FROM $BUILD_FROM
|
||||||
|
|
||||||
|
# 3. Copy go2rtc and ngrok to release
|
||||||
|
COPY --from=build /go2rtc/go2rtc /usr/local/bin
|
||||||
|
COPY --from=build /go2rtc/ngrok /usr/local/bin
|
||||||
|
|
||||||
|
# 4. Install ffmpeg
|
||||||
|
# apk base OK: 22 MiB in 40 packages
|
||||||
|
# ffmpeg OK: 113 MiB in 110 packages
|
||||||
|
# python3 OK: 161 MiB in 114 packages
|
||||||
|
RUN apk add --no-cache ffmpeg python3
|
||||||
|
|
||||||
|
# 5. Copy run to release
|
||||||
COPY run.sh /
|
COPY run.sh /
|
||||||
RUN chmod a+x /run.sh
|
RUN chmod a+x /run.sh
|
||||||
|
|
||||||
|
|||||||
+3
-13
@@ -2,23 +2,13 @@
|
|||||||
|
|
||||||
set +e
|
set +e
|
||||||
|
|
||||||
# add the feature for update to any version
|
# set cwd for go2rtc (for config file, Hass integration, etc)
|
||||||
if [ -f "/config/go2rtc.version" ]; then
|
|
||||||
branch=`cat /config/go2rtc.version`
|
|
||||||
echo "Update to version $branch"
|
|
||||||
git clone --depth 1 --branch "$branch" https://github.com/AlexxIT/go2rtc \
|
|
||||||
&& cd go2rtc \
|
|
||||||
&& CGO_ENABLED=0 go build -ldflags "-s -w" -trimpath -o /usr/local/bin \
|
|
||||||
&& rm -r /go2rtc && rm /config/go2rtc.version
|
|
||||||
fi
|
|
||||||
|
|
||||||
# set cwd for go2rtc (for config file, Hass itegration, etc)
|
|
||||||
cd /config
|
cd /config
|
||||||
|
|
||||||
# add the feature to override go2rtc binary from Hass config folder
|
# add the feature to override go2rtc binary from Hass config folder
|
||||||
export PATH="/config:$PATH"
|
export PATH="/config:$PATH"
|
||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
go2rtc
|
go2rtc
|
||||||
sleep 5
|
sleep 5
|
||||||
done
|
done
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package echo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"github.com/AlexxIT/go2rtc/cmd/app"
|
||||||
|
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/shell"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||||
|
"os/exec"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Init() {
|
||||||
|
log := app.GetLogger("echo")
|
||||||
|
|
||||||
|
streams.HandleFunc("echo", func(url string) (streamer.Producer, error) {
|
||||||
|
args := shell.QuoteSplit(url[5:])
|
||||||
|
|
||||||
|
b, err := exec.Command(args[0], args[1:]...).Output()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
b = bytes.TrimSpace(b)
|
||||||
|
|
||||||
|
log.Debug().Str("url", url).Msgf("[echo] %s", b)
|
||||||
|
|
||||||
|
return streams.GetProducer(string(b))
|
||||||
|
})
|
||||||
|
}
|
||||||
+2
-37
@@ -8,6 +8,7 @@ import (
|
|||||||
"github.com/AlexxIT/go2rtc/cmd/rtsp"
|
"github.com/AlexxIT/go2rtc/cmd/rtsp"
|
||||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||||
pkg "github.com/AlexxIT/go2rtc/pkg/rtsp"
|
pkg "github.com/AlexxIT/go2rtc/pkg/rtsp"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/shell"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"os"
|
"os"
|
||||||
@@ -49,7 +50,7 @@ func Handle(url string) (streamer.Producer, error) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
// remove `exec:`
|
// remove `exec:`
|
||||||
args := QuoteSplit(url[5:])
|
args := shell.QuoteSplit(url[5:])
|
||||||
cmd := exec.Command(args[0], args[1:]...)
|
cmd := exec.Command(args[0], args[1:]...)
|
||||||
|
|
||||||
if log.Trace().Enabled() {
|
if log.Trace().Enabled() {
|
||||||
@@ -86,39 +87,3 @@ func Handle(url string) (streamer.Producer, error) {
|
|||||||
|
|
||||||
var log zerolog.Logger
|
var log zerolog.Logger
|
||||||
var waiters map[string]chan streamer.Producer
|
var waiters map[string]chan streamer.Producer
|
||||||
|
|
||||||
func QuoteSplit(s string) []string {
|
|
||||||
var a []string
|
|
||||||
|
|
||||||
for len(s) > 0 {
|
|
||||||
is := strings.IndexByte(s, ' ')
|
|
||||||
if is >= 0 {
|
|
||||||
// skip prefix and double spaces
|
|
||||||
if is == 0 {
|
|
||||||
// goto next symbol
|
|
||||||
s = s[1:]
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if quote in word
|
|
||||||
if i := strings.IndexByte(s[:is], '"'); i >= 0 {
|
|
||||||
// search quote end
|
|
||||||
if is = strings.Index(s, `" `); is > 0 {
|
|
||||||
is += 1
|
|
||||||
} else {
|
|
||||||
is = -1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if is >= 0 {
|
|
||||||
a = append(a, strings.ReplaceAll(s[:is], `"`, ""))
|
|
||||||
s = s[is+1:]
|
|
||||||
} else {
|
|
||||||
//add last word
|
|
||||||
a = append(a, s)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return a
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package ivideon
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/ivideon"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Init() {
|
||||||
|
streams.HandleFunc("ivideon", func(url string) (streamer.Producer, error) {
|
||||||
|
id := strings.Replace(url[8:], "/", ":", 1)
|
||||||
|
prod := ivideon.NewClient(id)
|
||||||
|
if err := prod.Dial(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return prod, nil
|
||||||
|
})
|
||||||
|
}
|
||||||
+14
-5
@@ -48,13 +48,17 @@ func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defer stream.RemoveConsumer(cons)
|
||||||
|
|
||||||
w.Header().Set("Content-Type", cons.MimeType())
|
w.Header().Set("Content-Type", cons.MimeType())
|
||||||
|
|
||||||
data := cons.Init()
|
data, err := cons.Init()
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("[api.keyframe] init")
|
||||||
|
return
|
||||||
|
}
|
||||||
data = append(data, <-exit...)
|
data = append(data, <-exit...)
|
||||||
|
|
||||||
stream.RemoveConsumer(cons)
|
|
||||||
|
|
||||||
// Apple Safari won't show frame without length
|
// Apple Safari won't show frame without length
|
||||||
w.Header().Set("Content-Length", strconv.Itoa(len(data)))
|
w.Header().Set("Content-Length", strconv.Itoa(len(data)))
|
||||||
|
|
||||||
@@ -97,8 +101,13 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
w.Header().Set("Content-Type", cons.MimeType())
|
w.Header().Set("Content-Type", cons.MimeType())
|
||||||
|
|
||||||
data := cons.Init()
|
data, err := cons.Init()
|
||||||
if _, err := w.Write(data); err != nil {
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("[api.mp4] init")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err = w.Write(data); err != nil {
|
||||||
log.Error().Err(err).Msg("[api.mp4] write")
|
log.Error().Err(err).Msg("[api.mp4] write")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-1
@@ -41,5 +41,12 @@ func handlerWS(ctx *api.Context, msg *streamer.Message) {
|
|||||||
Type: MsgTypeMSE, Value: cons.MimeType(),
|
Type: MsgTypeMSE, Value: cons.MimeType(),
|
||||||
})
|
})
|
||||||
|
|
||||||
ctx.Write(cons.Init())
|
data, err := cons.Init()
|
||||||
|
if err != nil {
|
||||||
|
log.Warn().Err(err).Msg("[api.mse] init")
|
||||||
|
ctx.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Write(data)
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-1
@@ -8,6 +8,7 @@ import (
|
|||||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"net"
|
"net"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
@@ -47,6 +48,15 @@ var OnProducer func(conn streamer.Producer) bool // TODO: maybe rewrite...
|
|||||||
var log zerolog.Logger
|
var log zerolog.Logger
|
||||||
|
|
||||||
func rtspHandler(url string) (streamer.Producer, error) {
|
func rtspHandler(url string) (streamer.Producer, error) {
|
||||||
|
backchannel := true
|
||||||
|
|
||||||
|
if i := strings.IndexByte(url, '#'); i > 0 {
|
||||||
|
if url[i+1:] == "backchannel=0" {
|
||||||
|
backchannel = false
|
||||||
|
}
|
||||||
|
url = url[:i]
|
||||||
|
}
|
||||||
|
|
||||||
conn, err := rtsp.NewClient(url)
|
conn, err := rtsp.NewClient(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -67,8 +77,12 @@ func rtspHandler(url string) (streamer.Producer, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
conn.Backchannel = true
|
conn.Backchannel = backchannel
|
||||||
if err = conn.Describe(); err != nil {
|
if err = conn.Describe(); err != nil {
|
||||||
|
if !backchannel {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
// second try without backchannel, we need to reconnect
|
// second try without backchannel, we need to reconnect
|
||||||
if err = conn.Dial(); err != nil {
|
if err = conn.Dial(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -79,7 +79,11 @@ func (p *Producer) start() {
|
|||||||
log.Debug().Str("url", p.url).Msg("[streams] start producer")
|
log.Debug().Str("url", p.url).Msg("[streams] start producer")
|
||||||
|
|
||||||
p.state = stateStart
|
p.state = stateStart
|
||||||
go p.element.Start()
|
go func() {
|
||||||
|
if err := p.element.Start(); err != nil {
|
||||||
|
log.Warn().Err(err).Str("url", p.url).Msg("[streams] start")
|
||||||
|
}
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Producer) stop() {
|
func (p *Producer) stop() {
|
||||||
|
|||||||
@@ -4,10 +4,12 @@ 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/debug"
|
||||||
|
"github.com/AlexxIT/go2rtc/cmd/echo"
|
||||||
"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"
|
||||||
"github.com/AlexxIT/go2rtc/cmd/homekit"
|
"github.com/AlexxIT/go2rtc/cmd/homekit"
|
||||||
|
"github.com/AlexxIT/go2rtc/cmd/ivideon"
|
||||||
"github.com/AlexxIT/go2rtc/cmd/mp4"
|
"github.com/AlexxIT/go2rtc/cmd/mp4"
|
||||||
"github.com/AlexxIT/go2rtc/cmd/ngrok"
|
"github.com/AlexxIT/go2rtc/cmd/ngrok"
|
||||||
"github.com/AlexxIT/go2rtc/cmd/rtmp"
|
"github.com/AlexxIT/go2rtc/cmd/rtmp"
|
||||||
@@ -24,6 +26,8 @@ func main() {
|
|||||||
app.Init() // init config and logs
|
app.Init() // init config and logs
|
||||||
streams.Init() // load streams list
|
streams.Init() // load streams list
|
||||||
|
|
||||||
|
echo.Init()
|
||||||
|
|
||||||
rtsp.Init() // add support RTSP client and RTSP server
|
rtsp.Init() // add support RTSP client and RTSP server
|
||||||
rtmp.Init() // add support RTMP client
|
rtmp.Init() // add support RTMP client
|
||||||
exec.Init() // add support exec scheme (depends on RTSP server)
|
exec.Init() // add support exec scheme (depends on RTSP server)
|
||||||
@@ -38,6 +42,8 @@ func main() {
|
|||||||
srtp.Init()
|
srtp.Init()
|
||||||
homekit.Init()
|
homekit.Init()
|
||||||
|
|
||||||
|
ivideon.Init()
|
||||||
|
|
||||||
ngrok.Init()
|
ngrok.Init()
|
||||||
debug.Init()
|
debug.Init()
|
||||||
|
|
||||||
|
|||||||
@@ -58,3 +58,21 @@ func RepairAVC(track *streamer.Track) streamer.WrapperFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func SplitAVC(data []byte) [][]byte {
|
||||||
|
var nals [][]byte
|
||||||
|
for {
|
||||||
|
// get AVC length
|
||||||
|
size := int(binary.BigEndian.Uint32(data))
|
||||||
|
|
||||||
|
// check if multiple items in one packet
|
||||||
|
if size+4 < len(data) {
|
||||||
|
nals = append(nals, data[:size+4])
|
||||||
|
data = data[size+4:]
|
||||||
|
} else {
|
||||||
|
nals = append(nals, data)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nals
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,286 @@
|
|||||||
|
package ivideon
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/binary"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||||
|
"github.com/deepch/vdk/codec/h264parser"
|
||||||
|
"github.com/deepch/vdk/format/fmp4/fmp4io"
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
"github.com/pion/rtp"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
streamer.Element
|
||||||
|
|
||||||
|
ID string
|
||||||
|
|
||||||
|
conn *websocket.Conn
|
||||||
|
medias []*streamer.Media
|
||||||
|
tracks map[byte]*streamer.Track
|
||||||
|
|
||||||
|
closed bool
|
||||||
|
|
||||||
|
msg *message
|
||||||
|
t0 time.Time
|
||||||
|
|
||||||
|
buffer chan []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClient(id string) *Client {
|
||||||
|
return &Client{ID: id}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Dial() (err error) {
|
||||||
|
resp, err := http.Get(
|
||||||
|
"https://openapi-alpha.ivideon.com/cameras/" + c.ID +
|
||||||
|
"/live_stream?op=GET&access_token=public&q=2&" +
|
||||||
|
"video_codecs=h264&format=ws-fmp4",
|
||||||
|
)
|
||||||
|
|
||||||
|
data, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var v liveResponse
|
||||||
|
if err = json.Unmarshal(data, &v); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !v.Success {
|
||||||
|
return fmt.Errorf("wrong response: %s", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.conn, _, err = websocket.DefaultDialer.Dial(v.Result.URL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = c.getTracks(); err != nil {
|
||||||
|
_ = c.conn.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Handle() error {
|
||||||
|
c.buffer = make(chan []byte, 5)
|
||||||
|
// add delay to the stream for smooth playing (not a best solution)
|
||||||
|
c.t0 = time.Now().Add(time.Second)
|
||||||
|
|
||||||
|
// processing stream in separate thread for lower delay between packets
|
||||||
|
go c.worker()
|
||||||
|
|
||||||
|
_, data, err := c.conn.ReadMessage()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
track := c.tracks[c.msg.Track]
|
||||||
|
if track != nil {
|
||||||
|
c.buffer <- data
|
||||||
|
}
|
||||||
|
|
||||||
|
// we have one unprocessed msg after getTracks
|
||||||
|
for {
|
||||||
|
_, data, err = c.conn.ReadMessage()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var msg message
|
||||||
|
if err = json.Unmarshal(data, &msg); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch msg.Type {
|
||||||
|
case "stream-init":
|
||||||
|
continue
|
||||||
|
|
||||||
|
case "fragment":
|
||||||
|
_, data, err = c.conn.ReadMessage()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
track = c.tracks[msg.Track]
|
||||||
|
if track != nil {
|
||||||
|
c.buffer <- data
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("wrong message type: %s", data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Close() error {
|
||||||
|
if c.conn == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
close(c.buffer)
|
||||||
|
c.closed = true
|
||||||
|
return c.conn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) getTracks() error {
|
||||||
|
c.tracks = map[byte]*streamer.Track{}
|
||||||
|
|
||||||
|
for {
|
||||||
|
_, data, err := c.conn.ReadMessage()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var msg message
|
||||||
|
if err = json.Unmarshal(data, &msg); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch msg.Type {
|
||||||
|
case "stream-init":
|
||||||
|
s := msg.CodecString
|
||||||
|
i := strings.IndexByte(s, '.')
|
||||||
|
if i > 0 {
|
||||||
|
s = s[:i]
|
||||||
|
}
|
||||||
|
|
||||||
|
switch s {
|
||||||
|
case "avc1": // avc1.4d0029
|
||||||
|
// skip multiple identical init
|
||||||
|
if c.tracks[msg.TrackID] != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
codec := streamer.NewCodec(streamer.CodecH264)
|
||||||
|
codec.FmtpLine = "profile-level-id=" + msg.CodecString[i+1:]
|
||||||
|
codec.PayloadType = h264.PayloadTypeAVC
|
||||||
|
|
||||||
|
i = bytes.Index(msg.Data, []byte("avcC")) - 4
|
||||||
|
if i < 0 {
|
||||||
|
return fmt.Errorf("wrong AVC: %s", msg.Data)
|
||||||
|
}
|
||||||
|
|
||||||
|
avccLen := binary.BigEndian.Uint32(msg.Data[i:])
|
||||||
|
data = msg.Data[i+8 : i+int(avccLen)]
|
||||||
|
|
||||||
|
record := h264parser.AVCDecoderConfRecord{}
|
||||||
|
if _, err = record.Unmarshal(data); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
codec.FmtpLine += ";sprop-parameter-sets=" +
|
||||||
|
base64.StdEncoding.EncodeToString(record.SPS[0]) + "," +
|
||||||
|
base64.StdEncoding.EncodeToString(record.PPS[0])
|
||||||
|
|
||||||
|
media := &streamer.Media{
|
||||||
|
Kind: streamer.KindVideo,
|
||||||
|
Direction: streamer.DirectionSendonly,
|
||||||
|
Codecs: []*streamer.Codec{codec},
|
||||||
|
}
|
||||||
|
c.medias = append(c.medias, media)
|
||||||
|
|
||||||
|
track := &streamer.Track{
|
||||||
|
Direction: streamer.DirectionSendonly,
|
||||||
|
Codec: codec,
|
||||||
|
}
|
||||||
|
c.tracks[msg.TrackID] = track
|
||||||
|
|
||||||
|
case "mp4a": // mp4a.40.2
|
||||||
|
}
|
||||||
|
|
||||||
|
case "fragment":
|
||||||
|
c.msg = &msg
|
||||||
|
return nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("wrong message type: %s", data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) worker() {
|
||||||
|
var track *streamer.Track
|
||||||
|
for _, track = range c.tracks {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
for data := range c.buffer {
|
||||||
|
moof := &fmp4io.MovieFrag{}
|
||||||
|
if _, err := moof.Unmarshal(data, 0); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
moofLen := binary.BigEndian.Uint32(data)
|
||||||
|
_ = moofLen
|
||||||
|
|
||||||
|
mdat := moof.Unknowns[0]
|
||||||
|
if mdat.Tag() != fmp4io.MDAT {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
i, _ := mdat.Pos() // offset, size
|
||||||
|
data = data[i+8:]
|
||||||
|
|
||||||
|
traf := moof.Tracks[0]
|
||||||
|
ts := uint32(traf.DecodeTime.Time)
|
||||||
|
|
||||||
|
//println("!!!", (time.Duration(ts) * time.Millisecond).String(), time.Since(c.t0).String())
|
||||||
|
|
||||||
|
for _, entry := range traf.Run.Entries {
|
||||||
|
// synchronize framerate for WebRTC and MSE
|
||||||
|
d := time.Duration(ts)*time.Millisecond - time.Since(c.t0)
|
||||||
|
if d < 0 {
|
||||||
|
d = time.Duration(entry.Duration) * time.Millisecond / 2
|
||||||
|
}
|
||||||
|
time.Sleep(d)
|
||||||
|
|
||||||
|
// can be SPS, PPS and IFrame in one packet
|
||||||
|
for _, payload := range h264.SplitAVC(data[:entry.Size]) {
|
||||||
|
packet := &rtp.Packet{
|
||||||
|
// ivideon clockrate=1000, RTP clockrate=90000
|
||||||
|
Header: rtp.Header{Timestamp: ts * 90},
|
||||||
|
Payload: payload,
|
||||||
|
}
|
||||||
|
_ = track.WriteRTP(packet)
|
||||||
|
}
|
||||||
|
|
||||||
|
data = data[entry.Size:]
|
||||||
|
ts += entry.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(data) != 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type liveResponse struct {
|
||||||
|
Result struct {
|
||||||
|
URL string `json:"url"`
|
||||||
|
} `json:"result"`
|
||||||
|
Success bool `json:"success"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type message struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
|
||||||
|
CodecString string `json:"codec_string"`
|
||||||
|
Data []byte `json:"data"`
|
||||||
|
TrackID byte `json:"track_id"`
|
||||||
|
|
||||||
|
Track byte `json:"track"`
|
||||||
|
StartTime float32 `json:"start_time"`
|
||||||
|
Duration float32 `json:"duration"`
|
||||||
|
IsKey bool `json:"is_key"`
|
||||||
|
DataOffset uint32 `json:"data_offset"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package ivideon
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *Client) GetMedias() []*streamer.Media {
|
||||||
|
return c.medias
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.Track {
|
||||||
|
for _, track := range c.tracks {
|
||||||
|
if track.Codec == codec {
|
||||||
|
return track
|
||||||
|
}
|
||||||
|
}
|
||||||
|
panic(fmt.Sprintf("wrong media/codec: %+v %+v", media, codec))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Start() error {
|
||||||
|
err := c.Handle()
|
||||||
|
if c.closed {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Stop() error {
|
||||||
|
return c.Close()
|
||||||
|
}
|
||||||
+1
-1
@@ -86,7 +86,7 @@ func (c *Consumer) MimeType() string {
|
|||||||
return c.muxer.MimeType(c.codecs)
|
return c.muxer.MimeType(c.codecs)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Consumer) Init() []byte {
|
func (c *Consumer) Init() ([]byte, error) {
|
||||||
if c.muxer == nil {
|
if c.muxer == nil {
|
||||||
c.muxer = &Muxer{}
|
c.muxer = &Muxer{}
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-5
@@ -2,6 +2,7 @@ package mp4
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
|
"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/codec/h264parser"
|
"github.com/deepch/vdk/codec/h264parser"
|
||||||
@@ -32,18 +33,21 @@ func (m *Muxer) MimeType(codecs []*streamer.Codec) string {
|
|||||||
return s + `"`
|
return s + `"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Muxer) GetInit(codecs []*streamer.Codec) []byte {
|
func (m *Muxer) GetInit(codecs []*streamer.Codec) ([]byte, error) {
|
||||||
moov := MOOV()
|
moov := MOOV()
|
||||||
|
|
||||||
for _, codec := range codecs {
|
for _, codec := range codecs {
|
||||||
switch codec.Name {
|
switch codec.Name {
|
||||||
case streamer.CodecH264:
|
case streamer.CodecH264:
|
||||||
sps, pps := h264.GetParameterSet(codec.FmtpLine)
|
sps, pps := h264.GetParameterSet(codec.FmtpLine)
|
||||||
|
if sps == nil {
|
||||||
|
return nil, fmt.Errorf("empty SPS: %#v", codec)
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: remove
|
// TODO: remove
|
||||||
codecData, err := h264parser.NewCodecDataFromSPSAndPPS(sps, pps)
|
codecData, err := h264parser.NewCodecDataFromSPSAndPPS(sps, pps)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
width := codecData.Width()
|
width := codecData.Width()
|
||||||
@@ -83,7 +87,7 @@ func (m *Muxer) GetInit(codecs []*streamer.Codec) []byte {
|
|||||||
data := make([]byte, moov.Len())
|
data := make([]byte, moov.Len())
|
||||||
moov.Marshal(data)
|
moov.Marshal(data)
|
||||||
|
|
||||||
return append(FTYP(), data...)
|
return append(FTYP(), data...), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Muxer) Rewind() {
|
func (m *Muxer) Rewind() {
|
||||||
@@ -121,13 +125,15 @@ func (m *Muxer) Marshal(packet *rtp.Packet) []byte {
|
|||||||
}
|
}
|
||||||
|
|
||||||
entry := mp4io.TrackFragRunEntry{
|
entry := mp4io.TrackFragRunEntry{
|
||||||
Duration: 90000,
|
//Duration: 90000,
|
||||||
Size: uint32(len(packet.Payload)),
|
Size: uint32(len(packet.Payload)),
|
||||||
}
|
}
|
||||||
|
|
||||||
newTime := packet.Timestamp
|
newTime := packet.Timestamp
|
||||||
if m.pts > 0 {
|
if m.pts > 0 {
|
||||||
m.dts += uint64(newTime - m.pts)
|
//m.dts += uint64(newTime - m.pts)
|
||||||
|
entry.Duration = newTime - m.pts
|
||||||
|
m.dts += uint64(entry.Duration)
|
||||||
}
|
}
|
||||||
m.pts = newTime
|
m.pts = newTime
|
||||||
|
|
||||||
|
|||||||
+1
-20
@@ -2,7 +2,6 @@ package rtmp
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/binary"
|
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/h264"
|
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||||
@@ -134,7 +133,7 @@ func (c *Client) Handle() (err error) {
|
|||||||
|
|
||||||
var payloads [][]byte
|
var payloads [][]byte
|
||||||
if track.Codec.Name == streamer.CodecH264 {
|
if track.Codec.Name == streamer.CodecH264 {
|
||||||
payloads = splitAVC(pkt.Data)
|
payloads = h264.SplitAVC(pkt.Data)
|
||||||
} else {
|
} else {
|
||||||
payloads = [][]byte{pkt.Data}
|
payloads = [][]byte{pkt.Data}
|
||||||
}
|
}
|
||||||
@@ -156,21 +155,3 @@ func (c *Client) Close() error {
|
|||||||
c.closed = true
|
c.closed = true
|
||||||
return c.conn.Close()
|
return c.conn.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func splitAVC(data []byte) [][]byte {
|
|
||||||
var nals [][]byte
|
|
||||||
for {
|
|
||||||
// get AVC length
|
|
||||||
size := int(binary.BigEndian.Uint32(data))
|
|
||||||
|
|
||||||
// check if multiple items in one packet
|
|
||||||
if size+4 < len(data) {
|
|
||||||
nals = append(nals, data[:size+4])
|
|
||||||
data = data[size+4:]
|
|
||||||
} else {
|
|
||||||
nals = append(nals, data)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nals
|
|
||||||
}
|
|
||||||
|
|||||||
+38
-21
@@ -61,10 +61,10 @@ type Conn struct {
|
|||||||
|
|
||||||
auth *tcp.Auth
|
auth *tcp.Auth
|
||||||
conn net.Conn
|
conn net.Conn
|
||||||
|
mode Mode
|
||||||
reader *bufio.Reader
|
reader *bufio.Reader
|
||||||
sequence int
|
sequence int
|
||||||
|
uri string
|
||||||
mode Mode
|
|
||||||
|
|
||||||
tracks []*streamer.Track
|
tracks []*streamer.Track
|
||||||
channels map[byte]*streamer.Track
|
channels map[byte]*streamer.Track
|
||||||
@@ -76,24 +76,10 @@ type Conn struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewClient(uri string) (*Conn, error) {
|
func NewClient(uri string) (*Conn, error) {
|
||||||
var err error
|
|
||||||
|
|
||||||
c := new(Conn)
|
c := new(Conn)
|
||||||
c.URL, err = url.Parse(uri)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.IndexByte(c.URL.Host, ':') < 0 {
|
|
||||||
c.URL.Host += ":554"
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove UserInfo from URL
|
|
||||||
c.auth = tcp.NewAuth(c.URL.User)
|
|
||||||
c.mode = ModeClientProducer
|
c.mode = ModeClientProducer
|
||||||
c.URL.User = nil
|
c.uri = uri
|
||||||
|
return c, c.parseURI()
|
||||||
return c, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewServer(conn net.Conn) *Conn {
|
func NewServer(conn net.Conn) *Conn {
|
||||||
@@ -104,12 +90,29 @@ func NewServer(conn net.Conn) *Conn {
|
|||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Conn) parseURI() (err error) {
|
||||||
|
c.URL, err = url.Parse(c.uri)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.IndexByte(c.URL.Host, ':') < 0 {
|
||||||
|
c.URL.Host += ":554"
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove UserInfo from URL
|
||||||
|
c.auth = tcp.NewAuth(c.URL.User)
|
||||||
|
c.URL.User = nil
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Conn) Dial() (err error) {
|
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 {
|
if c.conn != nil {
|
||||||
c.auth.Reset()
|
_ = c.parseURI()
|
||||||
}
|
}
|
||||||
|
|
||||||
c.conn, err = net.DialTimeout(
|
c.conn, err = net.DialTimeout(
|
||||||
@@ -359,7 +362,21 @@ func (c *Conn) SetupMedia(
|
|||||||
var res *tcp.Response
|
var res *tcp.Response
|
||||||
res, err = c.Do(req)
|
res, err = c.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
// Dahua VTO2111D fail on this step because of backchannel
|
||||||
|
if c.Backchannel {
|
||||||
|
if err = c.Dial(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
c.Backchannel = false
|
||||||
|
if err = c.Describe(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
res, err = c.Do(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.Session == "" {
|
if c.Session == "" {
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package shell
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func QuoteSplit(s string) []string {
|
||||||
|
var a []string
|
||||||
|
|
||||||
|
for len(s) > 0 {
|
||||||
|
is := strings.IndexByte(s, ' ')
|
||||||
|
if is >= 0 {
|
||||||
|
// skip prefix and double spaces
|
||||||
|
if is == 0 {
|
||||||
|
// goto next symbol
|
||||||
|
s = s[1:]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if quote in word
|
||||||
|
if i := strings.IndexByte(s[:is], '"'); i >= 0 {
|
||||||
|
// search quote end
|
||||||
|
if is = strings.Index(s, `" `); is > 0 {
|
||||||
|
is += 1
|
||||||
|
} else {
|
||||||
|
is = -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if is >= 0 {
|
||||||
|
a = append(a, strings.ReplaceAll(s[:is], `"`, ""))
|
||||||
|
s = s[is+1:]
|
||||||
|
} else {
|
||||||
|
//add last word
|
||||||
|
a = append(a, s)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return a
|
||||||
|
}
|
||||||
@@ -80,12 +80,6 @@ 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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user