mirror of
https://github.com/AlexxIT/go2rtc.git
synced 2025-09-28 05:02:11 +08:00
Compare commits
27 Commits
v0.1-beta.
...
v0.1-beta.
Author | SHA1 | Date | |
---|---|---|---|
![]() |
ce0fac959f | ||
![]() |
1b14be7033 | ||
![]() |
bbbade4097 | ||
![]() |
8f43ad2a35 | ||
![]() |
105331d50f | ||
![]() |
a45d0b507b | ||
![]() |
407ccc45bc | ||
![]() |
428628fcce | ||
![]() |
fa23bb6899 | ||
![]() |
71e1c840a7 | ||
![]() |
63b9639e86 | ||
![]() |
ae3e1372c8 | ||
![]() |
800ebb39be | ||
![]() |
3a10cb25bb | ||
![]() |
7784b0e64c | ||
![]() |
945b486fe0 | ||
![]() |
d72d7b089c | ||
![]() |
d339fbe712 | ||
![]() |
3aeb278c47 | ||
![]() |
c92c1fc3e9 | ||
![]() |
def57119f4 | ||
![]() |
b20275d2b5 | ||
![]() |
a11ca1da6e | ||
![]() |
0fb7132947 | ||
![]() |
0f9e3c97c5 | ||
![]() |
e049a17216 | ||
![]() |
217c8c2bf6 |
103
README.md
103
README.md
@@ -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/HTTP](#source-ffmpeg), [USB Cameras](#source-ffmpeg-device) 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), [MSE/MP4](#module-mp4) or [MJPEG](#module-mjpeg)
|
||||||
- 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
|
||||||
@@ -98,13 +97,9 @@ Don't forget to fix the rights `chmod +x go2rtc_xxx_xxx` on Linux and Mac.
|
|||||||
- go2rtc > Install > Start
|
- go2rtc > Install > Start
|
||||||
2. Setup [Integration](#module-hass)
|
2. Setup [Integration](#module-hass)
|
||||||
|
|
||||||
**Optionally:**
|
|
||||||
|
|
||||||
- create `go2rtc.yaml` in your Home Assistant [config](https://www.home-assistant.io/docs/configuration) folder
|
|
||||||
|
|
||||||
### 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:
|
||||||
@@ -132,8 +127,9 @@ Available modules:
|
|||||||
- [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
|
- [webrtc](#module-webrtc) - WebRTC Server
|
||||||
- [ngrok](#module-ngrok) - Ngrok integration (external access for private network)
|
- [mp4](#module-mp4) - MSE, MP4 stream and MP4 shapshot
|
||||||
- [ffmpeg](#source-ffmpeg) - FFmpeg integration
|
- [ffmpeg](#source-ffmpeg) - FFmpeg integration
|
||||||
|
- [ngrok](#module-ngrok) - Ngrok integration (external access for private network)
|
||||||
- [hass](#module-hass) - Home Assistant integration
|
- [hass](#module-hass) - Home Assistant integration
|
||||||
- [log](#module-log) - logs config
|
- [log](#module-log) - logs config
|
||||||
|
|
||||||
@@ -148,11 +144,11 @@ 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 from bash or python
|
||||||
- [homekit](#source-homekit) - streaming from HomeKit Camera
|
- [homekit](#source-homekit) - streaming from HomeKit Camera
|
||||||
|
- [ivideon](#source-ivideon) - public cameras from [Ivideon](https://tv.ivideon.com/) service
|
||||||
- [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
|
||||||
@@ -254,6 +250,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:**
|
||||||
@@ -270,6 +279,15 @@ If you see a device but it does not have a pair button - it is paired to some ec
|
|||||||
|
|
||||||
**This source is in active development!** Tested only with [Aqara Camera Hub G3](https://www.aqara.com/eu/product/camera-hub-g3) (both EU and CN versions).
|
**This source is in active development!** Tested only with [Aqara Camera Hub G3](https://www.aqara.com/eu/product/camera-hub-g3) (both EU and CN versions).
|
||||||
|
|
||||||
|
#### Source: Ivideon
|
||||||
|
|
||||||
|
Support public cameras from service [Ivideon](https://tv.ivideon.com/).
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
sources:
|
||||||
|
quailcam: ivideon:100-tu5dkUPct39cTp9oNEN2B6/0
|
||||||
|
```
|
||||||
|
|
||||||
#### Source: Hass
|
#### Source: Hass
|
||||||
|
|
||||||
Support import camera links from [Home Assistant](https://www.home-assistant.io/) config files:
|
Support import camera links from [Home Assistant](https://www.home-assistant.io/) config files:
|
||||||
@@ -309,13 +327,9 @@ api:
|
|||||||
|
|
||||||
### Module: RTSP
|
### Module: RTSP
|
||||||
|
|
||||||
You can get any stream as RTSP-stream with codecs filter:
|
You can get any stream as RTSP-stream: `rtsp://192.168.1.123:8554/{stream_name}`
|
||||||
|
|
||||||
```
|
- you can omit the codec filters, so one first video and one first audio will be selected
|
||||||
rtsp://192.168.1.123/{stream_name}?video={codec}&audio={codec1}&audio={codec2}
|
|
||||||
```
|
|
||||||
|
|
||||||
- you can omit the codecs, so one first video and one first audio will be selected
|
|
||||||
- you can set `?video=copy` or just `?video`, so only one first video without audio will be selected
|
- you can set `?video=copy` or just `?video`, so only one first video without audio will be selected
|
||||||
- you can set multiple video or audio, so all of them will be selected
|
- you can set multiple video or audio, so all of them will be selected
|
||||||
|
|
||||||
@@ -444,25 +458,54 @@ tunnels:
|
|||||||
|
|
||||||
### Module: Hass
|
### Module: Hass
|
||||||
|
|
||||||
**go2rtc** compatible with Home Assistant [RTSPtoWebRTC](https://www.home-assistant.io/integrations/rtsp_to_webrtc/) integration.
|
If you install **go2rtc** as [Hass Add-on](#go2rtc-home-assistant-add-on) - you need to use localhost IP-address. In other cases you need to use IP-address of server with **go2rtc** application.
|
||||||
|
|
||||||
If you install **go2rtc** as [Hass Add-on](#go2rtc-home-assistant-add-on) - you need to use localhost IP-address, example:
|
#### From go2rtc to Hass
|
||||||
|
|
||||||
- `http://127.0.0.1:1984/` to web interface
|
Add any supported [stream source](#module-streams) as [Generic Camera](https://www.home-assistant.io/integrations/generic/) and view stream with built-in [Stream](https://www.home-assistant.io/integrations/stream/) integration. Technology `HLS`, supported codecs: `H264`, poor latency.
|
||||||
- `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 your stream to [go2rtc config](#configuration)
|
||||||
|
2. Hass > Settings > Integrations > Add Integration > [Generic Camera](https://my.home-assistant.io/redirect/config_flow_start/?domain=generic) > `rtsp://127.0.0.1:8554/camera1`
|
||||||
|
|
||||||
1. Add integration with link to go2rtc HTTP API:
|
#### From Hass to go2rtc
|
||||||
- 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:
|
View almost any Hass camera using `WebRTC` technology, supported codecs `H264`/`PCMU`/`PCMA`/`OPUS`, best latency.
|
||||||
- Hass > Settings > Integrations > Add Integration > [Generic Camera](https://my.home-assistant.io/redirect/config_flow_start/?domain=generic) > `rtsp://...` or `rtmp://...`
|
|
||||||
- you can use either direct RTSP links to cameras or take RTSP streams from **go2rtc**
|
When the stream starts - the camera `entity_id` will be added to go2rtc "on the fly". You don't need to add cameras manually to [go2rtc config](#configuration). Some cameras (like [Nest](https://www.home-assistant.io/integrations/nest/)) have a dynamic link to the stream, it will be updated each time a stream is started from the Hass interface.
|
||||||
3. Use Picture Entity or Picture Glance lovelace card
|
|
||||||
4. Open full screen card - this is should be WebRTC stream
|
1. 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. Use Picture Entity or Picture Glance lovelace card
|
||||||
|
|
||||||
|
You can add camera `entity_id` to [go2rtc config](#configuration) if you need transcoding:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
sources:
|
||||||
|
"camera.hall": ffmpeg:{input}#video=copy#audio=opus
|
||||||
|
```
|
||||||
|
|
||||||
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: MJPEG
|
||||||
|
|
||||||
|
**Important.** For stream as MJPEG format, your source MUST contain the MJPEG codec. If your camera outputs H264/H265 - you SHOULD use transcoding. With this example, your stream will have both H264 and MJPEG codecs:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
streams:
|
||||||
|
camera1:
|
||||||
|
- rtsp://rtsp:12345678@192.168.1.123/av_stream/ch0
|
||||||
|
- ffmpeg:rtsp://rtsp:12345678@192.168.1.123/av_stream/ch0#video=mjpeg
|
||||||
|
```
|
||||||
|
|
||||||
|
Example link to MJPEG: `http://192.168.1.123:1984/api/stream.mjpeg?src=camera1`
|
||||||
|
|
||||||
### Module: Log
|
### Module: Log
|
||||||
|
|
||||||
You can set different log levels for different modules.
|
You can set different log levels for different modules.
|
||||||
|
@@ -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
|
||||||
|
|
||||||
|
@@ -2,17 +2,7 @@
|
|||||||
|
|
||||||
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
|
||||||
|
@@ -36,8 +36,8 @@ func Init() {
|
|||||||
initStatic(cfg.Mod.StaticDir)
|
initStatic(cfg.Mod.StaticDir)
|
||||||
initWS()
|
initWS()
|
||||||
|
|
||||||
HandleFunc("/api/streams", streamsHandler)
|
HandleFunc("api/streams", streamsHandler)
|
||||||
HandleFunc("/api/ws", apiWS)
|
HandleFunc("api/ws", apiWS)
|
||||||
|
|
||||||
// ensure we can listen without errors
|
// ensure we can listen without errors
|
||||||
listener, err := net.Listen("tcp", cfg.Mod.Listen)
|
listener, err := net.Listen("tcp", cfg.Mod.Listen)
|
||||||
@@ -50,14 +50,29 @@ func Init() {
|
|||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
s := http.Server{}
|
s := http.Server{}
|
||||||
|
|
||||||
|
if log.Trace().Enabled() {
|
||||||
|
s.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
log.Trace().Stringer("url", r.URL).Msgf("[api] %s", r.Method)
|
||||||
|
http.DefaultServeMux.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if err = s.Serve(listener); err != nil {
|
if err = s.Serve(listener); err != nil {
|
||||||
log.Fatal().Err(err).Msg("[api] serve")
|
log.Fatal().Err(err).Msg("[api] serve")
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HandleFunc handle pattern with relative path:
|
||||||
|
// - "api/streams" => "{basepath}/api/streams"
|
||||||
|
// - "/streams" => "/streams"
|
||||||
func HandleFunc(pattern string, handler http.HandlerFunc) {
|
func HandleFunc(pattern string, handler http.HandlerFunc) {
|
||||||
http.HandleFunc(basePath+pattern, handler)
|
if len(pattern) == 0 || pattern[0] != '/' {
|
||||||
|
pattern = basePath + "/" + pattern
|
||||||
|
}
|
||||||
|
log.Trace().Str("path", pattern).Msg("[api] register path")
|
||||||
|
http.HandleFunc(pattern, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
func HandleWS(msgType string, handler WSHandler) {
|
func HandleWS(msgType string, handler WSHandler) {
|
||||||
@@ -73,7 +88,7 @@ func streamsHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
case "PUT":
|
case "PUT":
|
||||||
streams.Get(src)
|
streams.New(src, src)
|
||||||
return
|
return
|
||||||
case "DELETE":
|
case "DELETE":
|
||||||
streams.Delete(src)
|
streams.Delete(src)
|
||||||
@@ -86,13 +101,10 @@ func streamsHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
} else {
|
} else {
|
||||||
v = streams.All()
|
v = streams.All()
|
||||||
}
|
}
|
||||||
data, err := json.Marshal(v)
|
|
||||||
if err != nil {
|
e := json.NewEncoder(w)
|
||||||
log.Error().Err(err).Msg("[api.streams] marshal")
|
e.SetIndent("", " ")
|
||||||
}
|
_ = e.Encode(v)
|
||||||
if _, err = w.Write(data); err != nil {
|
|
||||||
log.Error().Err(err).Msg("[api.streams] write")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func apiWS(w http.ResponseWriter, r *http.Request) {
|
func apiWS(w http.ResponseWriter, r *http.Request) {
|
||||||
|
@@ -14,13 +14,13 @@ func initStatic(staticDir string) {
|
|||||||
root = http.FS(www.Static)
|
root = http.FS(www.Static)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
base := len(basePath)
|
||||||
fileServer := http.FileServer(root)
|
fileServer := http.FileServer(root)
|
||||||
|
|
||||||
HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
HandleFunc("", func(w http.ResponseWriter, r *http.Request) {
|
||||||
if basePath != "" {
|
if base > 0 {
|
||||||
r.URL.Path = r.URL.Path[len(basePath):]
|
r.URL.Path = r.URL.Path[base:]
|
||||||
}
|
}
|
||||||
|
|
||||||
fileServer.ServeHTTP(w, r)
|
fileServer.ServeHTTP(w, r)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@@ -10,8 +10,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
api.HandleFunc("/api/stack", stackHandler)
|
api.HandleFunc("api/stack", stackHandler)
|
||||||
api.HandleFunc("/api/exit", exitHandler)
|
api.HandleFunc("api/exit", exitHandler)
|
||||||
|
|
||||||
streams.HandleFunc("null", nullHandler)
|
streams.HandleFunc("null", nullHandler)
|
||||||
}
|
}
|
||||||
|
29
cmd/echo/echo.go
Normal file
29
cmd/echo/echo.go
Normal file
@@ -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))
|
||||||
|
})
|
||||||
|
}
|
@@ -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
|
|
||||||
}
|
|
||||||
|
@@ -39,3 +39,5 @@
|
|||||||
- https://html5test.com/
|
- https://html5test.com/
|
||||||
- https://trac.ffmpeg.org/wiki/Capture/Webcam
|
- https://trac.ffmpeg.org/wiki/Capture/Webcam
|
||||||
- https://trac.ffmpeg.org/wiki/DirectShow
|
- https://trac.ffmpeg.org/wiki/DirectShow
|
||||||
|
- https://stackoverflow.com/questions/53207692/libav-mjpeg-encoding-and-huffman-table
|
||||||
|
- https://github.com/tuupola/esp_video/blob/master/README.md
|
||||||
|
@@ -15,7 +15,7 @@ import (
|
|||||||
func Init() {
|
func Init() {
|
||||||
log = app.GetLogger("exec")
|
log = app.GetLogger("exec")
|
||||||
|
|
||||||
api.HandleFunc("/api/devices", handle)
|
api.HandleFunc("api/devices", handle)
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetInput(src string) (string, error) {
|
func GetInput(src string) (string, error) {
|
||||||
|
@@ -37,6 +37,7 @@ func Init() {
|
|||||||
"h264/ultra": "-codec:v libx264 -g 30 -preset ultrafast -tune zerolatency",
|
"h264/ultra": "-codec:v libx264 -g 30 -preset ultrafast -tune zerolatency",
|
||||||
"h264/high": "-codec:v libx264 -g 30 -preset superfast -tune zerolatency",
|
"h264/high": "-codec:v libx264 -g 30 -preset superfast -tune zerolatency",
|
||||||
"h265": "-codec:v libx265 -g 30 -preset ultrafast -tune zerolatency",
|
"h265": "-codec:v libx265 -g 30 -preset ultrafast -tune zerolatency",
|
||||||
|
"mjpeg": "-codec:v mjpeg -force_duplicated_matrix 1 -huffman 0 -pix_fmt yuvj420p",
|
||||||
"opus": "-codec:a libopus -ar 48000 -ac 2",
|
"opus": "-codec:a libopus -ar 48000 -ac 2",
|
||||||
"pcmu": "-codec:a pcm_mulaw -ar 8000 -ac 1",
|
"pcmu": "-codec:a pcm_mulaw -ar 8000 -ac 1",
|
||||||
"pcmu/16000": "-codec:a pcm_mulaw -ar 16000 -ac 1",
|
"pcmu/16000": "-codec:a pcm_mulaw -ar 16000 -ac 1",
|
||||||
@@ -70,7 +71,7 @@ func Init() {
|
|||||||
var input string
|
var input string
|
||||||
if i := strings.IndexByte(s, ':'); i > 0 {
|
if i := strings.IndexByte(s, ':'); i > 0 {
|
||||||
switch s[:i] {
|
switch s[:i] {
|
||||||
case "http", "https":
|
case "http", "https", "rtmp":
|
||||||
input = strings.Replace(tpl["http"], "{input}", s, 1)
|
input = strings.Replace(tpl["http"], "{input}", s, 1)
|
||||||
case "rtsp", "rtsps":
|
case "rtsp", "rtsps":
|
||||||
// https://ffmpeg.org/ffmpeg-protocols.html#rtsp
|
// https://ffmpeg.org/ffmpeg-protocols.html#rtsp
|
||||||
|
106
cmd/hass/api.go
Normal file
106
cmd/hass/api.go
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
package hass
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"github.com/AlexxIT/go2rtc/cmd/api"
|
||||||
|
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||||
|
"github.com/AlexxIT/go2rtc/cmd/webrtc"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func initAPI() {
|
||||||
|
ok := func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_, _ = w.Write([]byte(`{"status":1,"payload":{}}`))
|
||||||
|
}
|
||||||
|
|
||||||
|
// support https://www.home-assistant.io/integrations/rtsp_to_webrtc/
|
||||||
|
api.HandleFunc("/static", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
})
|
||||||
|
|
||||||
|
api.HandleFunc("/streams", ok)
|
||||||
|
|
||||||
|
api.HandleFunc("/stream/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch {
|
||||||
|
// /stream/{id}/add
|
||||||
|
case strings.HasSuffix(r.RequestURI, "/add"):
|
||||||
|
var v addJSON
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&v); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// we can get three types of links:
|
||||||
|
// 1. link to go2rtc stream: rtsp://...:8554/{stream_name}
|
||||||
|
// 2. static link to Hass camera
|
||||||
|
// 3. dynamic link to Hass camera
|
||||||
|
stream := streams.Get(v.Name)
|
||||||
|
if stream == nil {
|
||||||
|
// check if it is rtsp link to go2rtc
|
||||||
|
stream = rtspStream(v.Channels.First.Url)
|
||||||
|
if stream != nil {
|
||||||
|
streams.New(v.Name, stream)
|
||||||
|
} else {
|
||||||
|
stream = streams.New(v.Name, "{input}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stream.SetSource(v.Channels.First.Url)
|
||||||
|
|
||||||
|
ok(w, r)
|
||||||
|
|
||||||
|
// /stream/{id}/channel/0/webrtc
|
||||||
|
default:
|
||||||
|
i := strings.IndexByte(r.RequestURI[8:], '/')
|
||||||
|
name := r.RequestURI[8 : 8+i]
|
||||||
|
|
||||||
|
stream := streams.Get(name)
|
||||||
|
if stream == nil {
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.ParseForm(); err != nil {
|
||||||
|
log.Error().Err(err).Msg("[api.hass] parse form")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s := r.FormValue("data")
|
||||||
|
offer, err := base64.StdEncoding.DecodeString(s)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("[api.hass] sdp64 decode")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s, err = webrtc.ExchangeSDP(stream, string(offer), r.UserAgent())
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("[api.hass] exchange SDP")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s = base64.StdEncoding.EncodeToString([]byte(s))
|
||||||
|
_, _ = w.Write([]byte(s))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func rtspStream(url string) *streams.Stream {
|
||||||
|
if strings.HasPrefix(url, "rtsp://") {
|
||||||
|
if i := strings.IndexByte(url[7:], '/'); i > 0 {
|
||||||
|
return streams.Get(url[8+i:])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type addJSON struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Channels struct {
|
||||||
|
First struct {
|
||||||
|
//Name string `json:"name"`
|
||||||
|
Url string `json:"url"`
|
||||||
|
} `json:"0"`
|
||||||
|
} `json:"channels"`
|
||||||
|
}
|
@@ -1,20 +1,14 @@
|
|||||||
package hass
|
package hass
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/AlexxIT/go2rtc/cmd/api"
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/app"
|
"github.com/AlexxIT/go2rtc/cmd/app"
|
||||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||||
"github.com/AlexxIT/go2rtc/cmd/webrtc"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
@@ -28,11 +22,7 @@ func Init() {
|
|||||||
|
|
||||||
log = app.GetLogger("hass")
|
log = app.GetLogger("hass")
|
||||||
|
|
||||||
// support https://www.home-assistant.io/integrations/rtsp_to_webrtc/
|
initAPI()
|
||||||
api.HandleFunc("/static", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
})
|
|
||||||
api.HandleFunc("/stream", handler)
|
|
||||||
|
|
||||||
// support load cameras from Hass config file
|
// support load cameras from Hass config file
|
||||||
filename := path.Join(conf.Mod.Config, ".storage/core.config_entries")
|
filename := path.Join(conf.Mod.Config, ".storage/core.config_entries")
|
||||||
@@ -85,66 +75,6 @@ func Init() {
|
|||||||
|
|
||||||
var log zerolog.Logger
|
var log zerolog.Logger
|
||||||
|
|
||||||
func handler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if err := r.ParseForm(); err != nil {
|
|
||||||
log.Error().Err(err).Msg("[api.hass] parse form")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
src := r.FormValue("url")
|
|
||||||
src, err := url.QueryUnescape(src)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("[api.hass] query unescape")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
str := r.FormValue("sdp64")
|
|
||||||
|
|
||||||
offer, err := base64.StdEncoding.DecodeString(str)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("[api.hass] sdp64 decode")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if stream links to our rtsp server
|
|
||||||
if strings.HasPrefix(src, "rtsp://") {
|
|
||||||
i := strings.IndexByte(src[7:], '/')
|
|
||||||
if i > 0 && streams.Has(src[8+i:]) {
|
|
||||||
src = src[8+i:]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stream := streams.Get(src)
|
|
||||||
if stream == nil {
|
|
||||||
log.Error().Str("url", src).Msg("[api.hass] unsupported source")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
str, err = webrtc.ExchangeSDP(stream, string(offer), r.UserAgent())
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("[api.hass] exchange SDP")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resp := struct {
|
|
||||||
Answer string `json:"sdp64"`
|
|
||||||
}{
|
|
||||||
Answer: base64.StdEncoding.EncodeToString([]byte(str)),
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := json.Marshal(resp)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("[api.hass] marshal JSON")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
if _, err = w.Write(data); err != nil {
|
|
||||||
log.Error().Err(err).Msg("[api.hass] write")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type entries struct {
|
type entries struct {
|
||||||
Data struct {
|
Data struct {
|
||||||
Entries []struct {
|
Entries []struct {
|
||||||
|
@@ -54,15 +54,7 @@ func apiHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
items = append(items, device)
|
items = append(items, device)
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := json.Marshal(items)
|
_= json.NewEncoder(w).Encode(items)
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("[api.homekit]")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err = w.Write(data); err != nil {
|
|
||||||
log.Error().Err(err).Msg("[api.homekit]")
|
|
||||||
}
|
|
||||||
|
|
||||||
case "POST":
|
case "POST":
|
||||||
// TODO: post params...
|
// TODO: post params...
|
||||||
|
@@ -14,7 +14,7 @@ func Init() {
|
|||||||
|
|
||||||
streams.HandleFunc("homekit", streamHandler)
|
streams.HandleFunc("homekit", streamHandler)
|
||||||
|
|
||||||
api.HandleFunc("/api/homekit", apiHandler)
|
api.HandleFunc("api/homekit", apiHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
var log zerolog.Logger
|
var log zerolog.Logger
|
||||||
|
19
cmd/ivideon/ivideon.go
Normal file
19
cmd/ivideon/ivideon.go
Normal file
@@ -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
|
||||||
|
})
|
||||||
|
}
|
54
cmd/mjpeg/mjpeg.go
Normal file
54
cmd/mjpeg/mjpeg.go
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
package mjpeg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/AlexxIT/go2rtc/cmd/api"
|
||||||
|
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/mjpeg"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Init() {
|
||||||
|
api.HandleFunc("api/stream.mjpeg", handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
const header = "--frame\r\nContent-Type: image/jpeg\r\nContent-Length: "
|
||||||
|
|
||||||
|
func handler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
src := r.URL.Query().Get("src")
|
||||||
|
stream := streams.GetOrNew(src)
|
||||||
|
if stream == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
exit := make(chan struct{})
|
||||||
|
|
||||||
|
cons := &mjpeg.Consumer{}
|
||||||
|
cons.Listen(func(msg interface{}) {
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
case []byte:
|
||||||
|
data := []byte(header + strconv.Itoa(len(msg)))
|
||||||
|
data = append(data, 0x0D, 0x0A, 0x0D, 0x0A)
|
||||||
|
data = append(data, msg...)
|
||||||
|
data = append(data, 0x0D, 0x0A)
|
||||||
|
|
||||||
|
if _, err := w.Write(data); err != nil {
|
||||||
|
exit <- struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := stream.AddConsumer(cons); err != nil {
|
||||||
|
log.Error().Err(err).Msg("[api.mjpeg] add consumer")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", `multipart/x-mixed-replace; boundary=frame`)
|
||||||
|
|
||||||
|
<-exit
|
||||||
|
|
||||||
|
stream.RemoveConsumer(cons)
|
||||||
|
|
||||||
|
//log.Trace().Msg("[api.mjpeg] close")
|
||||||
|
}
|
@@ -16,8 +16,8 @@ func Init() {
|
|||||||
|
|
||||||
api.HandleWS(MsgTypeMSE, handlerWS)
|
api.HandleWS(MsgTypeMSE, handlerWS)
|
||||||
|
|
||||||
api.HandleFunc("/api/frame.mp4", handlerKeyframe)
|
api.HandleFunc("api/frame.mp4", handlerKeyframe)
|
||||||
api.HandleFunc("/api/stream.mp4", handlerMP4)
|
api.HandleFunc("api/stream.mp4", handlerMP4)
|
||||||
}
|
}
|
||||||
|
|
||||||
var log zerolog.Logger
|
var log zerolog.Logger
|
||||||
@@ -28,7 +28,7 @@ func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
src := r.URL.Query().Get("src")
|
src := r.URL.Query().Get("src")
|
||||||
stream := streams.Get(src)
|
stream := streams.GetOrNew(src)
|
||||||
if stream == nil {
|
if stream == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -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)))
|
||||||
|
|
||||||
@@ -71,7 +75,7 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) {
|
|||||||
log.Trace().Msgf("[api.mp4] %+v", r)
|
log.Trace().Msgf("[api.mp4] %+v", r)
|
||||||
|
|
||||||
src := r.URL.Query().Get("src")
|
src := r.URL.Query().Get("src")
|
||||||
stream := streams.Get(src)
|
stream := streams.GetOrNew(src)
|
||||||
if stream == nil {
|
if stream == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
@@ -11,7 +11,7 @@ const MsgTypeMSE = "mse" // fMP4
|
|||||||
|
|
||||||
func handlerWS(ctx *api.Context, msg *streamer.Message) {
|
func handlerWS(ctx *api.Context, msg *streamer.Message) {
|
||||||
src := ctx.Request.URL.Query().Get("src")
|
src := ctx.Request.URL.Query().Get("src")
|
||||||
stream := streams.Get(src)
|
stream := streams.GetOrNew(src)
|
||||||
if stream == nil {
|
if stream == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
@@ -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
|
||||||
@@ -143,15 +157,15 @@ func worker(address string) {
|
|||||||
|
|
||||||
log.Debug().Str("stream", name).Msg("[rtsp] new producer")
|
log.Debug().Str("stream", name).Msg("[rtsp] new producer")
|
||||||
|
|
||||||
str := streams.Get(conn.URL.Path[1:])
|
stream := streams.Get(name)
|
||||||
if str == nil {
|
if stream == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
str.AddProducer(conn)
|
stream.AddProducer(conn)
|
||||||
|
|
||||||
onDisconnect = func() {
|
onDisconnect = func() {
|
||||||
str.RemoveProducer(conn)
|
stream.RemoveProducer(conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
case streamer.StatePlaying:
|
case streamer.StatePlaying:
|
||||||
|
@@ -2,6 +2,7 @@ package streams
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -18,6 +19,8 @@ type Producer struct {
|
|||||||
streamer.Element
|
streamer.Element
|
||||||
|
|
||||||
url string
|
url string
|
||||||
|
template string
|
||||||
|
|
||||||
element streamer.Producer
|
element streamer.Producer
|
||||||
tracks []*streamer.Track
|
tracks []*streamer.Track
|
||||||
|
|
||||||
@@ -25,6 +28,13 @@ type Producer struct {
|
|||||||
mx sync.Mutex
|
mx sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Producer) SetSource(s string) {
|
||||||
|
if p.template == "" {
|
||||||
|
p.template = p.url
|
||||||
|
}
|
||||||
|
p.url = strings.Replace(p.template, "{input}", s, 1)
|
||||||
|
}
|
||||||
|
|
||||||
func (p *Producer) GetMedias() []*streamer.Media {
|
func (p *Producer) GetMedias() []*streamer.Media {
|
||||||
p.mx.Lock()
|
p.mx.Lock()
|
||||||
defer p.mx.Unlock()
|
defer p.mx.Unlock()
|
||||||
@@ -79,7 +89,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() {
|
||||||
|
@@ -17,25 +17,34 @@ type Stream struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewStream(source interface{}) *Stream {
|
func NewStream(source interface{}) *Stream {
|
||||||
s := new(Stream)
|
|
||||||
|
|
||||||
switch source := source.(type) {
|
switch source := source.(type) {
|
||||||
case string:
|
case string:
|
||||||
|
s := new(Stream)
|
||||||
prod := &Producer{url: source}
|
prod := &Producer{url: source}
|
||||||
s.producers = append(s.producers, prod)
|
s.producers = append(s.producers, prod)
|
||||||
|
return s
|
||||||
case []interface{}:
|
case []interface{}:
|
||||||
|
s := new(Stream)
|
||||||
for _, source := range source {
|
for _, source := range source {
|
||||||
prod := &Producer{url: source.(string)}
|
prod := &Producer{url: source.(string)}
|
||||||
s.producers = append(s.producers, prod)
|
s.producers = append(s.producers, prod)
|
||||||
}
|
}
|
||||||
|
return s
|
||||||
|
case *Stream:
|
||||||
|
return source
|
||||||
case map[string]interface{}:
|
case map[string]interface{}:
|
||||||
return NewStream(source["url"])
|
return NewStream(source["url"])
|
||||||
case nil:
|
case nil:
|
||||||
|
return new(Stream)
|
||||||
default:
|
default:
|
||||||
panic("wrong source type")
|
panic("wrong source type")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return s
|
func (s *Stream) SetSource(source string) {
|
||||||
|
for _, prod := range s.producers {
|
||||||
|
prod.SetSource(source)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Stream) AddConsumer(cons streamer.Consumer) (err error) {
|
func (s *Stream) AddConsumer(cons streamer.Consumer) (err error) {
|
||||||
|
@@ -24,10 +24,19 @@ func Init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Get(src string) *Stream {
|
func Get(name string) *Stream {
|
||||||
|
return streams[name]
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(name string, source interface{}) *Stream {
|
||||||
|
stream := NewStream(source)
|
||||||
|
streams[name] = stream
|
||||||
|
return stream
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetOrNew(src string) *Stream {
|
||||||
if stream, ok := streams[src]; ok {
|
if stream, ok := streams[src]; ok {
|
||||||
return stream
|
return stream
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !HasProducer(src) {
|
if !HasProducer(src) {
|
||||||
@@ -35,17 +44,8 @@ func Get(src string) *Stream {
|
|||||||
}
|
}
|
||||||
|
|
||||||
log.Info().Str("url", src).Msg("[streams] create new stream")
|
log.Info().Str("url", src).Msg("[streams] create new stream")
|
||||||
stream := NewStream(src)
|
|
||||||
streams[src] = stream
|
|
||||||
return stream
|
|
||||||
}
|
|
||||||
|
|
||||||
func Has(src string) bool {
|
return New(src, src)
|
||||||
return streams[src] != nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func New(name string, source interface{}) {
|
|
||||||
streams[name] = NewStream(source)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func Delete(name string) {
|
func Delete(name string) {
|
||||||
|
@@ -5,7 +5,6 @@ import (
|
|||||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/webrtc"
|
"github.com/AlexxIT/go2rtc/pkg/webrtc"
|
||||||
"github.com/pion/sdp/v3"
|
"github.com/pion/sdp/v3"
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var candidates []string
|
var candidates []string
|
||||||
@@ -32,16 +31,12 @@ func addCanditates(answer string) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, address := range candidates {
|
for _, address := range candidates {
|
||||||
if strings.HasPrefix(address, "stun:") {
|
var err error
|
||||||
ip, err := webrtc.GetPublicIP()
|
address, err = webrtc.LookupIP(address)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn().Err(err).Msg("[webrtc] public IP")
|
log.Warn().Err(err).Msg("[webrtc] candidate")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
address = ip.String() + address[4:]
|
|
||||||
|
|
||||||
log.Debug().Str("addr", address).Msg("[webrtc] stun public address")
|
|
||||||
}
|
|
||||||
|
|
||||||
cand, err := webrtc.NewCandidate(address)
|
cand, err := webrtc.NewCandidate(address)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
12
main.go
12
main.go
@@ -4,10 +4,13 @@ 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/mjpeg"
|
||||||
"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,20 +27,25 @@ func main() {
|
|||||||
app.Init() // init config and logs
|
app.Init() // init config and logs
|
||||||
streams.Init() // load streams list
|
streams.Init() // load streams list
|
||||||
|
|
||||||
|
api.Init() // init HTTP API server
|
||||||
|
|
||||||
|
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)
|
||||||
ffmpeg.Init() // add support ffmpeg scheme (depends on exec scheme)
|
ffmpeg.Init() // add support ffmpeg scheme (depends on exec scheme)
|
||||||
hass.Init() // add support hass scheme
|
hass.Init() // add support hass scheme
|
||||||
|
|
||||||
api.Init() // init HTTP API server
|
|
||||||
|
|
||||||
webrtc.Init()
|
webrtc.Init()
|
||||||
mp4.Init()
|
mp4.Init()
|
||||||
|
mjpeg.Init()
|
||||||
|
|
||||||
srtp.Init()
|
srtp.Init()
|
||||||
homekit.Init()
|
homekit.Init()
|
||||||
|
|
||||||
|
ivideon.Init()
|
||||||
|
|
||||||
ngrok.Init()
|
ngrok.Init()
|
||||||
debug.Init()
|
debug.Init()
|
||||||
|
|
||||||
|
@@ -1,3 +1,14 @@
|
|||||||
|
## RTP H264
|
||||||
|
|
||||||
|
Camera | NALu
|
||||||
|
-------|-----
|
||||||
|
EZVIZ C3S | 7f, 8f, 28:28:28 -> 5t, 28:28:28 -> 1t, 1t, 1t, 1t
|
||||||
|
Sonoff GK-200MP2-B | 28:28:28 -> 5t, 1t, 1t, 1t
|
||||||
|
Dahua IPC-K42 | 7f, 8f, 28:28:28 -> 5t, 28:28:28 -> 1t, 28:28:28 -> 1t
|
||||||
|
FFmpeg copy | 5t, 1t, 1t, 28:28:28 -> 1t, 28:28:28 -> 1t
|
||||||
|
FFmpeg h264 | 24 -> 6:5:5:5:5t, 24 -> 1:1:1:1t, 28:28:28 -> 5f, 28:28:28 -> 5f, 28:28:28 -> 5t
|
||||||
|
FFmpeg resize | 6f, 28:28:28 -> 5f, 28... -> 5t, 24 -> 1:1f, 24 -> 1:1t
|
||||||
|
|
||||||
## WebRTC
|
## WebRTC
|
||||||
|
|
||||||
Video codec | Media string | Device
|
Video codec | Media string | Device
|
||||||
|
@@ -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
|
||||||
|
}
|
||||||
|
@@ -22,52 +22,62 @@ func RTPDepay(track *streamer.Track) streamer.WrapperFunc {
|
|||||||
return func(packet *rtp.Packet) error {
|
return func(packet *rtp.Packet) error {
|
||||||
//nalUnitType := packet.Payload[0] & 0x1F
|
//nalUnitType := packet.Payload[0] & 0x1F
|
||||||
//fmt.Printf(
|
//fmt.Printf(
|
||||||
// "[RTP] codec: %s, nalu: %2d, size: %6d, ts: %10d, pt: %2d, ssrc: %d\n",
|
// "[RTP] codec: %s, nalu: %2d, size: %6d, ts: %10d, pt: %2d, ssrc: %d, seq: %d\n",
|
||||||
// track.Codec.Name, nalUnitType, len(packet.Payload), packet.Timestamp,
|
// track.Codec.Name, nalUnitType, len(packet.Payload), packet.Timestamp,
|
||||||
// packet.PayloadType, packet.SSRC,
|
// packet.PayloadType, packet.SSRC, packet.SequenceNumber,
|
||||||
//)
|
//)
|
||||||
|
|
||||||
// NALu packets can be split in different ways:
|
data, err := depack.Unmarshal(packet.Payload)
|
||||||
// - single type 7 and type 8 packets
|
if len(data) == 0 || err != nil {
|
||||||
// - join type 7 and type 8 packet (type 24)
|
|
||||||
// - split type 5 on multiple 28 packets
|
|
||||||
// - split type 5 on multiple separate 28 packets
|
|
||||||
units, err := depack.Unmarshal(packet.Payload)
|
|
||||||
if len(units) == 0 || err != nil {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
for len(units) > 0 {
|
for {
|
||||||
i := int(binary.BigEndian.Uint32(units)) + 4
|
unitType := NALUType(data)
|
||||||
unit := units[:i] // NAL Unit with AVC header
|
//fmt.Printf("[H264] nalu: %2d, size: %6d\n", unitType, len(data))
|
||||||
units = units[i:]
|
|
||||||
|
|
||||||
unitType := NALUType(unit)
|
// multiple 5 and 1 in one payload is OK
|
||||||
//fmt.Printf("[H264] type: %2d, size: %6d\n", unitType, i)
|
if unitType != NALUTypeIFrame && unitType != NALUTypePFrame {
|
||||||
|
i := int(binary.BigEndian.Uint32(data)) + 4
|
||||||
|
if i < len(data) {
|
||||||
|
data0 := data[:i] // NAL Unit with AVC header
|
||||||
|
data = data[i:]
|
||||||
switch unitType {
|
switch unitType {
|
||||||
case NALUTypeSPS:
|
case NALUTypeSPS:
|
||||||
//println("new SPS")
|
sps = data0
|
||||||
sps = unit
|
|
||||||
continue
|
continue
|
||||||
case NALUTypePPS:
|
case NALUTypePPS:
|
||||||
//println("new PPS")
|
pps = data0
|
||||||
pps = unit
|
|
||||||
continue
|
continue
|
||||||
case NALUTypeSEI:
|
case NALUTypeSEI:
|
||||||
// some unnecessary text information
|
// some unnecessary text information
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch unitType {
|
||||||
|
case NALUTypeSPS:
|
||||||
|
sps = data
|
||||||
|
return nil
|
||||||
|
case NALUTypePPS:
|
||||||
|
pps = data
|
||||||
|
return nil
|
||||||
|
case NALUTypeSEI:
|
||||||
|
// some unnecessary text information
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// ffmpeg with `-tune zerolatency` enable option `-x264opts sliced-threads=1`
|
// ffmpeg with `-tune zerolatency` enable option `-x264opts sliced-threads=1`
|
||||||
// and every NALU will be sliced to multiple NALUs
|
// and every NALU will be sliced to multiple NALUs
|
||||||
if !packet.Marker {
|
if !packet.Marker {
|
||||||
buffer = append(buffer, unit...)
|
buffer = append(buffer, data...)
|
||||||
continue
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if buffer != nil {
|
if buffer != nil {
|
||||||
buffer = append(buffer, unit...)
|
buffer = append(buffer, data...)
|
||||||
unit = buffer
|
data = buffer
|
||||||
buffer = nil
|
buffer = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,14 +101,10 @@ func RTPDepay(track *streamer.Track) streamer.WrapperFunc {
|
|||||||
|
|
||||||
clone = *packet
|
clone = *packet
|
||||||
clone.Version = RTPPacketVersionAVC
|
clone.Version = RTPPacketVersionAVC
|
||||||
clone.Payload = unit
|
clone.Payload = data
|
||||||
if err = push(&clone); err != nil {
|
return push(&clone)
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
286
pkg/ivideon/client.go
Normal file
286
pkg/ivideon/client.go
Normal file
@@ -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"`
|
||||||
|
}
|
31
pkg/ivideon/streamer.go
Normal file
31
pkg/ivideon/streamer.go
Normal file
@@ -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()
|
||||||
|
}
|
4
pkg/mjpeg/README.md
Normal file
4
pkg/mjpeg/README.md
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
## Useful links
|
||||||
|
|
||||||
|
- https://www.rfc-editor.org/rfc/rfc2435
|
||||||
|
- https://github.com/GStreamer/gst-plugins-good/blob/master/gst/rtp/gstrtpjpegdepay.c
|
87
pkg/mjpeg/consumer.go
Normal file
87
pkg/mjpeg/consumer.go
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
package mjpeg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||||
|
"github.com/pion/rtp"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Consumer struct {
|
||||||
|
streamer.Element
|
||||||
|
|
||||||
|
UserAgent string
|
||||||
|
RemoteAddr string
|
||||||
|
|
||||||
|
codecs []*streamer.Codec
|
||||||
|
start bool
|
||||||
|
|
||||||
|
send int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Consumer) GetMedias() []*streamer.Media {
|
||||||
|
return []*streamer.Media{{
|
||||||
|
Kind: streamer.KindVideo,
|
||||||
|
Direction: streamer.DirectionRecvonly,
|
||||||
|
Codecs: []*streamer.Codec{{Name: streamer.CodecJPEG}},
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track {
|
||||||
|
var header, payload []byte
|
||||||
|
|
||||||
|
push := func(packet *rtp.Packet) error {
|
||||||
|
//fmt.Printf(
|
||||||
|
// "[RTP] codec: %s, size: %6d, ts: %10d, pt: %2d, ssrc: %d, seq: %d, mark: %v\n",
|
||||||
|
// track.Codec.Name, len(packet.Payload), packet.Timestamp,
|
||||||
|
// packet.PayloadType, packet.SSRC, packet.SequenceNumber, packet.Marker,
|
||||||
|
//)
|
||||||
|
|
||||||
|
// https://www.rfc-editor.org/rfc/rfc2435#section-3.1
|
||||||
|
b := packet.Payload
|
||||||
|
|
||||||
|
// 3.1. JPEG header
|
||||||
|
t := b[4]
|
||||||
|
|
||||||
|
// 3.1.7. Restart Marker header
|
||||||
|
if 64 <= t && t <= 127 {
|
||||||
|
b = b[12:] // skip it
|
||||||
|
} else {
|
||||||
|
b = b[8:]
|
||||||
|
}
|
||||||
|
|
||||||
|
if header == nil {
|
||||||
|
var lqt, cqt []byte
|
||||||
|
|
||||||
|
// 3.1.8. Quantization Table header
|
||||||
|
q := packet.Payload[5]
|
||||||
|
if q >= 128 {
|
||||||
|
lqt = b[4:68]
|
||||||
|
cqt = b[68:132]
|
||||||
|
b = b[132:]
|
||||||
|
} else {
|
||||||
|
lqt, cqt = MakeTables(q)
|
||||||
|
}
|
||||||
|
|
||||||
|
w := uint16(packet.Payload[6]) << 3
|
||||||
|
h := uint16(packet.Payload[7]) << 3
|
||||||
|
//fmt.Printf("t: %d, q: %d, w: %d, h: %d\n", t, q, w, h)
|
||||||
|
header = MakeHeaders(t, w, h, lqt, cqt)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3.1.9. JPEG Payload
|
||||||
|
payload = append(payload, b...)
|
||||||
|
|
||||||
|
if packet.Marker {
|
||||||
|
b = append(header, payload...)
|
||||||
|
if end := b[len(b)-2:]; end[0] != 0xFF && end[1] != 0xD9 {
|
||||||
|
b = append(b, 0xFF, 0xD9)
|
||||||
|
}
|
||||||
|
c.Fire(b)
|
||||||
|
|
||||||
|
header = nil
|
||||||
|
payload = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return track.Bind(push)
|
||||||
|
}
|
182
pkg/mjpeg/rfc2435.go
Normal file
182
pkg/mjpeg/rfc2435.go
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
package mjpeg
|
||||||
|
|
||||||
|
// RFC 2435. Appendix A
|
||||||
|
|
||||||
|
var jpeg_luma_quantizer = []byte{
|
||||||
|
16, 11, 10, 16, 24, 40, 51, 61,
|
||||||
|
12, 12, 14, 19, 26, 58, 60, 55,
|
||||||
|
14, 13, 16, 24, 40, 57, 69, 56,
|
||||||
|
14, 17, 22, 29, 51, 87, 80, 62,
|
||||||
|
18, 22, 37, 56, 68, 109, 103, 77,
|
||||||
|
24, 35, 55, 64, 81, 104, 113, 92,
|
||||||
|
49, 64, 78, 87, 103, 121, 120, 101,
|
||||||
|
72, 92, 95, 98, 112, 100, 103, 99,
|
||||||
|
}
|
||||||
|
var jpeg_chroma_quantizer = []byte{
|
||||||
|
17, 18, 24, 47, 99, 99, 99, 99,
|
||||||
|
18, 21, 26, 66, 99, 99, 99, 99,
|
||||||
|
24, 26, 56, 99, 99, 99, 99, 99,
|
||||||
|
47, 66, 99, 99, 99, 99, 99, 99,
|
||||||
|
99, 99, 99, 99, 99, 99, 99, 99,
|
||||||
|
99, 99, 99, 99, 99, 99, 99, 99,
|
||||||
|
99, 99, 99, 99, 99, 99, 99, 99,
|
||||||
|
99, 99, 99, 99, 99, 99, 99, 99,
|
||||||
|
}
|
||||||
|
|
||||||
|
func MakeTables(q byte) (lqt, cqt []byte) {
|
||||||
|
var factor int
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case q < 1:
|
||||||
|
factor = 1
|
||||||
|
case q > 99:
|
||||||
|
factor = 99
|
||||||
|
default:
|
||||||
|
factor = int(q)
|
||||||
|
}
|
||||||
|
|
||||||
|
if q < 50 {
|
||||||
|
factor = 5000 / factor
|
||||||
|
} else if q > 99 {
|
||||||
|
factor = 200 - factor*2
|
||||||
|
}
|
||||||
|
|
||||||
|
lqt = make([]byte, 64)
|
||||||
|
cqt = make([]byte, 64)
|
||||||
|
|
||||||
|
for i := 0; i < 64; i++ {
|
||||||
|
lq := (int(jpeg_luma_quantizer[i])*factor + 50) / 100
|
||||||
|
cq := (int(jpeg_chroma_quantizer[i])*factor + 50) / 100
|
||||||
|
|
||||||
|
/* Limit the quantizers to 1 <= q <= 255 */
|
||||||
|
switch {
|
||||||
|
case lq < 1:
|
||||||
|
lqt[i] = 1
|
||||||
|
case lq > 255:
|
||||||
|
lqt[i] = 255
|
||||||
|
default:
|
||||||
|
lqt[i] = byte(lq)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case cq < 1:
|
||||||
|
cqt[i] = 1
|
||||||
|
case cq > 255:
|
||||||
|
cqt[i] = 255
|
||||||
|
default:
|
||||||
|
cqt[i] = byte(cq)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// RFC 2435. Appendix B
|
||||||
|
|
||||||
|
var lum_dc_codelens = []byte{
|
||||||
|
0, 1, 5, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
}
|
||||||
|
var lum_dc_symbols = []byte{
|
||||||
|
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11,
|
||||||
|
}
|
||||||
|
var lum_ac_codelens = []byte{
|
||||||
|
0, 2, 1, 3, 3, 2, 4, 3, 5, 5, 4, 4, 0, 0, 1, 0x7d,
|
||||||
|
}
|
||||||
|
var lum_ac_symbols = []byte{
|
||||||
|
0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12,
|
||||||
|
0x21, 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07,
|
||||||
|
0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xa1, 0x08,
|
||||||
|
0x23, 0x42, 0xb1, 0xc1, 0x15, 0x52, 0xd1, 0xf0,
|
||||||
|
0x24, 0x33, 0x62, 0x72, 0x82, 0x09, 0x0a, 0x16,
|
||||||
|
0x17, 0x18, 0x19, 0x1a, 0x25, 0x26, 0x27, 0x28,
|
||||||
|
0x29, 0x2a, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39,
|
||||||
|
0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49,
|
||||||
|
0x4a, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59,
|
||||||
|
0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69,
|
||||||
|
0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79,
|
||||||
|
0x7a, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89,
|
||||||
|
0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98,
|
||||||
|
0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7,
|
||||||
|
0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6,
|
||||||
|
0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5,
|
||||||
|
0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4,
|
||||||
|
0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xe1, 0xe2,
|
||||||
|
0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea,
|
||||||
|
0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8,
|
||||||
|
0xf9, 0xfa,
|
||||||
|
}
|
||||||
|
var chm_dc_codelens = []byte{
|
||||||
|
0, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0,
|
||||||
|
}
|
||||||
|
var chm_dc_symbols = []byte{
|
||||||
|
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11,
|
||||||
|
}
|
||||||
|
var chm_ac_codelens = []byte{
|
||||||
|
0, 2, 1, 2, 4, 4, 3, 4, 7, 5, 4, 4, 0, 1, 2, 0x77,
|
||||||
|
}
|
||||||
|
var chm_ac_symbols = []byte{
|
||||||
|
0x00, 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21,
|
||||||
|
0x31, 0x06, 0x12, 0x41, 0x51, 0x07, 0x61, 0x71,
|
||||||
|
0x13, 0x22, 0x32, 0x81, 0x08, 0x14, 0x42, 0x91,
|
||||||
|
0xa1, 0xb1, 0xc1, 0x09, 0x23, 0x33, 0x52, 0xf0,
|
||||||
|
0x15, 0x62, 0x72, 0xd1, 0x0a, 0x16, 0x24, 0x34,
|
||||||
|
0xe1, 0x25, 0xf1, 0x17, 0x18, 0x19, 0x1a, 0x26,
|
||||||
|
0x27, 0x28, 0x29, 0x2a, 0x35, 0x36, 0x37, 0x38,
|
||||||
|
0x39, 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48,
|
||||||
|
0x49, 0x4a, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58,
|
||||||
|
0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68,
|
||||||
|
0x69, 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78,
|
||||||
|
0x79, 0x7a, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87,
|
||||||
|
0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96,
|
||||||
|
0x97, 0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5,
|
||||||
|
0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4,
|
||||||
|
0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3,
|
||||||
|
0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2,
|
||||||
|
0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda,
|
||||||
|
0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9,
|
||||||
|
0xea, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8,
|
||||||
|
0xf9, 0xfa,
|
||||||
|
}
|
||||||
|
|
||||||
|
func MakeHeaders(t byte, w, h uint16, lqt, cqt []byte) []byte {
|
||||||
|
// Appendix A from https://www.rfc-editor.org/rfc/rfc2435
|
||||||
|
p := []byte{0xFF, 0xD8}
|
||||||
|
|
||||||
|
p = MakeQuantHeader(p, lqt, 0)
|
||||||
|
p = MakeQuantHeader(p, cqt, 1)
|
||||||
|
|
||||||
|
if t == 0 {
|
||||||
|
t = 0x21
|
||||||
|
} else {
|
||||||
|
t = 0x22
|
||||||
|
}
|
||||||
|
|
||||||
|
p = append(p,
|
||||||
|
0xFF, 0xC0, 0, 17, 8,
|
||||||
|
byte(h>>8), byte(h&0xFF),
|
||||||
|
byte(w>>8), byte(w&0xFF),
|
||||||
|
3, 0, t, 0, 1, 0x11, 1, 2, 0x11, 1,
|
||||||
|
)
|
||||||
|
|
||||||
|
p = MakeHuffmanHeader(p, lum_dc_codelens, lum_dc_symbols, 0, 0)
|
||||||
|
p = MakeHuffmanHeader(p, lum_ac_codelens, lum_ac_symbols, 0, 1)
|
||||||
|
p = MakeHuffmanHeader(p, chm_dc_codelens, chm_dc_symbols, 1, 0)
|
||||||
|
p = MakeHuffmanHeader(p, chm_ac_codelens, chm_ac_symbols, 1, 1)
|
||||||
|
|
||||||
|
return append(p, 0xFF, 0xDA, 0, 12, 3, 0, 0, 1, 0x11, 2, 0x11, 0, 63, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func MakeQuantHeader(p []byte, qt []byte, tableNo byte) []byte {
|
||||||
|
p = append(p, 0xFF, 0xDB, 0, 67, tableNo)
|
||||||
|
return append(p, qt...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func MakeHuffmanHeader(p, codelens, symbols []byte, tableNo, tableClass byte) []byte {
|
||||||
|
p = append(p,
|
||||||
|
0xFF, 0xC4, 0,
|
||||||
|
byte(3+len(codelens)+len(symbols)),
|
||||||
|
(tableClass<<4)|tableNo,
|
||||||
|
)
|
||||||
|
p = append(p, codelens...)
|
||||||
|
return append(p, symbols...)
|
||||||
|
}
|
@@ -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{}
|
||||||
}
|
}
|
||||||
|
@@ -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
|
||||||
|
|
||||||
|
@@ -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
|
|
||||||
}
|
|
||||||
|
@@ -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,8 +362,22 @@ 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 {
|
||||||
|
// Dahua VTO2111D fail on this step because of backchannel
|
||||||
|
if c.Backchannel {
|
||||||
|
if err = c.Dial(); err != nil {
|
||||||
return nil, err
|
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 == "" {
|
||||||
// Session: 216525287999;timeout=60
|
// Session: 216525287999;timeout=60
|
||||||
|
41
pkg/shell/shell.go
Normal file
41
pkg/shell/shell.go
Normal file
@@ -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
|
||||||
|
}
|
@@ -25,6 +25,7 @@ const (
|
|||||||
CodecVP8 = "VP8"
|
CodecVP8 = "VP8"
|
||||||
CodecVP9 = "VP9"
|
CodecVP9 = "VP9"
|
||||||
CodecAV1 = "AV1"
|
CodecAV1 = "AV1"
|
||||||
|
CodecJPEG = "JPEG" // payloadType: 26
|
||||||
|
|
||||||
CodecPCMU = "PCMU" // payloadType: 0
|
CodecPCMU = "PCMU" // payloadType: 0
|
||||||
CodecPCMA = "PCMA" // payloadType: 8
|
CodecPCMA = "PCMA" // payloadType: 8
|
||||||
@@ -36,7 +37,7 @@ const (
|
|||||||
|
|
||||||
func GetKind(name string) string {
|
func GetKind(name string) string {
|
||||||
switch name {
|
switch name {
|
||||||
case CodecH264, CodecH265, CodecVP8, CodecVP9, CodecAV1:
|
case CodecH264, CodecH265, CodecVP8, CodecVP9, CodecAV1, CodecJPEG:
|
||||||
return KindVideo
|
return KindVideo
|
||||||
case CodecPCMU, CodecPCMA, CodecAAC, CodecOpus, CodecG722, CodecMPA:
|
case CodecPCMU, CodecPCMA, CodecAAC, CodecOpus, CodecG722, CodecMPA:
|
||||||
return KindAudio
|
return KindAudio
|
||||||
@@ -129,12 +130,14 @@ type Codec struct {
|
|||||||
func NewCodec(name string) *Codec {
|
func NewCodec(name string) *Codec {
|
||||||
name = strings.ToUpper(name)
|
name = strings.ToUpper(name)
|
||||||
switch name {
|
switch name {
|
||||||
case CodecH264, CodecH265, CodecVP8, CodecVP9, CodecAV1:
|
case CodecH264, CodecH265, CodecVP8, CodecVP9, CodecAV1, CodecJPEG:
|
||||||
return &Codec{Name: name, ClockRate: 90000}
|
return &Codec{Name: name, ClockRate: 90000}
|
||||||
case CodecPCMU, CodecPCMA:
|
case CodecPCMU, CodecPCMA:
|
||||||
return &Codec{Name: name, ClockRate: 8000}
|
return &Codec{Name: name, ClockRate: 8000}
|
||||||
case CodecOpus:
|
case CodecOpus:
|
||||||
return &Codec{Name: name, ClockRate: 48000, Channels: 2}
|
return &Codec{Name: name, ClockRate: 48000, Channels: 2}
|
||||||
|
case "MJPEG":
|
||||||
|
return &Codec{Name: CodecJPEG, ClockRate: 90000}
|
||||||
}
|
}
|
||||||
|
|
||||||
panic(fmt.Sprintf("unsupported codec: %s", name))
|
panic(fmt.Sprintf("unsupported codec: %s", name))
|
||||||
@@ -257,6 +260,7 @@ func UnmarshalCodec(md *sdp.MediaDescription, payloadType string) *Codec {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if c.Name == "" {
|
if c.Name == "" {
|
||||||
|
// https://en.wikipedia.org/wiki/RTP_payload_formats
|
||||||
switch payloadType {
|
switch payloadType {
|
||||||
case "0":
|
case "0":
|
||||||
c.Name = CodecPCMU
|
c.Name = CodecPCMU
|
||||||
@@ -267,6 +271,9 @@ func UnmarshalCodec(md *sdp.MediaDescription, payloadType string) *Codec {
|
|||||||
case "14":
|
case "14":
|
||||||
c.Name = CodecMPA
|
c.Name = CodecMPA
|
||||||
c.ClockRate = 44100
|
c.ClockRate = 44100
|
||||||
|
case "26":
|
||||||
|
c.Name = CodecJPEG
|
||||||
|
c.ClockRate = 90000
|
||||||
default:
|
default:
|
||||||
c.Name = payloadType
|
c.Name = payloadType
|
||||||
}
|
}
|
||||||
|
@@ -32,6 +32,8 @@ func (t *Track) WriteRTP(p *rtp.Packet) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *Track) Bind(w WriterFunc) *Track {
|
func (t *Track) Bind(w WriterFunc) *Track {
|
||||||
|
t.mx.Lock()
|
||||||
|
|
||||||
if t.Sink == nil {
|
if t.Sink == nil {
|
||||||
t.Sink = map[*Track]WriterFunc{}
|
t.Sink = map[*Track]WriterFunc{}
|
||||||
}
|
}
|
||||||
@@ -39,9 +41,10 @@ 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()
|
t.mx.Unlock()
|
||||||
|
|
||||||
return clone
|
return clone
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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 {
|
||||||
|
@@ -1,12 +1,14 @@
|
|||||||
package webrtc
|
package webrtc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||||
"github.com/pion/ice/v2"
|
"github.com/pion/ice/v2"
|
||||||
"github.com/pion/stun"
|
"github.com/pion/stun"
|
||||||
"github.com/pion/webrtc/v3"
|
"github.com/pion/webrtc/v3"
|
||||||
"net"
|
"net"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewCandidate(address string) (string, error) {
|
func NewCandidate(address string) (string, error) {
|
||||||
@@ -34,6 +36,31 @@ func NewCandidate(address string) (string, error) {
|
|||||||
return "candidate:" + cand.Marshal(), nil
|
return "candidate:" + cand.Marshal(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func LookupIP(address string) (string, error) {
|
||||||
|
if strings.HasPrefix(address, "stun:") {
|
||||||
|
ip, err := GetPublicIP()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return ip.String() + address[4:], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if IsIP(address) {
|
||||||
|
return address, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
i := strings.IndexByte(address, ':')
|
||||||
|
ips, err := net.LookupIP(address[:i])
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if len(ips) == 0 {
|
||||||
|
return "", fmt.Errorf("can't resolve: %s", address)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ips[0].String() + address[i:], nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetPublicIP example from https://github.com/pion/stun
|
// GetPublicIP example from https://github.com/pion/stun
|
||||||
func GetPublicIP() (net.IP, error) {
|
func GetPublicIP() (net.IP, error) {
|
||||||
c, err := stun.Dial("udp", "stun.l.google.com:19302")
|
c, err := stun.Dial("udp", "stun.l.google.com:19302")
|
||||||
@@ -63,6 +90,15 @@ func GetPublicIP() (net.IP, error) {
|
|||||||
return xorAddr.IP, nil
|
return xorAddr.IP, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func IsIP(host string) bool {
|
||||||
|
for _, i := range host {
|
||||||
|
if i >= 'A' {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func MimeType(codec *streamer.Codec) string {
|
func MimeType(codec *streamer.Codec) string {
|
||||||
switch codec.Name {
|
switch codec.Name {
|
||||||
case streamer.CodecH264:
|
case streamer.CodecH264:
|
||||||
|
@@ -69,6 +69,7 @@
|
|||||||
// '<a href="video.html?src={name}">video</a>',
|
// '<a href="video.html?src={name}">video</a>',
|
||||||
'<a href="api/stream.mp4?src={name}">mp4</a>',
|
'<a href="api/stream.mp4?src={name}">mp4</a>',
|
||||||
'<a href="api/frame.mp4?src={name}">frame</a>',
|
'<a href="api/frame.mp4?src={name}">frame</a>',
|
||||||
|
`<a href="rtsp://${location.hostname}:8554/{name}">rtsp</a>`,
|
||||||
'<a href="api/streams?src={name}">info</a>',
|
'<a href="api/streams?src={name}">info</a>',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user